fix(pipeline): same-named sources in two wings, entity/source slug clashes, model-chosen types (review of #211)
- a source whose slug is taken by anything but this wing's own page gets a wing-qualified, stable slug (talk-homelab) instead of failing the ingest - an extracted entity/concept whose slug belongs to a different page is skipped with a warning, so one page can no longer lock a source out of re-ingest - in a wing only source/concept/entity are valid page types; the model (and so the clip) can no longer choose wiki/<type>/ directories Mutation-checked; stable-slug choice pinned directly because title resolution masks it at the Run level. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -92,27 +93,77 @@ func TestRun_TheSameEntityCitedFromTwoWingsIsOnePageWithBothSources(t *testing.T
|
||||
}
|
||||
|
||||
// A source slug is the graph's identity too: a second wing must not take over
|
||||
// the slug of a source that lives elsewhere.
|
||||
func TestRun_ASourceSlugAlreadyUsedInAnotherWingIsRefused(t *testing.T) {
|
||||
// the slug of a page that lives elsewhere. Two wings dropping a file of the same
|
||||
// name (notes.md, readme.md) is ordinary, so the second source gets a distinct,
|
||||
// stable slug instead of failing.
|
||||
func TestRun_ASourceWhoseSlugIsTakenElsewhereGetsADistinctStableSlug(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body", "talk", false)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/homelab/facts"), 0o755))
|
||||
|
||||
_, err = Run(context.Background(), wingCfg("homelab", sourceEntityConcept()...), dir, "different body", "talk", false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already used")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists(dir, "wiki/web-clip/sources/talk.md"), "the first wing's page is untouched")
|
||||
assert.True(t, exists(dir, "wiki/homelab/sources/talk-homelab.md"), "the second gets a wing-qualified slug")
|
||||
assert.False(t, exists(dir, "wiki/homelab/sources/talk.md"))
|
||||
|
||||
_, err = Run(context.Background(), wingCfg("homelab", sourceEntityConcept()...), dir, "different body, edited", "talk", false)
|
||||
require.NoError(t, err)
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "wiki/homelab/sources/*.md"))
|
||||
assert.Len(t, files, 1, "re-ingesting finds the same page, it does not make talk-homelab-2")
|
||||
}
|
||||
|
||||
func TestRun_ASourceSlugAlreadyInTheRetiredLayoutIsRefused(t *testing.T) {
|
||||
func TestRun_ASourceWhoseSlugIsTakenByARetiredPageOrASharedPageGetsADistinctSlug(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/sources"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "wiki/sources/talk.md"), []byte("---\ntitle: Talk\n---\nold"), 0o644))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/entities"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "wiki/entities/other.md"), []byte("---\ntitle: Other\n---\nx"), 0o644))
|
||||
|
||||
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body", "talk", false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already used")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists(dir, "wiki/web-clip/sources/talk-web-clip.md"))
|
||||
assert.Equal(t, "---\ntitle: Talk\n---\nold", read(t, dir, "wiki/sources/talk.md"), "the retired page is untouched")
|
||||
}
|
||||
|
||||
// The other direction: a model-extracted entity or concept whose slug is a
|
||||
// source's slug in some wing would lock that source out of re-ingest. The page
|
||||
// is skipped with a warning; the ingest itself succeeds.
|
||||
func TestRun_AnEntityWhoseSlugIsASourceElsewhereIsSkippedNotWritten(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, err := Run(context.Background(), wingCfg("web-clip", []RawPage{
|
||||
{Title: "Jev", Type: "source", Subtype: "video", Content: "## Summary\n\nA clip titled Jev.\n"},
|
||||
}...), dir, "body", "jev", false)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/homelab/facts"), 0o755))
|
||||
|
||||
res, err := Run(context.Background(), wingCfg("homelab", sourceEntityConcept()...), dir, "another body", "other-talk", false)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists(dir, "wiki/entities/jev.md"), "the entity would take the source's slug")
|
||||
assert.Contains(t, strings.Join(res.Warnings, "\n"), "jev")
|
||||
|
||||
_, err = Run(context.Background(), wingCfg("web-clip", []RawPage{
|
||||
{Title: "Jev", Type: "source", Subtype: "video", Content: "## Summary\n\nEdited.\n"},
|
||||
}...), dir, "body, edited", "jev", false)
|
||||
require.NoError(t, err, "the first wing can still re-ingest its own source")
|
||||
}
|
||||
|
||||
// The type comes from the model, so from the clip: in a wing it may only be one
|
||||
// of the three the layout has a place for.
|
||||
func TestRun_WithAWingAModelChosenTypeCannotWriteOutsideTheLayout(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/alpha/facts"), 0o755))
|
||||
pages := append(sourceEntityConcept(),
|
||||
RawPage{Title: "Planted", Type: "sources", Content: "## x\n\nplanted\n"},
|
||||
RawPage{Title: "Loose", Type: "alpha", Content: "## x\n\nloose\n"},
|
||||
RawPage{Title: "Sneaky", Type: "homelab", Content: "## x\n\nsneaky\n"})
|
||||
res, err := Run(context.Background(), wingCfg("web-clip", pages...), dir, "body", "talk", false)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists(dir, "wiki/sources/planted.md"))
|
||||
assert.False(t, exists(dir, "wiki/alpha/loose.md"))
|
||||
assert.False(t, exists(dir, "wiki/homelab"))
|
||||
assert.Contains(t, strings.Join(res.Warnings, "\n"), "invalid type")
|
||||
}
|
||||
|
||||
func TestRun_ReIngestingTheSameSourceIntoItsOwnWingIsNotACollision(t *testing.T) {
|
||||
@@ -141,16 +192,44 @@ func listAll(t *testing.T, dir string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// The first match must not hide a second one: this wing's own page of the name
|
||||
// exists AND a page of the same name lives elsewhere.
|
||||
func TestRun_ASlugUsedInTwoPlacesIsRefusedEvenWhenTheOwnPageIsOneOfThem(t *testing.T) {
|
||||
// Title resolution can mask a wrong slug choice at the Run level, so the
|
||||
// stability of the choice is pinned directly: the page this wing made earlier
|
||||
// under the qualified slug is "its own" and is found again.
|
||||
func TestResolveSourceSlug(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/web-clip/sources"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "wiki/web-clip/sources/talk.md"), []byte("---\ntitle: Talk\n---\nown"), 0o644))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/entities"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "wiki/entities/talk.md"), []byte("---\ntitle: Talk\n---\nan entity"), 0o644))
|
||||
for _, rel := range []string{
|
||||
"wiki/web-clip/sources/talk.md",
|
||||
"wiki/homelab/sources/talk-homelab.md",
|
||||
"wiki/entities/kube.md",
|
||||
} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, rel)), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, rel), []byte("---\ntitle: x\n---\n"), 0o644))
|
||||
}
|
||||
tests := []struct {
|
||||
name, wing, base, want string
|
||||
}{
|
||||
{"free slug is used as is", "homelab", "fresh", "fresh"},
|
||||
{"own page under the plain slug", "web-clip", "talk", "talk"},
|
||||
{"taken elsewhere: wing-qualified", "tapir", "talk", "talk-tapir"},
|
||||
{"own earlier qualified page is found again", "homelab", "talk", "talk-homelab"},
|
||||
{"taken by a shared page", "tapir", "kube", "kube-tapir"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := resolveSourceSlug(dir, tt.wing, tt.base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body", "talk", false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already used")
|
||||
func TestResolveSourceSlug_AFurtherClashGetsANumber(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, rel := range []string{"wiki/a/sources/talk.md", "wiki/entities/talk-b.md"} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, rel)), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, rel), []byte("x"), 0o644))
|
||||
}
|
||||
got, err := resolveSourceSlug(dir, "b", "talk")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "talk-b-2", got)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user