78c894234c
gates / gates (push) Successful in 46s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
106 lines
4.5 KiB
Go
106 lines
4.5 KiB
Go
package stacks
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestRestartStack_R1026_RecreatesAnUnchangedCompose pins the CONSEQUENCE of R-1026: a restart with an unchanged
|
|
// compose starts the container again. The compose binary is a stub on PATH that „recreates" (writes a marker) only when
|
|
// it is asked to — exactly as real compose does nothing on `up -d` with an unchanged compose; the start-time read
|
|
// answers from that marker. Today's (pre-v0.313.0) RestartStack ran `up -d`: the marker is never written, the start
|
|
// time never moves, and this test fails with "no container started again".
|
|
//
|
|
// COMPANION RED-PROOF (2026-10-15): drop "--force-recreate" from RestartStack's compose call → this test fails
|
|
// („restart must succeed: … no container started again"). Restored.
|
|
func TestRestartStack_R1026_RecreatesAnUnchangedCompose(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("the stub compose binary is a shell script")
|
|
}
|
|
var logs strings.Builder
|
|
m, _ := newInstalledManager(t, "services:\n web:\n image: nginx:1.27\n", "deployed: true\nenv: {}\n")
|
|
m.logger = log.New(&logs, "", 0)
|
|
|
|
bin := t.TempDir()
|
|
marker := filepath.Join(t.TempDir(), "recreated")
|
|
script := fmt.Sprintf("#!/bin/sh\nfor a in \"$@\"; do [ \"$a\" = \"--force-recreate\" ] && touch %q; done\nexit 0\n", marker)
|
|
if err := os.WriteFile(filepath.Join(bin, "docker-compose"), []byte(script), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
m.composeCmd = "docker-compose"
|
|
m.execFn = func(string, ...string) (string, error) { return "", nil }
|
|
|
|
started := func() string {
|
|
if _, err := os.Stat(marker); err == nil {
|
|
return "2026-10-15T09:00:05Z"
|
|
}
|
|
return "2026-10-14T20:41:00Z"
|
|
}
|
|
m.installedExecFn = func(_ context.Context, _ string, _ []string, name string, args ...string) (string, error) {
|
|
if name == "docker" && len(args) > 0 && args[0] == "compose" {
|
|
name, args = "docker-compose", args[1:]
|
|
}
|
|
switch {
|
|
case name == "docker-compose" && len(args) > 0 && args[0] == "ps":
|
|
return `{"ID":"aaa111bbb222","Name":"web-1","Service":"web"}`, nil
|
|
case name == "docker" && len(args) > 0 && args[0] == "inspect" && strings.Contains(strings.Join(args, " "), "State.StartedAt"):
|
|
return "aaa111bbb222ccc333" + inspectSep + "started" + inspectSep + started() + "\n", nil
|
|
case name == "docker" && len(args) > 0 && args[0] == "inspect":
|
|
return "aaa111bbb222" + inspectSep + "nginx:1.27" + inspectSep + "sha256:i\n", nil
|
|
case name == "docker" && len(args) > 1 && args[0] == "image":
|
|
return "sha256:i" + inspectSep + "nginx@sha256:d\n", nil
|
|
}
|
|
t.Fatalf("unexpected command: %s %v", name, args)
|
|
return "", nil
|
|
}
|
|
|
|
before := started()
|
|
if err := m.RestartStack("bookstack"); err != nil {
|
|
t.Fatalf("restart must succeed: %v\nlog:\n%s", err, logs.String())
|
|
}
|
|
if after := started(); after == before {
|
|
t.Fatalf("the container's start time did not move (%s) — the restart restarted nothing (R-1026)", after)
|
|
}
|
|
if !strings.Contains(logs.String(), "started again: web 2026-10-14T20:41:00Z → 2026-10-15T09:00:05Z") {
|
|
t.Errorf("the log must say what changed. Log:\n%s", logs.String())
|
|
}
|
|
}
|
|
|
|
// TestRestartStack_R1026_NothingStartedIsAnError: compose answers 0 but no start time moved — that is reported as a
|
|
// failure, never as „restarted successfully" (the presence-is-not-success rule).
|
|
func TestRestartStack_R1026_NothingStartedIsAnError(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("the stub compose binary is a shell script")
|
|
}
|
|
var logs strings.Builder
|
|
m, _ := newInstalledManager(t, "services:\n web:\n image: nginx:1.27\n", "deployed: true\nenv: {}\n")
|
|
m.logger = log.New(&logs, "", 0)
|
|
withFakeCompose(t, m) // exits 0 and recreates nothing
|
|
m.installedExecFn = func(_ context.Context, _ string, _ []string, name string, args ...string) (string, error) {
|
|
if name == "docker" && len(args) > 0 && args[0] == "compose" {
|
|
name, args = "docker-compose", args[1:]
|
|
}
|
|
if name == "docker-compose" {
|
|
return `{"ID":"aaa111","Name":"web-1","Service":"web"}`, nil
|
|
}
|
|
if strings.Contains(strings.Join(args, " "), "State.StartedAt") {
|
|
return "aaa111" + inspectSep + "started" + inspectSep + "2026-10-14T20:41:00Z\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
err := m.RestartStack("bookstack")
|
|
if err == nil || !strings.Contains(err.Error(), "no container started again") {
|
|
t.Fatalf("a restart that started nothing must fail, got %v", err)
|
|
}
|
|
if strings.Contains(logs.String(), "restarted successfully") {
|
|
t.Errorf("no success line may be logged. Log:\n%s", logs.String())
|
|
}
|
|
}
|