feat(cli): add version diagnostics and build-aware start

This commit is contained in:
2026-08-15 23:35:55 +02:00
parent 9a8755e4c3
commit 7ac99b4f7e
11 changed files with 989 additions and 306 deletions
+22 -14
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/doctor"
)
func TestRunBuildsStartsAndVerifiesInOrder(t *testing.T) {
@@ -35,7 +36,7 @@ func TestRunBuildsStartsAndVerifiesInOrder(t *testing.T) {
want := []string{
"docker engine", "docker compose", "architecture", "compose config", "compose build",
"compose up", "health", "health", "doctor", "pi doctor",
"compose up", "health", "health", "doctor docker", "doctor compose", "compose config", "doctor config", "health", "workflow doctor", "pi doctor",
}
if got := collapseStages(runner.stages); strings.Join(got, " | ") != strings.Join(want, " | ") {
t.Fatalf("runner stages = %v, want %v", got, want)
@@ -119,11 +120,11 @@ func TestRunUpFailurePreservesCauseAndOffersRecovery(t *testing.T) {
func TestRunAggregateDoctorFailurePreservesCauseAndOffersRecovery(t *testing.T) {
_, request := setupRunFixture(t, false)
runner := &setupRunner{failureAt: "doctor"}
runner := &setupRunner{failureAt: "doctor config"}
_, 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")
assertRecoveryFailure(t, err, "setup doctor reported failed checks", "core")
assertSetupStages(t, runner, "docker engine", "docker compose", "architecture", "compose config", "compose build", "compose up", "health", "doctor docker", "doctor compose", "compose config", "doctor config", "health", "workflow doctor", "pi doctor")
}
func TestRunPiDoctorFailurePreservesCauseAndOffersRecovery(t *testing.T) {
@@ -131,8 +132,8 @@ func TestRunPiDoctorFailurePreservesCauseAndOffersRecovery(t *testing.T) {
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")
assertRecoveryFailure(t, err, "setup doctor reported failed checks", "core")
assertSetupStages(t, runner, "docker engine", "docker compose", "architecture", "compose config", "compose build", "compose up", "health", "doctor docker", "doctor compose", "compose config", "doctor config", "health", "workflow doctor", "pi doctor")
}
func TestRequireVolumesRequiresEveryInstallationVolume(t *testing.T) {
@@ -145,16 +146,16 @@ func TestRequireVolumesRequiresEveryInstallationVolume(t *testing.T) {
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 := doctor.ValidateVolumes(renderedConfigForVolumes(volumes...)); err == nil || !strings.Contains(err.Error(), missing) {
t.Fatalf("ValidateVolumes() 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 := doctor.ValidateVolumes(renderedConfigForVolumes("unrelated")); err == nil {
t.Fatal("ValidateVolumes() 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)
if err := doctor.ValidateVolumes(renderedConfigForVolumes(all...)); err != nil {
t.Fatalf("ValidateVolumes() error = %v, want complete installation volume set", err)
}
}
@@ -216,7 +217,10 @@ type setupRunner struct {
func (r *setupRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) {
stage, result := setupStage(args)
if stage == "doctor" && containsStage(r.stages, "doctor") {
if stage == "docker compose" && containsStage(r.stages, "docker compose") {
stage = "doctor compose"
}
if stage == "doctor config" && containsStage(r.stages, "workflow doctor") {
stage = "pi doctor"
}
r.stages = append(r.stages, stage)
@@ -258,6 +262,8 @@ func setupStage(args []string) (string, compose.Result) {
switch {
case joined == "version --format {{.Server.Version}}":
return "docker engine", compose.Result{Stdout: "26.0.0\n"}
case joined == "version --format {{.Client.Version}}":
return "doctor docker", compose.Result{Stdout: "26.0.0\n"}
case joined == "compose version --short":
return "docker compose", compose.Result{Stdout: "v2.30.0\n"}
case joined == "version --format {{.Server.Arch}}":
@@ -271,7 +277,9 @@ func setupStage(args []string) (string, compose.Result) {
case strings.HasSuffix(joined, " ps --all --format json"):
return "health", compose.Result{}
case strings.HasSuffix(joined, " config --format json"):
return "doctor", compose.Result{Stdout: renderedSetupConfig}
return "doctor config", compose.Result{Stdout: renderedSetupConfig}
case strings.Contains(joined, "exec -T core tht doctor --json"):
return "workflow doctor", compose.Result{Stdout: `{"ok":true,"components":{}}`}
case strings.Contains(joined, " ps -q core"):
return "pi doctor", compose.Result{Stdout: "core-id\n"}
case strings.HasPrefix(joined, "inspect --format"):