package stacks import ( "os" "path/filepath" "strings" "testing" ) // R-621 (the open half): the hold keeps the app's own log, and the logs a person can READ — the logs // page, /api/stacks//logs, the operator's remote diagnostics, all through GetLogs — now show it // when the held app has no live output. Before this, GetLogs returned "" for a held app (no // containers), so the kept file was reachable only by a shell on the box. func TestR621_GetLogsFallsBackToTheHoldLog(t *testing.T) { m, dir := newPinManager(t, pinTplOld, pinTplNew, "deployed: true\nenv: {}\n") live := "" m.logsComposeFn = func(string, ...string) (string, error) { return live, nil } // No hold log yet: an empty live log stays empty (nothing invented). if got, err := m.GetLogs("nextcloud", 200); err != nil || got != "" { t.Fatalf("no hold log: got %q, %v", got, err) } for ts, body := range map[string]string{ "20260921T211700Z": "web-1 | OLDER hold\n", "20260922T080000Z": "web-1 | Applying migration 0009... OK\nweb-1 | never bound :8000\n", "20260923T000000Z": " \n", // a newer, EMPTY capture must not hide the useful one } { p := filepath.Join(dir, "hold-logs", ts) if err := os.MkdirAll(p, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(p, "compose-logs.txt"), []byte(body), 0o644); err != nil { t.Fatal(err) } } got, err := m.GetLogs("nextcloud", 200) if err != nil { t.Fatal(err) } if !strings.Contains(got, "never bound :8000") || !strings.HasPrefix(got, "--- hold-logs/20260922T080000Z/compose-logs.txt ---\n") { t.Fatalf("R-621: a held app's logs must show the newest non-empty kept log under its header, got %q", got) } if strings.Contains(got, "OLDER hold") { t.Errorf("only the newest kept log is served, got %q", got) } // A running app's own output always wins over a kept log. live = "web-1 | serving\n" if got, _ := m.GetLogs("nextcloud", 200); got != live { t.Errorf("live output must win over the hold log, got %q", got) } }