package repl import ( "bytes" "context" "strings" "testing" "github.com/smallnest/pigo/internal/dream" ) // sampleReport is a non-trivial Report used across the renderer tests: every // counter is distinct so a mis-wired field is caught, and Notes/Reconciled are // populated so their rendering is exercised too. func sampleReport() dream.Report { r := dream.Report{ Merged: 2, Deduped: 1, PathsCleaned: 3, Pruned: 4, Distilled: 5, BytesBefore: 4096, BytesAfter: 2048, FilesBefore: 9, FilesAfter: 7, Notes: []string{"pruned stale entry X", "no new sessions"}, } r.Reconciled.Indexed = 6 r.Reconciled.Pruned = 1 return r } func TestRenderReportTable(t *testing.T) { var buf bytes.Buffer RenderReportTable(&buf, sampleReport()) got := buf.String() // Key counts must appear (label + value). for _, want := range []string{ "dream report", "merged", "deduped", "paths-cleaned", "pruned", "distilled", "4.0KB → 2.0KB", // bytes before→after "9 → 7", // files before→after "indexed 6, pruned 1", "pruned stale entry X", "no new sessions", } { if !strings.Contains(got, want) { t.Errorf("full table missing %q\n---\n%s", want, got) } } // A non-dry-run report must NOT carry the DRY-RUN label. if strings.Contains(got, "DRY-RUN") { t.Errorf("non-dry-run table should not show DRY-RUN label:\n%s", got) } } func TestRenderReportTableDryRun(t *testing.T) { r := sampleReport() r.DryRun = true var buf bytes.Buffer RenderReportTable(&buf, r) got := buf.String() if !strings.Contains(got, "DRY-RUN") { t.Errorf("dry-run table must show DRY-RUN label:\n%s", got) } if !strings.Contains(got, "nothing written") { t.Errorf("dry-run table should state nothing was written:\n%s", got) } } func TestRenderReportLine(t *testing.T) { got := RenderReportLine(sampleReport()) for _, want := range []string{ "dream:", "merged 2", "deduped 1", "paths-cleaned 3", "pruned 4", "distilled 5", "4.0KB→2.0KB", "9→7 files", } { if !strings.Contains(got, want) { t.Errorf("one-line summary missing %q: %q", want, got) } } if strings.Contains(got, "DRY-RUN") { t.Errorf("non-dry-run line should not show DRY-RUN: %q", got) } } func TestRenderReportLineDryRun(t *testing.T) { r := sampleReport() r.DryRun = true got := RenderReportLine(r) if !strings.Contains(got, "DRY-RUN") { t.Errorf("dry-run line must show DRY-RUN: %q", got) } } func TestRenderReportZeroValue(t *testing.T) { // The zero value is a valid "nothing changed" report and must render cleanly. var buf bytes.Buffer RenderReportTable(&buf, dream.Report{}) if line := RenderReportLine(dream.Report{}); !strings.Contains(line, "merged 0") { t.Errorf("zero-value line should render zero counts: %q", line) } if !strings.Contains(buf.String(), "0B → 0B") { t.Errorf("zero-value table should render 0B → 0B:\n%s", buf.String()) } } func TestFormatBytes(t *testing.T) { cases := []struct { in int64 want string }{ {0, "0B"}, {512, "512B"}, {1024, "1.0KB"}, {1536, "1.5KB"}, {1048576, "1.0MB"}, } for _, c := range cases { if got := formatBytes(c.in); got != c.want { t.Errorf("formatBytes(%d) = %q, want %q", c.in, got, c.want) } } } func TestDreamHasDryRun(t *testing.T) { cases := []struct { line string want bool }{ {"/dream", false}, {"/dream --dry-run", true}, {"/dream --dry-run", true}, {"/dream --dryrun", false}, {"/dream extra --dry-run", true}, } for _, c := range cases { if got := dreamHasDryRun(c.line); got != c.want { t.Errorf("dreamHasDryRun(%q) = %v, want %v", c.line, got, c.want) } } } // TestRunDreamRendersCannedReport exercises the parse+render path via the spawn // seam with a canned Report, without spawning a real LLM-backed dream. func TestRunDreamRendersCannedReport(t *testing.T) { orig := spawnDream t.Cleanup(func() { spawnDream = orig }) spawnDream = func(_ context.Context, _ string, dryRun bool) (dreamSubprocessResult, error) { r := sampleReport() r.DryRun = dryRun return dreamSubprocessResult{report: r}, nil } var buf bytes.Buffer runDream(&buf, replDeps{}, "/dream") got := buf.String() if !strings.Contains(got, "dream report") || !strings.Contains(got, "merged") { t.Errorf("runDream should render the full table:\n%s", got) } if strings.Contains(got, "DRY-RUN") { t.Errorf("non-dry-run runDream should not show DRY-RUN:\n%s", got) } } func TestRunDreamDryRunLabel(t *testing.T) { orig := spawnDream t.Cleanup(func() { spawnDream = orig }) spawnDream = func(_ context.Context, _ string, dryRun bool) (dreamSubprocessResult, error) { r := sampleReport() r.DryRun = dryRun return dreamSubprocessResult{report: r}, nil } var buf bytes.Buffer runDream(&buf, replDeps{}, "/dream --dry-run") if !strings.Contains(buf.String(), "DRY-RUN") { t.Errorf("/dream --dry-run should render DRY-RUN label:\n%s", buf.String()) } } // TestRunDreamFailure asserts a subprocess failure (exit 1 / unparseable stdout) // prints a clear error and does not crash the REPL (SPEC §6.1). func TestRunDreamFailure(t *testing.T) { orig := spawnDream t.Cleanup(func() { spawnDream = orig }) spawnDream = func(_ context.Context, _ string, _ bool) (dreamSubprocessResult, error) { return dreamSubprocessResult{}, errFake } var buf bytes.Buffer runDream(&buf, replDeps{}, "/dream") if !strings.Contains(buf.String(), "dream failed") { t.Errorf("failed run should print an error:\n%s", buf.String()) } } var errFake = &fakeErr{} type fakeErr struct{} func (*fakeErr) Error() string { return "boom" } func TestDreamLockHeld(t *testing.T) { // No memory root → never held. if dreamLockHeld("") { t.Fatal("empty memoryRoot must read as not held") } root := t.TempDir() // No lock file yet → not held. if dreamLockHeld(root) { t.Fatal("missing lock must read as not held") } // Acquire a real lock via the dream package → held. lock, err := dream.AcquireLock(root) if err != nil { t.Fatalf("AcquireLock: %v", err) } if !dreamLockHeld(root) { t.Error("a freshly acquired lock must read as held") } // Release → not held. if err := lock.Release(); err != nil { t.Fatalf("Release: %v", err) } if dreamLockHeld(root) { t.Error("a released lock must read as not held") } } // TestRunDreamLockedNotice asserts the manual command surfaces the locked // message and does NOT spawn when a live lock is present (SPEC §6.1). func TestRunDreamLockedNotice(t *testing.T) { root := t.TempDir() lock, err := dream.AcquireLock(root) if err != nil { t.Fatalf("AcquireLock: %v", err) } t.Cleanup(func() { _ = lock.Release() }) orig := spawnDream t.Cleanup(func() { spawnDream = orig }) spawned := false spawnDream = func(_ context.Context, _ string, _ bool) (dreamSubprocessResult, error) { spawned = true return dreamSubprocessResult{}, nil } var buf bytes.Buffer runDream(&buf, replDeps{memoryRoot: root}, "/dream") if spawned { t.Error("runDream must not spawn while a live lock is held") } if !strings.Contains(buf.String(), "已有 dream 在运行") { t.Errorf("locked run should print the locked notice:\n%s", buf.String()) } }