package legacymove_test import ( "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mathiasbq/hyperguild/ingestion/internal/legacymove" ) func page(t *testing.T, dir, rel, body string) { t.Helper() full := filepath.Join(dir, rel) require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755)) require.NoError(t, os.WriteFile(full, []byte(body), 0o644)) } func src(typ string) string { return "---\ntitle: T\ntype: " + typ + "\n---\n\n## Summary\n\nBody with [[a-link|A]] kept as is.\n" } func byFrom(items []legacymove.Item) map[string]legacymove.Item { m := map[string]legacymove.Item{} for _, it := range items { m[it.From] = it } return m } func TestPlan_VideosAndArticlesMoveToTheDefaultWing(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) page(t, dir, "wiki/sources/an-article.md", src("article")) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) m := byFrom(items) assert.Equal(t, legacymove.Move, m["wiki/sources/a-talk.md"].Action) assert.Equal(t, "wiki/web-clip/sources/a-talk.md", m["wiki/sources/a-talk.md"].To) assert.Equal(t, legacymove.Move, m["wiki/sources/an-article.md"].Action) } func TestPlan_NotesProjectsAndUntypedPagesStayWhereTheyAre(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-note.md", src("note")) page(t, dir, "wiki/sources/a-project.md", src("project")) page(t, dir, "wiki/sources/untyped.md", "---\ntitle: T\n---\nbody") page(t, dir, "wiki/sources/_index.md", "index") items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) m := byFrom(items) for _, f := range []string{"a-note", "a-project", "untyped"} { assert.Equal(t, legacymove.Skip, m["wiki/sources/"+f+".md"].Action, f) } _, listed := m["wiki/sources/_index.md"] assert.False(t, listed, "the index is not a page") } func TestPlan_AutoTunnelLogsAreDeleted(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/tunnel-candidates-2026-06-14.md", src("video")) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) assert.Equal(t, legacymove.Delete, byFrom(items)["wiki/sources/tunnel-candidates-2026-06-14.md"].Action, "never moved, whatever the type says") } func TestPlan_ASlugAlreadyUsedElsewhereIsSkippedNotOverwritten(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) page(t, dir, "wiki/entities/a-talk.md", "an entity with the same slug") items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) it := byFrom(items)["wiki/sources/a-talk.md"] assert.Equal(t, legacymove.Skip, it.Action) assert.Contains(t, it.Reason, "already used") } func TestPlan_ADefaultWingThatIsNotAPlainNameIsRefused(t *testing.T) { _, err := legacymove.Plan(t.TempDir(), "../x") assert.Error(t, err) } func TestApply_MovesStampsAndArchivesEverythingItTouches(t *testing.T) { dir := t.TempDir() talk := src("video") junk := "tunnel log" page(t, dir, "wiki/sources/a-talk.md", talk) page(t, dir, "wiki/sources/tunnel-candidates-2026-06-14.md", junk) page(t, dir, "wiki/sources/a-note.md", src("note")) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) res, err := legacymove.Apply(dir, items, time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC)) require.NoError(t, err) assert.Equal(t, 1, res.Moved) assert.Equal(t, 1, res.Deleted) moved, err := os.ReadFile(filepath.Join(dir, "wiki/web-clip/sources/a-talk.md")) require.NoError(t, err) assert.Contains(t, string(moved), "wing: web-clip") assert.Contains(t, string(moved), "hall: sources") assert.Contains(t, string(moved), "Body with [[a-link|A]] kept as is.", "body and links are byte-preserved") for _, gone := range []string{"wiki/sources/a-talk.md", "wiki/sources/tunnel-candidates-2026-06-14.md"} { _, statErr := os.Stat(filepath.Join(dir, gone)) assert.ErrorIs(t, statErr, os.ErrNotExist, gone) } _, statErr := os.Stat(filepath.Join(dir, "wiki/sources/a-note.md")) assert.NoError(t, statErr, "a skipped page is left exactly where it is") again, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) for _, it := range again { assert.Equal(t, legacymove.Skip, it.Action, "nothing is left to do: %s", it.From) } } func TestRollback_RestoresEveryTouchedPageByteForByte(t *testing.T) { dir := t.TempDir() talk := src("video") junk := "tunnel log" page(t, dir, "wiki/sources/a-talk.md", talk) page(t, dir, "wiki/sources/tunnel-candidates-2026-06-14.md", junk) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) res, err := legacymove.Apply(dir, items, time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC)) require.NoError(t, err) require.NoError(t, legacymove.Rollback(dir, res.ArchiveDir)) got, err := os.ReadFile(filepath.Join(dir, "wiki/sources/a-talk.md")) require.NoError(t, err) assert.Equal(t, talk, string(got)) got, err = os.ReadFile(filepath.Join(dir, "wiki/sources/tunnel-candidates-2026-06-14.md")) require.NoError(t, err) assert.Equal(t, junk, string(got)) _, statErr := os.Stat(filepath.Join(dir, "wiki/web-clip/sources/a-talk.md")) assert.ErrorIs(t, statErr, os.ErrNotExist, "the moved copy is removed") } func TestApply_NeverOverwritesAndKeepsTheSourceIfTheDestinationAppearsLate(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) page(t, dir, "wiki/web-clip/sources/a-talk.md", "appeared after the plan") _, err = legacymove.Apply(dir, items, time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC)) require.Error(t, err) got, _ := os.ReadFile(filepath.Join(dir, "wiki/web-clip/sources/a-talk.md")) assert.Equal(t, "appeared after the plan", string(got)) _, statErr := os.Stat(filepath.Join(dir, "wiki/sources/a-talk.md")) assert.NoError(t, statErr, "the source survives") } func TestApply_TheArchiveIsOutsideRawSoTheWatcherNeverIngestsIt(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/tunnel-candidates-2026-06-14.md", "x") items, _ := legacymove.Plan(dir, "web-clip") res, err := legacymove.Apply(dir, items, time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC)) require.NoError(t, err) assert.False(t, strings.Contains(res.ArchiveDir, "/raw"), res.ArchiveDir) assert.True(t, strings.HasPrefix(res.ArchiveDir, "archive/"), res.ArchiveDir) } // --- raw/ leftovers from before inbox zero --- func rawFile(t *testing.T, dir, rel, body string) { page(t, dir, rel, body) } func TestPlanRaw_AnArchivedOriginalAndItsMarkerAreDeleted(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/clip.md", "the clip text") rawFile(t, dir, "raw/clip.md.processed", "2026-10-19") rawFile(t, dir, "raw/processed/2026-10-19/clip.md", "the clip text") items, err := legacymove.PlanRaw(dir) require.NoError(t, err) m := byFrom(items) assert.Equal(t, legacymove.Delete, m["raw/clip.md"].Action) assert.Equal(t, legacymove.Delete, m["raw/clip.md.processed"].Action) } func TestPlanRaw_AnOriginalWhoseArchiveCopyDiffersIsLeftAlone(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/clip.md", "the clip text, edited since") rawFile(t, dir, "raw/clip.md.processed", "2026-10-19") rawFile(t, dir, "raw/processed/2026-10-19/clip.md", "the clip text") items, err := legacymove.PlanRaw(dir) require.NoError(t, err) m := byFrom(items) assert.Equal(t, legacymove.Skip, m["raw/clip.md"].Action, "no identical archive copy: not ours to delete") assert.Contains(t, m["raw/clip.md"].Reason, "no identical copy") _, hasMarker := m["raw/clip.md.processed"] assert.False(t, hasMarker, "the marker of a note we keep is kept") } func TestPlanRaw_ACopyUnderATimestampedNameCounts(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/clip.md", "same text") rawFile(t, dir, "raw/clip.md.processed", "d") rawFile(t, dir, "raw/processed/2026-10-19/clip-180200.md", "same text") rawFile(t, dir, "raw/processed/2026-10-19/clip-2.md", "other text") items, err := legacymove.PlanRaw(dir) require.NoError(t, err) assert.Equal(t, legacymove.Delete, byFrom(items)["raw/clip.md"].Action) } func TestPlanRaw_AnOrphanMarkerIsDeleted(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/gone.md.processed", "2026-06-16") items, err := legacymove.PlanRaw(dir) require.NoError(t, err) assert.Equal(t, legacymove.Delete, byFrom(items)["raw/gone.md.processed"].Action) } func TestPlanRaw_TheAutoTunnelReviewQueueAndANoteWithoutAMarkerAreNotTouched(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/tunnel-candidates-2026-06-14.md", "AutoTunnel's own queue") rawFile(t, dir, "raw/tunnel-candidates-2026-06-14.md.processed", "x") rawFile(t, dir, "raw/fresh.md", "an unprocessed clip") rawFile(t, dir, "raw/processed/2026-10-19/fresh.md", "an unprocessed clip") // same bytes, but no marker: still in the inbox items, err := legacymove.PlanRaw(dir) require.NoError(t, err) m := byFrom(items) for _, f := range []string{"raw/tunnel-candidates-2026-06-14.md", "raw/tunnel-candidates-2026-06-14.md.processed", "raw/fresh.md"} { _, listed := m[f] assert.False(t, listed, "%s is not a leftover", f) } } func TestApplyAndRollback_HandleRawLeftoversAndNeverCollideWithSourcesOfTheSameName(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/same.md", "raw text") rawFile(t, dir, "raw/same.md.processed", "d") rawFile(t, dir, "raw/processed/2026-10-19/same.md", "raw text") page(t, dir, "wiki/sources/same.md", src("video")) // a wiki source with the same basename rawItems, err := legacymove.PlanRaw(dir) require.NoError(t, err) srcItems, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) res, err := legacymove.Apply(dir, append(rawItems, srcItems...), time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC)) require.NoError(t, err) for _, gone := range []string{"raw/same.md", "raw/same.md.processed", "wiki/sources/same.md"} { _, statErr := os.Stat(filepath.Join(dir, gone)) assert.ErrorIs(t, statErr, os.ErrNotExist, gone) } _, statErr := os.Stat(filepath.Join(dir, "raw/processed/2026-10-19/same.md")) assert.NoError(t, statErr, "the archive copy in raw/processed is never touched") require.NoError(t, legacymove.Rollback(dir, res.ArchiveDir)) got, err := os.ReadFile(filepath.Join(dir, "raw/same.md")) require.NoError(t, err) assert.Equal(t, "raw text", string(got), "the raw note and the wiki source of one name did not overwrite each other in the archive") got, err = os.ReadFile(filepath.Join(dir, "wiki/sources/same.md")) require.NoError(t, err) assert.Equal(t, src("video"), string(got)) } // --- review of #213 --- var day = time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC) // F1: a second Apply on the same day must not touch the first run's archive. func TestApply_ASecondRunTheSameDayGetsItsOwnArchiveAndKeepsTheFirst(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) page(t, dir, "wiki/sources/tunnel-candidates-1.md", "FIRST tunnel bytes") items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) first, err := legacymove.Apply(dir, items, day) require.NoError(t, err) page(t, dir, "wiki/sources/tunnel-candidates-1.md", "SECOND tunnel bytes") // same name reappears items2, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) second, err := legacymove.Apply(dir, items2, day) require.NoError(t, err) assert.NotEqual(t, first.ArchiveDir, second.ArchiveDir) got, err := os.ReadFile(filepath.Join(dir, first.ArchiveDir, "originals", "wiki__sources__tunnel-candidates-1.md")) require.NoError(t, err) assert.Equal(t, "FIRST tunnel bytes", string(got), "the first run's archived original is intact") require.NoError(t, legacymove.Rollback(dir, first.ArchiveDir)) _, statErr := os.Stat(filepath.Join(dir, "wiki/sources/a-talk.md")) assert.NoError(t, statErr, "the first run's move is still rolled back by its own manifest") } // F2: Plan decided on the bytes it saw. Apply must not delete different bytes. func TestApply_ARawNoteThatChangedSincePlanningIsNotDeleted(t *testing.T) { dir := t.TempDir() rawFile(t, dir, "raw/a.md", "bytes that have an archive copy") rawFile(t, dir, "raw/a.md.processed", "d") rawFile(t, dir, "raw/processed/2026-10-19/a.md", "bytes that have an archive copy") items, err := legacymove.PlanRaw(dir) require.NoError(t, err) rawFile(t, dir, "raw/a.md", "NEW UNPROCESSED BYTES") // dropped again before Apply runs res, err := legacymove.Apply(dir, items, day) require.NoError(t, err) got, err := os.ReadFile(filepath.Join(dir, "raw/a.md")) require.NoError(t, err, "the changed note is still in the inbox") assert.Equal(t, "NEW UNPROCESSED BYTES", string(got)) assert.Contains(t, strings.Join(res.Changed, ","), "raw/a.md") } func TestApply_AWikiSourceThatChangedSincePlanningIsNotMoved(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) page(t, dir, "wiki/sources/a-talk.md", src("video")+"\nan edit made after the plan\n") res, err := legacymove.Apply(dir, items, day) require.NoError(t, err) assert.Zero(t, res.Moved) got, _ := os.ReadFile(filepath.Join(dir, "wiki/sources/a-talk.md")) assert.Contains(t, string(got), "an edit made after the plan") assert.NotEmpty(t, res.Changed) } // F3: rollback checks everything first, changes nothing on a conflict, can be // repeated, and never throws away an edit. func TestRollback_RefusesBeforeChangingAnythingWhenATargetExists(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) page(t, dir, "wiki/sources/b-talk.md", src("video")) items, _ := legacymove.Plan(dir, "web-clip") res, err := legacymove.Apply(dir, items, day) require.NoError(t, err) page(t, dir, "wiki/sources/b-talk.md", "someone put a different b here") err = legacymove.Rollback(dir, res.ArchiveDir) require.Error(t, err) _, statErr := os.Stat(filepath.Join(dir, "wiki/sources/a-talk.md")) assert.ErrorIs(t, statErr, os.ErrNotExist, "nothing was restored: a was not touched either") _, statErr = os.Stat(filepath.Join(dir, "wiki/web-clip/sources/a-talk.md")) assert.NoError(t, statErr, "and the moved copies are still there") } func TestRollback_CanBeRunTwice(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) items, _ := legacymove.Plan(dir, "web-clip") res, err := legacymove.Apply(dir, items, day) require.NoError(t, err) require.NoError(t, legacymove.Rollback(dir, res.ArchiveDir)) require.NoError(t, legacymove.Rollback(dir, res.ArchiveDir), "a repeat finds everything already restored") got, _ := os.ReadFile(filepath.Join(dir, "wiki/sources/a-talk.md")) assert.Equal(t, src("video"), string(got)) } func TestRollback_DoesNotDeleteAMovedPageThatWasEditedSinceTheMove(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/a-talk.md", src("video")) items, _ := legacymove.Plan(dir, "web-clip") res, err := legacymove.Apply(dir, items, day) require.NoError(t, err) moved := filepath.Join(dir, "wiki/web-clip/sources/a-talk.md") b, _ := os.ReadFile(moved) require.NoError(t, os.WriteFile(moved, append(b, []byte("\nmy edit\n")...), 0o644)) err = legacymove.Rollback(dir, res.ArchiveDir) require.Error(t, err) got, readErr := os.ReadFile(moved) require.NoError(t, readErr) assert.Contains(t, string(got), "my edit", "the edit survives") _, statErr := os.Stat(filepath.Join(dir, "wiki/sources/a-talk.md")) assert.ErrorIs(t, statErr, os.ErrNotExist, "nothing was changed") } // F4: a name whose archive key cannot fit must be skipped at planning time. func TestPlanRaw_ANameTooLongToArchiveIsSkippedNotAbortedOn(t *testing.T) { dir := t.TempDir() long := strings.Repeat("a", 240) + ".md" rawFile(t, dir, "raw/"+long, "text") rawFile(t, dir, "raw/"+long+".processed", "d") rawFile(t, dir, "raw/processed/2026-10-19/"+long, "text") rawFile(t, dir, "raw/ok.md", "fine") rawFile(t, dir, "raw/ok.md.processed", "d") rawFile(t, dir, "raw/processed/2026-10-19/ok.md", "fine") items, err := legacymove.PlanRaw(dir) require.NoError(t, err) m := byFrom(items) assert.Equal(t, legacymove.Skip, m["raw/"+long].Action) assert.Contains(t, m["raw/"+long].Reason, "too long") assert.Equal(t, legacymove.Delete, m["raw/ok.md"].Action) _, err = legacymove.Apply(dir, items, day) require.NoError(t, err, "the long name does not wedge the run") _, statErr := os.Stat(filepath.Join(dir, "raw/"+long)) assert.NoError(t, statErr, "and it is left where it is") } // F6: do not stamp a page that already carries a wing or hall; only a top-level // type inside a closed frontmatter block decides. func TestPlan_AMovedPageIsNeverGivenDuplicateWingKeys(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/has-wing.md", "---\ntitle: T\ntype: video\nwing: old\nhall: facts\n---\nbody") items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) it := byFrom(items)["wiki/sources/has-wing.md"] assert.Equal(t, legacymove.Skip, it.Action) assert.Contains(t, it.Reason, "already has") } func TestPlan_OnlyATopLevelTypeInAClosedBlockClassifiesAPage(t *testing.T) { dir := t.TempDir() page(t, dir, "wiki/sources/nested.md", "---\ntitle: T\nmeta:\n type: video\n---\nbody") page(t, dir, "wiki/sources/blockscalar.md", "---\ntitle: T\ndesc: |\n type: article\n---\nbody") page(t, dir, "wiki/sources/unterminated.md", "---\ntitle: T\ntype: video\nbody with no closing fence") items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) m := byFrom(items) for _, f := range []string{"nested", "blockscalar", "unterminated"} { assert.Equal(t, legacymove.Skip, m["wiki/sources/"+f+".md"].Action, f) } } func TestPlan_AWikiSourceWithATooLongNameIsSkipped(t *testing.T) { dir := t.TempDir() long := strings.Repeat("a", 240) + ".md" page(t, dir, "wiki/sources/"+long, src("video")) items, err := legacymove.Plan(dir, "web-clip") require.NoError(t, err) it := byFrom(items)["wiki/sources/"+long] assert.Equal(t, legacymove.Skip, it.Action) assert.Contains(t, it.Reason, "too long") }