256 lines
10 KiB
Go
256 lines
10 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
)
|
|
|
|
func TestExecuteCleanSessionDeletesWorkAndSpoolButPreservesCache(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
workDir := artifacts.SessionWorkDirForCampaign(workspaceRoot, "sample-campaign", "2026-05-03")
|
|
spoolDir := artifacts.SessionSpoolDir(filepath.Join(workspaceRoot, "spool"), "sample-campaign", "2026-05-03")
|
|
cachePath, err := artifacts.S3AudioCachePath(filepath.Join(workspaceRoot, "cache"), "test-bucket", "dnd/campaigns/sample-campaign/sessions/2026-05-03/audio/alice.flac")
|
|
if err != nil {
|
|
t.Fatalf("S3AudioCachePath() error = %v", err)
|
|
}
|
|
mustWriteTestFile(t, filepath.Join(workDir, "manifest.json"), "{}")
|
|
mustWriteTestFile(t, filepath.Join(spoolDir, "run-1", "audio", "alice.flac"), "audio")
|
|
mustWriteTestFile(t, cachePath, "cached-audio")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
cleanAssertMissing(t, workDir)
|
|
cleanAssertMissing(t, spoolDir)
|
|
cleanAssertExists(t, cachePath)
|
|
if !strings.Contains(stdout.String(), "Cache: preserved") {
|
|
t.Fatalf("stdout = %q, want cache preserved", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteCleanSessionDryRunDeletesNothing(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
workDir := artifacts.SessionWorkDirForCampaign(workspaceRoot, "sample-campaign", "2026-05-03")
|
|
spoolDir := artifacts.SessionSpoolDir(filepath.Join(workspaceRoot, "spool"), "sample-campaign", "2026-05-03")
|
|
mustWriteTestFile(t, filepath.Join(workDir, "manifest.json"), "{}")
|
|
mustWriteTestFile(t, filepath.Join(spoolDir, "run-1", "audio", "alice.flac"), "audio")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--dry-run"}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
cleanAssertExists(t, workDir)
|
|
cleanAssertExists(t, spoolDir)
|
|
if !strings.Contains(stdout.String(), "Would delete:") {
|
|
t.Fatalf("stdout = %q, want dry-run delete plan", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteCleanMissingSessionPathsSucceeds(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "Missing:") {
|
|
t.Fatalf("stdout = %q, want missing path output", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteCleanSessionClearCacheRemovesOnlyS3AudioCache(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
if err := os.WriteFile(sessionPath, []byte(`session_id: 2026-05-03
|
|
inputs:
|
|
audio_s3:
|
|
prefix: audio/
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("write session: %v", err)
|
|
}
|
|
|
|
audioKey := "dnd/campaigns/sample-campaign/sessions/2026-05-03/audio/alice.flac"
|
|
fake := &storage.FakeBackend{}
|
|
fake.SeedObject(storage.FakeObject{Key: audioKey, Data: []byte("audio")})
|
|
var storeInitCalls int
|
|
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
|
|
|
cacheRoot := filepath.Join(workspaceRoot, "cache")
|
|
cachePath, err := artifacts.S3AudioCachePath(cacheRoot, "test-bucket", audioKey)
|
|
if err != nil {
|
|
t.Fatalf("S3AudioCachePath() error = %v", err)
|
|
}
|
|
otherCachePath, err := artifacts.S3AudioCachePath(cacheRoot, "test-bucket", "dnd/campaigns/other/sessions/2026-05-03/audio/bob.flac")
|
|
if err != nil {
|
|
t.Fatalf("S3AudioCachePath() error = %v", err)
|
|
}
|
|
mustWriteTestFile(t, cachePath, "cached-audio")
|
|
mustWriteTestFile(t, otherCachePath, "other-audio")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--clear-cache"}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
cleanAssertMissing(t, cachePath)
|
|
cleanAssertExists(t, otherCachePath)
|
|
if storeInitCalls != 1 {
|
|
t.Fatalf("object store init calls = %d, want 1", storeInitCalls)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCleanLocalAudioClearCacheIsNoop(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--clear-cache"}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "Cache: skipped (session does not use audio_s3)") {
|
|
t.Fatalf("stdout = %q, want local audio cache no-op", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteCleanAllDeletesWorkAndSpoolContentsButPreservesCache(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
|
|
workRoot := filepath.Join(workspaceRoot, "work")
|
|
spoolRoot := filepath.Join(workspaceRoot, "spool")
|
|
cachePath := filepath.Join(workspaceRoot, "cache", "keep.txt")
|
|
mustWriteTestFile(t, filepath.Join(workRoot, "sample-campaign", "2026-05-03", "manifest.json"), "{}")
|
|
mustWriteTestFile(t, filepath.Join(spoolRoot, "sample-campaign", "2026-05-03", "run-1", "audio", "alice.flac"), "audio")
|
|
mustWriteTestFile(t, cachePath, "cache")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "--config", pipelinePath, "--all"}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
cleanAssertMissing(t, workRoot)
|
|
cleanAssertExists(t, spoolRoot)
|
|
cleanAssertMissing(t, filepath.Join(spoolRoot, "sample-campaign"))
|
|
cleanAssertExists(t, cachePath)
|
|
}
|
|
|
|
func TestExecuteCleanAllClearCacheRemovesS3AudioNamespaceOnly(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
|
|
cacheRoot := filepath.Join(workspaceRoot, "cache")
|
|
audioCachePath, err := artifacts.S3AudioCachePath(cacheRoot, "test-bucket", "dnd/campaigns/sample-campaign/sessions/2026-05-03/audio/alice.flac")
|
|
if err != nil {
|
|
t.Fatalf("S3AudioCachePath() error = %v", err)
|
|
}
|
|
otherCachePath, err := artifacts.S3AudioCachePath(cacheRoot, "test-bucket", "other-root/campaigns/sample-campaign/sessions/2026-05-03/audio/alice.flac")
|
|
if err != nil {
|
|
t.Fatalf("S3AudioCachePath() error = %v", err)
|
|
}
|
|
mustWriteTestFile(t, audioCachePath, "cached-audio")
|
|
mustWriteTestFile(t, otherCachePath, "other-cache")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "--config", pipelinePath, "--all", "--clear-cache"}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
cleanAssertMissing(t, audioCachePath)
|
|
cleanAssertExists(t, otherCachePath)
|
|
}
|
|
|
|
func TestExecuteCleanAllRejectsSessionScopedFlags(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--all"}, &stdout, &stderr)
|
|
if code == 0 {
|
|
t.Fatal("exit code = 0, want non-zero")
|
|
}
|
|
if !strings.Contains(stderr.String(), "--all cannot be combined") {
|
|
t.Fatalf("stderr = %q, want --all conflict", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestCleanRequiresSessionID(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean"}, &stdout, &stderr)
|
|
if code == 0 {
|
|
t.Fatal("exit code = 0, want non-zero")
|
|
}
|
|
if !strings.Contains(stderr.String(), "session_id is required unless --all is set") {
|
|
t.Fatalf("stderr = %q, want missing session-id", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestCleanRejectsUnsafeTargets(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
if err := reportCleanScopedDir(&bytes.Buffer{}, root, filepath.Join(outside, "target"), "test.outside", false); err == nil {
|
|
t.Fatal("outside target error = nil, want error")
|
|
}
|
|
if err := reportCleanScopedDir(&bytes.Buffer{}, root, root, "test.root", false); err == nil {
|
|
t.Fatal("root target error = nil, want error")
|
|
}
|
|
filePath := filepath.Join(root, "file.txt")
|
|
mustWriteTestFile(t, filePath, "file")
|
|
if err := reportCleanScopedDir(&bytes.Buffer{}, root, filePath, "test.file", false); err == nil {
|
|
t.Fatal("file target error = nil, want error")
|
|
}
|
|
symlinkPath := filepath.Join(root, "link")
|
|
if err := os.Symlink(filepath.Join(root, "missing"), symlinkPath); err != nil {
|
|
t.Fatalf("Symlink() error = %v", err)
|
|
}
|
|
if err := reportCleanScopedDir(&bytes.Buffer{}, root, symlinkPath, "test.symlink", false); err == nil {
|
|
t.Fatal("symlink target error = nil, want error")
|
|
}
|
|
}
|
|
|
|
func TestClearIsNotCommandAlias(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clear"}, &stdout, &stderr)
|
|
if code == 0 {
|
|
t.Fatal("exit code = 0, want non-zero")
|
|
}
|
|
if !strings.Contains(stderr.String(), `unknown command: "clear"`) {
|
|
t.Fatalf("stderr = %q, want unknown clear command", stderr.String())
|
|
}
|
|
}
|
|
|
|
func cleanAssertExists(t *testing.T, path string) {
|
|
t.Helper()
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected %q to exist: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func cleanAssertMissing(t *testing.T, path string) {
|
|
t.Helper()
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("expected %q to be missing, stat err=%v", path, err)
|
|
}
|
|
}
|