ZDDC/zddc/internal/zddc/scan_test.go
ZDDC e4e0fedaa2 refactor(history): store under .zddc.d/history/; drop .history carve-out + dead .devshell
Consolidate edit-history bookkeeping under the single reserved .zddc.d/
sidecar (where tokens + access logs already live), instead of its own
top-level .history/ dot-name:

- history.go: record + text history now write/read <dir>/.zddc.d/history/<stem>/
  (was <dir>/.history/<stem>/). Const renamed .history → .zddc.d/history and
  unexported (the only external user was the dispatch carve-out). The history
  VIEWER endpoints (<record>.yaml?history=1, <file>?history=…) read it
  server-side, so they keep working for anyone with read on the live file;
  the raw store is bookkeeping, blocked by the existing dot-prefix guard.
- main.go: drop the .history GET carve-out (b9ebee7) — superseded; history is
  reached via the viewer, not raw browsing. Reword the guard comment to
  "reserve .zddc.d/ bookkeeping" (Part B will replace the blanket block with a
  .zddc.d/ admin-fence).
- Delete dead .devshell references (the dev-shell was dropped from the chart):
  guard comment, paths.go comment, test fixtures/cases (→ .zddc.d), and docs.

This is Part A of the approved plan: ship history in its permanent home so we
never migrate it twice. Tests updated to the new paths; the obsolete
TestDispatchHistoryReadCarveOut is removed (raw-block covered by
TestDispatchHidesDotPrefixedSegments, viewer by mdhistory_test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:48:41 -05:00

125 lines
3.4 KiB
Go

package zddc
import (
"os"
"path/filepath"
"testing"
)
// scanFixture lays down a small tree with a few .zddc files and reserved-
// prefix directories that should be pruned. Returns the fsRoot.
func scanFixture(t *testing.T) string {
t.Helper()
root := t.TempDir()
mk := func(rel, body string) {
path := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
}
mk(".zddc", "admins:\n - alice@example.com\n")
mk("ProjectA/.zddc", "title: A\n")
mk("ProjectA/Sub/.zddc", "title: A-sub\n")
mk("ProjectB/.zddc", "title: B\n")
// Reserved-prefix subtrees must be pruned.
mk(".zddc.d/.zddc", "title: hidden\n")
mk("_template/.zddc", "title: scaffold\n")
return root
}
func TestScanZddcFiles_FindsAllAndPrunesReservedPrefixes(t *testing.T) {
InvalidateScanCache()
root := scanFixture(t)
dirs, err := ScanZddcFiles(root)
if err != nil {
t.Fatalf("ScanZddcFiles: %v", err)
}
want := []string{
root,
filepath.Join(root, "ProjectA"),
filepath.Join(root, "ProjectA", "Sub"),
filepath.Join(root, "ProjectB"),
}
if len(dirs) != len(want) {
t.Fatalf("got %d dirs, want %d: %v", len(dirs), len(want), dirs)
}
for i, w := range want {
if dirs[i] != w {
t.Errorf("dirs[%d] = %q, want %q", i, dirs[i], w)
}
}
}
// Cache hit returns the same slice header as the first call (proves the
// walk wasn't repeated). Cache miss after invalidation returns a freshly-
// allocated slice.
func TestScanZddcFiles_CachesAndInvalidates(t *testing.T) {
InvalidateScanCache()
root := scanFixture(t)
first, err := ScanZddcFiles(root)
if err != nil {
t.Fatalf("first call: %v", err)
}
second, err := ScanZddcFiles(root)
if err != nil {
t.Fatalf("second call: %v", err)
}
if &first[0] != &second[0] {
t.Errorf("expected cached slice reuse — got fresh allocation")
}
// Add a new .zddc file. Without invalidation the cache returns stale data.
newDir := filepath.Join(root, "ProjectC")
if err := os.MkdirAll(newDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(newDir, ".zddc"), []byte("title: C\n"), 0o644); err != nil {
t.Fatalf("write .zddc: %v", err)
}
stale, _ := ScanZddcFiles(root)
if len(stale) != len(first) {
t.Errorf("expected stale cached result before invalidation; got %d entries (first had %d)", len(stale), len(first))
}
InvalidateScanCache()
fresh, err := ScanZddcFiles(root)
if err != nil {
t.Fatalf("post-invalidate: %v", err)
}
if len(fresh) != len(first)+1 {
t.Errorf("expected fresh result with new entry; got %v", fresh)
}
wantNew := filepath.Join(root, "ProjectC")
found := false
for _, d := range fresh {
if d == wantNew {
found = true
break
}
}
if !found {
t.Errorf("fresh scan missing %q; got %v", wantNew, fresh)
}
}
// Walk errors must NOT poison the cache — a transient permissions blip
// during the walk should leave the cache empty so the next call retries.
func TestScanZddcFiles_DoesNotCacheOnError(t *testing.T) {
InvalidateScanCache()
bogus := filepath.Join(t.TempDir(), "does-not-exist")
_, err := ScanZddcFiles(bogus)
if err == nil {
t.Fatalf("expected error walking nonexistent path")
}
// Cache should not have stored anything.
if _, ok := scanCache.Load(filepath.Clean(bogus)); ok {
t.Errorf("cache populated despite walk error")
}
}