package stacks import ( "bytes" "log" "os" "path/filepath" "strings" "testing" ) // R-864 (v0.294.0): a failed compose command logs and returns the TAIL of stderr. THE NIGHT'S SHAPE (2026-10-04, // BookStack on the fresh Tester 1 box, audits/night-2026-10-04/tester1/t8-*): the stderr opened with the pull // progress below (the first line is verbatim from that night) and the reason came last; v0.293.0 kept the first // 500 bytes and logged only "Image … Pulling" — the reason was lost. The night's last line itself was lost by // exactly this defect; the one here is Docker's message for an image deleted under compose (R-863). // COMPANION RED-PROOF: use truncateStr instead of tailStr for stderr in composeExecCustomEnv → "the reason was cut". func TestR864_FailedComposeLogsTheReasonNotThePullProgress(t *testing.T) { var lines []string lines = append(lines, "Image lscr.io/linuxserver/bookstack:26.09.1@sha256:99cd1f5707c1911afad213adec5c9739763b76f843d1477142231834ecdcb6f7 Pulling") lines = append(lines, "Image mariadb:11.4@sha256:dfff46ef3f9d Pulling") for i := 0; i < 16; i++ { lines = append(lines, " a1b2c3d4e5f6 Pull complete") } lines = append(lines, "Image mariadb:11.4@sha256:dfff46ef3f9d Pulled") reason := "Error response from daemon: No such image: mariadb@sha256:dfff46ef3f9d — árvíztűrő" lines = append(lines, reason) stderr := strings.Join(lines, "\n") if len(stderr) < 700 { t.Fatalf("the fixture must exceed the 500-byte cut (%d)", len(stderr)) } m := gateManager(t, "display_name: G\n") dir := t.TempDir() // a stub under os.TempDir(): R-650's sanctioned seam, never this host's Docker errf := filepath.Join(dir, "stderr.txt") must(t, os.WriteFile(errf, []byte(stderr), 0o644)) must(t, os.WriteFile(filepath.Join(dir, "docker"), []byte("#!/bin/sh\nwhile IFS= read -r l || [ -n \"$l\" ]; do printf '%s\\n' \"$l\" >&2; done < "+errf+"\nexit 1\n"), 0o755)) t.Setenv("PATH", dir) var buf bytes.Buffer m.logger = log.New(&buf, "", 0) _, err := m.composeExecCustomEnv(dir, []string{"PATH=" + dir}, "up", "-d") if err == nil { t.Fatal("no error from a failed compose") } if !strings.Contains(err.Error(), reason) { t.Fatalf("the reason was cut from the returned error: %q", err.Error()) } if !strings.Contains(buf.String(), reason) { t.Fatalf("the reason was cut from the log:\n%s", buf.String()) } } func TestR864_TailStrKeepsTheEndOnARuneBoundary(t *testing.T) { s := strings.Repeat("x", 10) + "őőő" got := tailStr(s, 5) // the cut falls inside "ő" (2 bytes each) if got != "...őő" { t.Fatalf("tailStr = %q", got) } if tailStr("short", 500) != "short" { t.Fatal("a short string must pass unchanged") } }