PlanRaw: an original that was archived but never removed (it has a .processed marker AND a byte-identical copy under raw/processed/) is deleted with its marker; a marker without a note is deleted; an unmarked note, a note without an identical archive copy, and AutoTunnel's tunnel-candidates-* review queue are never touched. The archive key flattens the whole path so raw/x.md and wiki/sources/x.md cannot overwrite each other. Live dry run: 84 entries, 16 move, 53 delete (24 junk + 29 raw leftovers), 15 left. Mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
272 lines
10 KiB
Go
272 lines
10 KiB
Go
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))
|
|
}
|