package main import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "gitea.lab.audasmedia.com.au/sam/artemis_control/internal/recipe" ) // TestRotateRunLogKeepsRecentFiles proves the bound: the live file moves aside, // the older files shift up, and the oldest file is dropped once the limit is // reached. No history is deleted until the keep count is actually exceeded. func TestRotateRunLogKeepsRecentFiles(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "runs.jsonl") // No file yet: nothing to rotate and no error. if err := rotateRunLog(path, 100, 1000, 2); err != nil { t.Fatalf("rotateRunLog on a missing file: %v", err) } // A file that still fits is left alone. if err := os.WriteFile(path, []byte("small\n"), 0o644); err != nil { t.Fatal(err) } if err := rotateRunLog(path, 10, 1000, 2); err != nil { t.Fatalf("rotateRunLog under the bound: %v", err) } if _, err := os.Stat(rotatedName(path, 1)); !os.IsNotExist(err) { t.Fatalf("a file under the bound must not rotate") } // Three rotations with keep=2: live is empty, .1 holds the second write, // .2 holds the first, and the first file is gone. for i := 1; i <= 3; i++ { if err := os.WriteFile(path, []byte(fmt.Sprintf("write-%d\n", i)), 0o644); err != nil { t.Fatal(err) } if err := rotateRunLog(path, 1000, 1, 2); err != nil { t.Fatalf("rotate %d: %v", i, err) } } want := map[string]string{ rotatedName(path, 1): "write-3\n", rotatedName(path, 2): "write-2\n", } for name, body := range want { got, err := os.ReadFile(name) if err != nil { t.Fatalf("read %s: %v", name, err) } if string(got) != body { t.Errorf("%s = %q, want %q", filepath.Base(name), got, body) } } // The third rotation moved the live file aside, so the next append creates // it again (appendRunLog opens with O_CREATE). if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("the live file should have moved to %s", filepath.Base(rotatedName(path, 1))) } // write-1 fell off the end, because keep=2 keeps only two rotated files. if _, err := os.Stat(rotatedName(path, 3)); !os.IsNotExist(err) { t.Errorf("keep=2 must not leave a third rotated file") } } // TestAppendRunLogRotatesAndKeepsEveryEntry drives the real logging path with a // tiny bound and checks that every entry survives in one of the files, that // each line is still complete JSON, and that the file count stays bounded. func TestAppendRunLogRotatesAndKeepsEveryEntry(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "runs.jsonl") t.Setenv("ARTEMIS_LOG_MAX_BYTES", "600") t.Setenv("ARTEMIS_LOG_KEEP", "3") const entries = 40 for i := 0; i < entries; i++ { res := &recipe.RunResult{ Task: "spotify_radio", Params: map[string]string{"query": fmt.Sprintf("query %d", i)}, OK: true, Steps: []recipe.StepResult{{Index: 1, Verb: "launch", OK: true}}, } if err := appendRunLog(path, res); err != nil { t.Fatalf("appendRunLog %d: %v", i, err) } } files := []string{path} for i := 1; i <= logKeep(); i++ { files = append(files, rotatedName(path, i)) } for i := logKeep() + 1; i <= logKeep()+1; i++ { if _, err := os.Stat(rotatedName(path, i)); !os.IsNotExist(err) { t.Errorf("rotated file %d exists past the keep bound", i) } } seen := map[string]bool{} total := 0 for _, name := range files { raw, err := os.ReadFile(name) if err != nil { if os.IsNotExist(err) { continue } t.Fatalf("read %s: %v", name, err) } size := int64(len(raw)) if size > logMaxBytes() { t.Errorf("%s is %d bytes, over the %d byte bound", filepath.Base(name), size, logMaxBytes()) } for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { if line == "" { continue } var entry struct { Time string `json:"time"` Task string `json:"task"` Params map[string]string } if err := json.Unmarshal([]byte(line), &entry); err != nil { t.Fatalf("%s holds a truncated line %q: %v", filepath.Base(name), line, err) } if key := entry.Params["query"]; key != "" { if seen[key] { t.Errorf("entry %q was written twice", key) } seen[key] = true total++ } } } // A rotation bound of four small files cannot hold all 40 entries, so the // oldest records are gone. What survives is a contiguous, complete tail. if total == 0 { t.Errorf("no entries survived rotation") } if total >= entries { t.Errorf("kept %d entries, want fewer than all %d: a tiny bound must drop the oldest", total, entries) } }