Files
felhom-controller/controller/internal/stacks/r864_stderr_tail_test.go
T

62 lines
2.6 KiB
Go

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")
}
}