FindSlug returned the first hit, so a wing's own page of a name masked another page of the same name (the retired dirs are searched before the wings, and wings alphabetically). FindSlugs returns every match and callers refuse any that is not their own. Found by the legacy-move planner's own test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
157 lines
6.7 KiB
Go
157 lines
6.7 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func wingCfg(wing string, pages ...RawPage) Config {
|
|
return Config{
|
|
Wing: wing,
|
|
Schema: "# Schema",
|
|
Complete: func(context.Context, string, string) (string, error) {
|
|
return mustJSON(pages), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func sourceEntityConcept() []RawPage {
|
|
return []RawPage{
|
|
{Title: "Dark Factory Talk", Type: "source", Subtype: "video", Domain: "ai",
|
|
Content: "## Summary\n\nA talk.\n\n## Entities Mentioned\n\n[[Jev]]\n\n## Concepts Introduced or Reinforced\n\n[[Dark Factory]]\n"},
|
|
{Title: "Jev", Type: "entity", Subtype: "tool", Domain: "ai",
|
|
Content: "## Definition\n\nA decision model.\n\n## Sources\n"},
|
|
{Title: "Dark Factory", Type: "concept", Domain: "ai",
|
|
Content: "## Definition\n\nA fully automated build line.\n\n## Sources\n"},
|
|
}
|
|
}
|
|
|
|
func exists(dir, rel string) bool {
|
|
_, err := os.Stat(filepath.Join(dir, rel))
|
|
return err == nil
|
|
}
|
|
|
|
func read(t *testing.T, dir, rel string) string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(filepath.Join(dir, rel))
|
|
require.NoError(t, err)
|
|
return string(b)
|
|
}
|
|
|
|
// With a wing, the SOURCE belongs to the wing; concepts and entities are the
|
|
// shared cross-wing layer (wiki/concepts, wiki/entities) because their slug is
|
|
// the graph's global identity and they are cited from many wings.
|
|
func TestRun_WithAWingTheSourceIsPerWingAndConceptsEntitiesAreShared(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body of the talk", "dark-factory-talk", false)
|
|
require.NoError(t, err)
|
|
|
|
src := read(t, dir, "wiki/web-clip/sources/dark-factory-talk.md")
|
|
assert.Contains(t, src, "wing: web-clip")
|
|
assert.Contains(t, src, "hall: sources")
|
|
assert.True(t, exists(dir, "wiki/entities/jev.md"))
|
|
assert.True(t, exists(dir, "wiki/concepts/dark-factory.md"))
|
|
assert.NotContains(t, read(t, dir, "wiki/entities/jev.md"), "wing:", "a shared page belongs to no single wing")
|
|
assert.False(t, exists(dir, "wiki/web-clip/facts"), "no per-wing copy of a shared page")
|
|
assert.False(t, exists(dir, "wiki/sources"), "the retired source layout is not used")
|
|
}
|
|
|
|
func TestRun_WithAWingTheSharedPageLinksBackToTheSource(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body", "dark-factory-talk", false)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, read(t, dir, "wiki/entities/jev.md"), "[[dark-factory-talk|Dark Factory Talk]]")
|
|
}
|
|
|
|
func TestRun_TheSameEntityCitedFromTwoWingsIsOnePageWithBothSources(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "first body", "talk-one", false)
|
|
require.NoError(t, err)
|
|
// A wing must already exist to be named; create it the way a note would.
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki/homelab/facts"), 0o755))
|
|
|
|
again := []RawPage{
|
|
{Title: "Second Talk", Type: "source", Subtype: "video", Content: "## Summary\n\nMore.\n\n## Entities Mentioned\n\n[[The Jev]]\n"},
|
|
{Title: "The Jev", Type: "entity", Subtype: "tool", Content: "## Definition\n\nStill a decision model.\n\n## Sources\n"},
|
|
}
|
|
_, err = Run(context.Background(), wingCfg("homelab", again...), dir, "second body", "talk-two", false)
|
|
require.NoError(t, err)
|
|
|
|
files, err := filepath.Glob(filepath.Join(dir, "wiki/entities/*.md"))
|
|
require.NoError(t, err)
|
|
assert.Len(t, files, 1, "one entity page, matched by title with the leading article ignored")
|
|
jev := read(t, dir, "wiki/entities/jev.md")
|
|
assert.Contains(t, jev, "talk-one")
|
|
assert.Contains(t, jev, "talk-two")
|
|
assert.False(t, exists(dir, "wiki/homelab/facts/jev.md"), "no second copy in the second wing")
|
|
}
|
|
|
|
// 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) {
|
|
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")
|
|
assert.False(t, exists(dir, "wiki/homelab/sources/talk.md"))
|
|
}
|
|
|
|
func TestRun_ASourceSlugAlreadyInTheRetiredLayoutIsRefused(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))
|
|
|
|
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body", "talk", false)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already used")
|
|
}
|
|
|
|
func TestRun_ReIngestingTheSameSourceIntoItsOwnWingIsNotACollision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body one", "talk", false)
|
|
require.NoError(t, err)
|
|
_, err = Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body two, edited", "talk", false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestRun_WithAWingRejectsANameThatIsNotAPlainWing(t *testing.T) {
|
|
for _, bad := range []string{"../escape", "a/b", "Radar", "has space", "."} {
|
|
t.Run(bad, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := Run(context.Background(), wingCfg(bad, sourceEntityConcept()...), dir, "body", "talk", false)
|
|
require.Error(t, err)
|
|
assert.Len(t, listAll(t, dir), 1, "nothing is written for a rejected wing (only the temp dir itself exists)")
|
|
})
|
|
}
|
|
}
|
|
|
|
func listAll(t *testing.T, dir string) []string {
|
|
t.Helper()
|
|
var out []string
|
|
_ = filepath.WalkDir(dir, func(p string, _ os.DirEntry, _ error) error { out = append(out, p); return nil })
|
|
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) {
|
|
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))
|
|
|
|
_, err := Run(context.Background(), wingCfg("web-clip", sourceEntityConcept()...), dir, "body", "talk", false)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already used")
|
|
}
|