fix(setup): harden verification and recovery
This commit is contained in:
@@ -94,6 +94,104 @@ func TestRunTimesOutWithPartialStartupGuidance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBuildFailureDoesNotAttemptContainerStartup(t *testing.T) {
|
||||
_, request := setupRunFixture(t, false)
|
||||
runner := &setupRunner{failureAt: "compose build"}
|
||||
|
||||
_, err := Run(context.Background(), runner, request, strings.NewReader(""), io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "setup image build") {
|
||||
t.Fatalf("Run() error = %v, want original build failure", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "tht status") {
|
||||
t.Fatalf("Run() error = %q, must not offer partial-start recovery before compose up", err)
|
||||
}
|
||||
assertSetupStages(t, runner, "docker engine", "docker compose", "architecture", "compose config", "compose build")
|
||||
}
|
||||
|
||||
func TestRunUpFailurePreservesCauseAndOffersRecovery(t *testing.T) {
|
||||
_, request := setupRunFixture(t, false)
|
||||
runner := &setupRunner{failureAt: "compose up"}
|
||||
|
||||
_, err := Run(context.Background(), runner, request, strings.NewReader(""), io.Discard)
|
||||
assertRecoveryFailure(t, err, "setup stack start", "core")
|
||||
assertSetupStages(t, runner, "docker engine", "docker compose", "architecture", "compose config", "compose build", "compose up")
|
||||
}
|
||||
|
||||
func TestRunAggregateDoctorFailurePreservesCauseAndOffersRecovery(t *testing.T) {
|
||||
_, request := setupRunFixture(t, false)
|
||||
runner := &setupRunner{failureAt: "doctor"}
|
||||
|
||||
_, err := Run(context.Background(), runner, request, strings.NewReader(""), io.Discard)
|
||||
assertRecoveryFailure(t, err, "setup doctor", "core")
|
||||
assertSetupStages(t, runner, "docker engine", "docker compose", "architecture", "compose config", "compose build", "compose up", "health", "doctor")
|
||||
}
|
||||
|
||||
func TestRunPiDoctorFailurePreservesCauseAndOffersRecovery(t *testing.T) {
|
||||
_, request := setupRunFixture(t, false)
|
||||
runner := &setupRunner{failureAt: "pi doctor"}
|
||||
|
||||
_, err := Run(context.Background(), runner, request, strings.NewReader(""), io.Discard)
|
||||
assertRecoveryFailure(t, err, "setup Pi doctor", "core")
|
||||
assertSetupStages(t, runner, "docker engine", "docker compose", "architecture", "compose config", "compose build", "compose up", "health", "doctor", "pi doctor")
|
||||
}
|
||||
|
||||
func TestRequireVolumesRequiresEveryInstallationVolume(t *testing.T) {
|
||||
all := []string{"settings", "pi-state", "workspace-registry", "workspace-secrets", "sessions", "qdrant-data", "embedding-models"}
|
||||
for _, missing := range all {
|
||||
t.Run("missing "+missing, func(t *testing.T) {
|
||||
volumes := make([]string, 0, len(all)-1)
|
||||
for _, name := range all {
|
||||
if name != missing {
|
||||
volumes = append(volumes, name)
|
||||
}
|
||||
}
|
||||
if err := requireVolumes(renderedConfigForVolumes(volumes...)); err == nil || !strings.Contains(err.Error(), missing) {
|
||||
t.Fatalf("requireVolumes() error = %v, want missing %q", err, missing)
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := requireVolumes(renderedConfigForVolumes("unrelated")); err == nil {
|
||||
t.Fatal("requireVolumes() error = nil, want required-volume failure for unrelated-only configuration")
|
||||
}
|
||||
if err := requireVolumes(renderedConfigForVolumes(all...)); err != nil {
|
||||
t.Fatalf("requireVolumes() error = %v, want complete installation volume set", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunIgnoresIrrelevantCRLFFilesDuringLineEndingCheck(t *testing.T) {
|
||||
projectRoot, request := setupRunFixture(t, true)
|
||||
irrelevant := filepath.Join(projectRoot, "node_modules", "unrelated", "generated.yml")
|
||||
if err := os.MkdirAll(filepath.Dir(irrelevant), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(irrelevant, []byte("generated: true\r\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := Run(context.Background(), &setupRunner{}, request, strings.NewReader(""), io.Discard); err != nil {
|
||||
t.Fatalf("Run() error = %v, want irrelevant CRLF file ignored", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRecoveryFailure(t *testing.T, err error, cause, service string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("Run() error = nil, want failure")
|
||||
}
|
||||
for _, text := range []string{cause, "tht logs " + service, "tht status", "left running"} {
|
||||
if !strings.Contains(err.Error(), text) {
|
||||
t.Errorf("Run() error = %q, want %q", err, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertSetupStages(t *testing.T, runner *setupRunner, want ...string) {
|
||||
t.Helper()
|
||||
if got := collapseStages(runner.stages); strings.Join(got, " | ") != strings.Join(want, " | ") {
|
||||
t.Fatalf("runner stages = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
const unhealthyServicesJSON = `[
|
||||
{"Service":"core","State":"running","Health":"healthy"},
|
||||
{"Service":"frontend","State":"running","Health":"starting"},
|
||||
@@ -187,7 +285,15 @@ func setupStage(args []string) (string, compose.Result) {
|
||||
}
|
||||
}
|
||||
|
||||
const renderedSetupConfig = `{"volumes":{"settings":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}`
|
||||
const renderedSetupConfig = `{"volumes":{"settings":{},"pi-state":{},"workspace-registry":{},"workspace-secrets":{},"sessions":{},"qdrant-data":{},"embedding-models":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}`
|
||||
|
||||
func renderedConfigForVolumes(volumes ...string) string {
|
||||
entries := make([]string, 0, len(volumes))
|
||||
for _, volume := range volumes {
|
||||
entries = append(entries, `"`+volume+`":{}`)
|
||||
}
|
||||
return `{"volumes":{` + strings.Join(entries, ",") + `}}`
|
||||
}
|
||||
|
||||
func setupRunFixture(t *testing.T, configureOnly bool) (string, Request) {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user