feat(legacymove): clear the retired wiki/sources layout with an archive and a rollback (#209)
Videos and articles move to the default wing (wing/hall stamped, body and links byte-preserved); auto-tunnel review logs are deleted; notes, projects and untyped pages stay. Plan is read-only; Apply archives every page it touches to archive/wing-migration-<date>/ (outside raw/) before changing anything, creates destinations O_EXCL and removes sources only after; Rollback restores byte for byte. concepts/ and entities/ are the shared layer and are untouched. Dry run against the live brain: 16 move, 24 delete, 15 left. Mutation-checked: junk guard, type filter, slug collision, no-overwrite, archive-before-change, action gate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user