package main import ( "bytes" "context" "fmt" "io" "os" "path/filepath" "strconv" "strings" "testing" "github.com/aritmolab/thothii/tools/tht/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/pi" "github.com/aritmolab/thothii/tools/tht/internal/testsupport" ) func TestInstallationRunnerMapsProfileToSessionInventoryScope(t *testing.T) { for _, test := range []struct { profile string want string }{ {profile: "local", want: "mine"}, {profile: "server", want: "all"}, } { runner := installationRunner{installation: config.Installation{Profile: test.profile}} if got := runner.SessionInventoryScope(); got != test.want { t.Fatalf("profile %q maps to session scope %q, want %q", test.profile, got, test.want) } } } func TestRootCommandIdentity(t *testing.T) { fixture := newCLIFixture(t, "") fixture.setEnvironment(t) retiredCommand := "thoth" + "ctl" for _, test := range []struct { name string args []string wantCode int wantText string }{ { name: "help banner", args: []string{"--help"}, wantCode: 0, wantText: "Usage: tht ", }, { name: "version path", args: []string{"--installation", fixture.installationPath, "version"}, wantCode: 2, wantText: `tht: unknown command "version"`, }, { name: "retired command is not an alias", args: []string{"--installation", fixture.installationPath, retiredCommand}, wantCode: 2, wantText: `tht: unknown command "` + retiredCommand + `"`, }, } { t.Run(test.name, func(t *testing.T) { var stdout, stderr bytes.Buffer if got := run(context.Background(), test.args, &stdout, &stderr); got != test.wantCode { t.Fatalf("run(%v) exit code = %d, want %d", test.args, got, test.wantCode) } if got := stdout.String() + stderr.String(); !strings.Contains(got, test.wantText) { t.Fatalf("run(%v) output = %q, want %q", test.args, got, test.wantText) } }) } } func TestBootstrapCommandsDoNotRequireAnInstallationDescriptor(t *testing.T) { projectRoot := newProjectWithoutInstallation(t) previousDirectory, err := os.Getwd() if err != nil { t.Fatal(err) } if err := os.Chdir(filepath.Join(projectRoot, "frontend")); err != nil { t.Fatal(err) } t.Cleanup(func() { if restoreErr := os.Chdir(previousDirectory); restoreErr != nil { t.Errorf("restore working directory: %v", restoreErr) } }) t.Setenv("THOTHII_INSTALLATION", "") for _, args := range [][]string{nil, {"help"}, {"version"}, {"setup"}} { var stdout, stderr bytes.Buffer _ = run(context.Background(), args, &stdout, &stderr) if output := stdout.String() + stderr.String(); strings.Contains(output, "installation descriptor") { t.Fatalf("run(%v) required a descriptor: %q", args, output) } } } // Catches interactive configuration prompts that use retired model-only data instead of the // provider, model, and reasoning choices supplied by the dedicated Pi Management API. func TestResolvePiConfigureUsesNumberedClosedChoicesOnlyForTTY(t *testing.T) { runner := &wizardRunner{} var prompt bytes.Buffer defaults, err := resolvePiConfigure( context.Background(), runner, nil, strings.NewReader("2\n1\n3\n"), &prompt, true, ) if err != nil { t.Fatal(err) } want := pi.Defaults{Provider: "zai", Model: "glm-5.2", Thinking: "high"} if defaults != want { t.Fatalf("defaults = %#v", defaults) } for _, expected := range []string{"1) deepseek", "2) zai", "1) glm-5.2", "3) high"} { if !strings.Contains(prompt.String(), expected) { t.Errorf("prompt %q missing %q", prompt.String(), expected) } } } func TestResolvePiConfigureRequiresExplicitFlagsWithoutTTY(t *testing.T) { runner := &wizardRunner{} _, err := resolvePiConfigure(context.Background(), runner, nil, strings.NewReader("1\n1\n1\n"), io.Discard, false) if err == nil || !strings.Contains(err.Error(), "non-interactive") { t.Fatalf("resolvePiConfigure() error = %v, want explicit non-interactive guidance", err) } if len(runner.calls) != 0 { t.Fatalf("Docker calls = %v, want none", runner.calls) } } func TestPiLifecycleContractErrorsExitTwo(t *testing.T) { for _, lifecycleErr := range []error{pi.ErrActiveSessions, pi.ErrInterruptedUpdate} { var stderr bytes.Buffer wrapped := fmt.Errorf("automatic rollback succeeded: %w", lifecycleErr) if code := piFailure(&stderr, wrapped, nil); code != 2 { t.Errorf("piFailure(%v) = %d, want 2", lifecycleErr, code) } } } func TestLogsRejectsFollowAndOtherArguments(t *testing.T) { if _, err := logsArgs([]string{"--follow"}); err == nil { t.Fatal("logsArgs(--follow) error = nil, want bounded-log rejection") } if got, err := logsArgs(nil); err != nil || strings.Join(got, " ") != "logs --tail 200" { t.Fatalf("logsArgs(nil) = %v, %v", got, err) } } func TestParseArgsMakesInstallationOptionalAndAcceptsOverrideAfterCommand(t *testing.T) { if installation, command, commandArgs, err := parseArgs([]string{"pi", "status"}); err != nil || installation != "" || command != "pi" || strings.Join(commandArgs, " ") != "status" { t.Fatalf("implicit parse = %q, %q, %v, %v", installation, command, commandArgs, err) } want := "/tmp/thothii-installation.yaml" installation, command, commandArgs, err := parseArgs([]string{"pi", "update", "--installation", want}) if err != nil { t.Fatal(err) } if installation != want || command != "pi" || strings.Join(commandArgs, " ") != "update" { t.Fatalf("trailing override parse = %q, %q, %v", installation, command, commandArgs) } } func TestRunPiStatusUsesInstallationEnvironmentWithoutFlag(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) t.Setenv("THOTHII_INSTALLATION", fixture.installationPath) var stdout, stderr bytes.Buffer if code := run(context.Background(), []string{"pi", "status"}, &stdout, &stderr); code != 0 { t.Fatalf("run() exit code = %d, stderr = %s", code, stderr.String()) } if stdout.String() != "Pi version: 0.80.3\n" { t.Fatalf("stdout = %q, want image-bundled Pi version", stdout.String()) } } func TestUsageDocumentsClosedConfigureUpdateSourcesRestartAndMaintenanceRecovery(t *testing.T) { if strings.Contains(usage, "--follow") { t.Fatal("usage still advertises unbounded log following") } for _, required := range []string{ "--provider P --model M --thinking low|medium|high", "--source build", "--source pull --image IMAGE@sha256:DIGEST", "pi restart --yes [--drain]", "Recreate only core with the currently selected Pi image", "pi maintenance status", "pi maintenance recover --yes", "sessions migrate --yes", "remove --yes ID...", } { if !strings.Contains(usage, required) { t.Errorf("usage missing %q", required) } } } func TestParsePiRestartArgs(t *testing.T) { restartPath := "/var/lib/tht/restart-state.json" updatePath := "/var/lib/tht/update-state.json" for _, test := range []struct { name string args []string want pi.RestartRequest wantErr string }{ { name: "confirmed", args: []string{"--yes"}, want: pi.RestartRequest{StatePath: restartPath, UpdateStatePath: updatePath, Confirm: true}, }, { name: "drain", args: []string{"--yes", "--drain"}, want: pi.RestartRequest{StatePath: restartPath, UpdateStatePath: updatePath, Confirm: true, Drain: true}, }, {name: "duplicate yes", args: []string{"--yes", "--yes"}, wantErr: "--yes may be supplied once"}, {name: "duplicate drain", args: []string{"--drain", "--drain"}, wantErr: "--drain may be supplied once"}, {name: "unknown", args: []string{"--force"}, wantErr: "unknown pi restart option"}, } { t.Run(test.name, func(t *testing.T) { got, err := parsePiRestartArgs(test.args, restartPath, updatePath) if test.wantErr != "" { if err == nil || err.Error() != test.wantErr { t.Fatalf("parsePiRestartArgs(%v) error = %v, want %q", test.args, err, test.wantErr) } return } if err != nil { t.Fatalf("parsePiRestartArgs(%v) error = %v", test.args, err) } if got != test.want { t.Fatalf("parsePiRestartArgs(%v) = %#v, want %#v", test.args, got, test.want) } }) } } func TestRunSessionsMigrateRequiresExplicitConfirmationBeforeDocker(t *testing.T) { fixture := newCLIFixture(t, "") fixture.setProfile(t, "server") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer code := run(context.Background(), []string{ "--installation", fixture.installationPath, "sessions", "migrate", }, &stdout, &stderr) if code != 2 || !strings.Contains(stderr.String(), "sessions migrate requires --yes") { t.Fatalf("exit = %d, stderr = %q", code, stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunSessionsMigrateRedactsCompleteDetailBeforeDisplayBound(t *testing.T) { longSecret := "long-secret-" + strings.Repeat("s", 700) for _, spec := range []struct { name string secret string failure string leakedProbe string }{ { name: "secret longer than display limit", secret: longSecret, failure: longSecret + " rejected by TLS", leakedProbe: longSecret[:64], }, { name: "secret crossing display boundary", secret: "boundary-secret-value", failure: strings.Repeat("p", 500) + "boundary-secret-value rejected", leakedProbe: "boundary-sec", }, } { t.Run(spec.name, func(t *testing.T) { fixture := newCLIFixture(t, "MIGRATION_PASSWORD_FILE=%s\n") fixture.setProfile(t, "server") secretPath := filepath.Join(fixture.root, "migration-password") if err := os.WriteFile(secretPath, []byte(spec.secret), 0o600); err != nil { t.Fatal(err) } fixture.setEnvironment(t, secretPath) t.Setenv("THT_FAKE_CONFIG", `{"services":{"core":{"image":"thothii-core:local"},"session-migrate":{"image":"thothii-core:local"}}}`) t.Setenv("THT_FAKE_MIGRATION_FAILURE", spec.failure) t.Setenv("THT_FAKE_MIGRATION_EXIT", "23") var stdout, stderr bytes.Buffer code := run(context.Background(), []string{ "--installation", fixture.installationPath, "sessions", "migrate", "--yes", }, &stdout, &stderr) if code != 1 { t.Fatalf("exit = %d, stderr = %q", code, stderr.String()) } for _, required := range []string{"stage=session-migration", "class=nonzero-exit", "[REDACTED]"} { if !strings.Contains(stderr.String(), required) { t.Errorf("stderr = %q, missing %q", stderr.String(), required) } } if strings.Contains(stderr.String(), spec.secret) || strings.Contains(stderr.String(), spec.leakedProbe) { t.Fatalf("stderr exposed secret or prefix: %q", stderr.String()) } if stderr.Len() > 640 { t.Fatalf("stderr exceeded bounded display: %d bytes", stderr.Len()) } }) } } func TestRunRemoveDisplaysExactInstallationTargetsBeforeConfirmation(t *testing.T) { fixture := newCLIFixture(t, "") fixture.setProfile(t, "server") fixture.setEnvironment(t) t.Setenv("THT_FAKE_STOPPED_PS", `[{"ID":"core-id","Name":"exact-core","Service":"core","State":"exited"},{"ID":"front-id","Name":"exact-frontend","Service":"frontend","State":"exited"}]`) var stdout, stderr bytes.Buffer code := run(context.Background(), []string{ "--installation", fixture.installationPath, "remove", }, &stdout, &stderr) if code != 2 || !strings.Contains(stderr.String(), "re-run with remove --yes core-id front-id") { t.Fatalf("exit = %d, stderr = %q", code, stderr.String()) } for _, value := range []string{"exact-core", "core-id", "exact-frontend", "front-id", "exited"} { if !strings.Contains(stdout.String(), value) { t.Errorf("target display %q missing %q", stdout.String(), value) } } calls := fixture.invocations(t) if len(calls) != 1 { t.Fatalf("Docker calls = %#v", calls) } assertInvocationContains(t, calls, "ps", "--all", "--format", "json", "core", "frontend") } type wizardRunner struct{ calls []string } func (r *wizardRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) { r.calls = append(r.calls, strings.Join(args, " ")) return compose.Result{Stdout: `{"providers":["deepseek","zai"],"models":[{"provider":"deepseek","id":"deepseek-v4"},{"provider":"zai","id":"glm-5.2"}],"reasoning":["low","medium","high"]}`}, nil } func TestRunLogsRedactsAnUnlabelledDeclaredSecret(t *testing.T) { fixture := newCLIFixture(t, "UNLABELLED_SECRET_FILE=%s\n") secretPath := filepath.Join(fixture.root, "operator-secret") if err := os.WriteFile(secretPath, []byte("unlabelled-secret\r\n"), 0o600); err != nil { t.Fatal(err) } fixture.setEnvironment(t, secretPath) t.Setenv("THT_FAKE_LOG", "fake Docker log: unlabelled-secret") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if strings.Contains(stdout.String(), "unlabelled-secret") { t.Fatalf("logs exposed an unlabelled secret: %q", stdout.String()) } if stdout.String() != "fake Docker log: [REDACTED]\n" { t.Errorf("logs = %q, want redacted output", stdout.String()) } } func TestRunPiLogsRedactsNestedAuthAndBundleScalars(t *testing.T) { fixture := newCLIFixture(t, "") authFile := filepath.Join(fixture.root, "pi-auth.json") bundleFile := filepath.Join(fixture.root, "thothii.secrets") if err := os.WriteFile(authFile, []byte(`{ "providers": { "dummy-provider": { "auth": { "key": "dummy-canary-pi-log-json", "nested": {"access": "dummy-canary-pi-log-nested"} } } } }`), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(bundleFile, []byte( "MODEL_API_KEY=dummy-canary-pi-log-model\n"+ "DWH_PASSWORD=dummy-canary-pi-log-dwh\n", ), 0o600); err != nil { t.Fatal(err) } fixture.setEnvContents(t, "PI_AUTH_FILE="+authFile+"\nTHT_SECRETS_FILE="+bundleFile+"\n") t.Setenv( "THT_FAKE_LOG", "dummy-canary-pi-log-json dummy-canary-pi-log-nested "+ "dummy-canary-pi-log-model dummy-canary-pi-log-dwh", ) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "logs", }, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %q", exitCode, stderr.String()) } for _, canary := range []string{ "dummy-canary-pi-log-json", "dummy-canary-pi-log-nested", "dummy-canary-pi-log-model", "dummy-canary-pi-log-dwh", } { if strings.Contains(stdout.String()+stderr.String(), canary) { t.Fatalf("pi logs exposed scalar %q: stdout=%q stderr=%q", canary, stdout.String(), stderr.String()) } } if strings.Count(stdout.String(), "[REDACTED]") != 4 { t.Fatalf("pi logs = %q, want four scalar redactions", stdout.String()) } assertInvocationContains(t, fixture.invocations(t), "logs", "--tail", "200", "core") } func TestRunResolvesComposeDotenvCommentsQuotesAndInterpolationForSecretFiles(t *testing.T) { fixture := newCLIFixture(t, "") secretDirectory := filepath.Join(fixture.root, "secret directory") if err := os.Mkdir(secretDirectory, 0o700); err != nil { t.Fatal(err) } inlineSecret := filepath.Join(secretDirectory, "inline") doubleQuotedSecret := filepath.Join(secretDirectory, "double quoted") singleQuotedSecret := filepath.Join(secretDirectory, "single quoted") interpolatedSecret := filepath.Join(secretDirectory, "interpolated") for path, value := range map[string]string{ inlineSecret: "inline-secret", doubleQuotedSecret: "double-quoted-secret", singleQuotedSecret: "single-quoted-secret", interpolatedSecret: "interpolated-secret", } { if err := os.WriteFile(path, []byte(value), 0o600); err != nil { t.Fatal(err) } } fixture.setEnvContents(t, "SECRET_ROOT="+secretDirectory+"\n"+ "INLINE_TOKEN_FILE="+inlineSecret+" # Compose comment\n"+ "DOUBLE_TOKEN_FILE=\""+doubleQuotedSecret+"\" # Compose comment\n"+ "SINGLE_TOKEN_FILE='"+singleQuotedSecret+"' # Compose comment\n"+ "INTERPOLATED_TOKEN_SOURCE=\"${SECRET_ROOT}/interpolated\"\n") t.Setenv("THT_FAKE_LOG", "inline-secret double-quoted-secret single-quoted-secret interpolated-secret") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } for _, secret := range []string{"inline-secret", "double-quoted-secret", "single-quoted-secret", "interpolated-secret"} { if strings.Contains(stdout.String(), secret) { t.Errorf("logs exposed %q: %q", secret, stdout.String()) } } } func TestRunRedactsSecretSourceInBothStreams(t *testing.T) { fixture := newCLIFixture(t, "") secretPath := filepath.Join(fixture.root, "source-secret") if err := os.WriteFile(secretPath, []byte("source-secret"), 0o600); err != nil { t.Fatal(err) } fixture.setEnvContents(t, "UNLABELLED_SECRET_SOURCE="+secretPath+"\n") t.Setenv("THT_FAKE_LOG", "stdout source-secret") t.Setenv("THT_FAKE_FAILURE", "stderr source-secret") t.Setenv("THT_FAKE_EXIT", "17") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 17 { t.Errorf("run() exit code = %d, want 17", exitCode) } if strings.Contains(stdout.String()+stderr.String(), "source-secret") { t.Errorf("output exposed source secret: stdout=%q stderr=%q", stdout.String(), stderr.String()) } } func TestRunRedactsSecretWhenDoctorFails(t *testing.T) { fixture := newCLIFixture(t, "") secretPath := filepath.Join(fixture.root, "doctor-secret") if err := os.WriteFile(secretPath, []byte("doctor-secret"), 0o600); err != nil { t.Fatal(err) } fixture.setEnvContents(t, "DOCTOR_SECRET_FILE="+secretPath+"\n") t.Setenv("THT_FAKE_FAIL_ON", "version") t.Setenv("THT_FAKE_FAILURE", "doctor saw doctor-secret") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) if exitCode != 41 { t.Errorf("run() exit code = %d, want 41", exitCode) } if strings.Contains(stdout.String()+stderr.String(), "doctor-secret") { t.Errorf("doctor failure exposed secret: stdout=%q stderr=%q", stdout.String(), stderr.String()) } } func TestRunFailsClosedForUnresolvedSecretSourceInterpolation(t *testing.T) { fixture := newCLIFixture(t, "MISSING_TOKEN_SOURCE=${MISSING_SECRET_ROOT}/token\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 2 { t.Errorf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "installation secret declarations could not be read") { t.Errorf("stderr = %q, want fail-closed declaration error", stderr.String()) } if _, err := os.Stat(fixture.argsFile); !os.IsNotExist(err) { t.Errorf("Docker was invoked after unresolved interpolation: stat error = %v", err) } } func TestRunFailsClosedForTraversalAndParentSymlinkSecretSources(t *testing.T) { for name, source := range map[string]func(*testing.T, cliFixture) string{ "traversal": func(t *testing.T, fixture cliFixture) string { secret := filepath.Join(fixture.root, "secret") if err := os.WriteFile(secret, []byte("traversal-secret"), 0o600); err != nil { t.Fatal(err) } return filepath.Join(fixture.root, "subdirectory") + string(filepath.Separator) + ".." + string(filepath.Separator) + "secret" }, "parent symlink": func(t *testing.T, fixture cliFixture) string { realDirectory := filepath.Join(fixture.root, "real") if err := os.Mkdir(realDirectory, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(realDirectory, "secret"), []byte("symlink-secret"), 0o600); err != nil { t.Fatal(err) } linkDirectory := filepath.Join(fixture.root, "linked") testsupport.SymlinkOrSkip(t, realDirectory, linkDirectory) return filepath.Join(linkDirectory, "secret") }, "final symlink": func(t *testing.T, fixture cliFixture) string { realSecret := filepath.Join(fixture.root, "real-secret") if err := os.WriteFile(realSecret, []byte("final-symlink-secret"), 0o600); err != nil { t.Fatal(err) } linkSecret := filepath.Join(fixture.root, "linked-secret") testsupport.SymlinkOrSkip(t, realSecret, linkSecret) return linkSecret }, } { t.Run(name, func(t *testing.T) { fixture := newCLIFixture(t, "") unsafeSource := source(t, fixture) fixture.setEnvContents(t, "UNSAFE_SECRET_SOURCE="+unsafeSource+"\n") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 2 { t.Errorf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "could not be read") { t.Errorf("stderr = %q, want sanitized unsafe-file error", stderr.String()) } if strings.Contains(stderr.String(), unsafeSource) { t.Errorf("stderr revealed unsafe source path: %q", stderr.String()) } assertDockerNotInvoked(t, fixture) }) } } func TestRunFailsClosedForOversizedEnvAndSecretFiles(t *testing.T) { t.Run("environment", func(t *testing.T) { fixture := newCLIFixture(t, "") fixture.setEnvContents(t, strings.Repeat("A", 1<<20+1)) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 2 || !strings.Contains(stderr.String(), "installation secret declarations could not be read") { t.Errorf("exit=%d stderr=%q, want sanitized oversized-env failure", exitCode, stderr.String()) } assertDockerNotInvoked(t, fixture) }) t.Run("secret", func(t *testing.T) { fixture := newCLIFixture(t, "") secretPath := filepath.Join(fixture.root, "large-secret") if err := os.WriteFile(secretPath, make([]byte, 64*1024+1), 0o600); err != nil { t.Fatal(err) } fixture.setEnvContents(t, "LARGE_SECRET_FILE="+secretPath+"\n") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 2 || !strings.Contains(stderr.String(), "declared secret file could not be read") { t.Errorf("exit=%d stderr=%q, want sanitized oversized-secret failure", exitCode, stderr.String()) } assertDockerNotInvoked(t, fixture) }) } func TestRunFailsClosedForTooManyOrTooLargeSecretSources(t *testing.T) { t.Run("too many sources", func(t *testing.T) { fixture := newCLIFixture(t, "") var declarations strings.Builder for index := range 33 { secretPath := filepath.Join(fixture.root, "secret-count-"+strconv.Itoa(index)) if err := os.WriteFile(secretPath, []byte("secret"), 0o600); err != nil { t.Fatal(err) } fmt.Fprintf(&declarations, "SECRET_%d_FILE=%s\n", index, secretPath) } fixture.setEnvContents(t, declarations.String()) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 2 || !strings.Contains(stderr.String(), "installation secret declarations could not be read") { t.Errorf("exit=%d stderr=%q, want sanitized source-count failure", exitCode, stderr.String()) } assertDockerNotInvoked(t, fixture) }) t.Run("total source bytes", func(t *testing.T) { fixture := newCLIFixture(t, "") var declarations strings.Builder for index := range 5 { secretPath := filepath.Join(fixture.root, "secret-total-"+strconv.Itoa(index)) if err := os.WriteFile(secretPath, bytes.Repeat([]byte("x"), 60*1024), 0o600); err != nil { t.Fatal(err) } fmt.Fprintf(&declarations, "SECRET_%d_SOURCE=%s\n", index, secretPath) } fixture.setEnvContents(t, declarations.String()) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr) if exitCode != 2 || !strings.Contains(stderr.String(), "declared secret file could not be read") { t.Errorf("exit=%d stderr=%q, want sanitized total-size failure", exitCode, stderr.String()) } assertDockerNotInvoked(t, fixture) }) } func TestRunStatusUsesStableComposeArguments(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) for range 2 { var stdout, stderr bytes.Buffer if exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "status"}, &stdout, &stderr); exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } } invocations := fixture.invocations(t) if len(invocations) != 2 { t.Fatalf("docker invocations = %d, want 2", len(invocations)) } if strings.Join(invocations[0], "\x00") != strings.Join(invocations[1], "\x00") { t.Errorf("Compose arguments changed between identical status calls: %#v then %#v", invocations[0], invocations[1]) } wantSuffix := []string{ "--project-directory", fixture.projectDirectory, "--env-file", fixture.envFile, "-f", filepath.Join(fixture.projectDirectory, "compose.yaml"), "-f", filepath.Join(fixture.projectDirectory, "deploy", "compose.local.yaml"), "ps", "--format", "json", } got := invocations[0] if len(got) != len(wantSuffix)+3 || got[0] != "compose" || got[1] != "--project-name" || !strings.HasPrefix(got[2], "thothii-") { t.Fatalf("unexpected Compose prefix: %#v", got) } for index, want := range wantSuffix { if got[index+3] != want { t.Errorf("argument %d = %q, want %q", index+3, got[index+3], want) } } } func TestRunStartAutomaticallyUsesTheDurableCurrentImageOverride(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) installation, err := config.Load(fixture.installationPath) if err != nil { t.Fatal(err) } currentImage := installation.CurrentImageOverridePath() if err := os.MkdirAll(filepath.Dir(currentImage), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(currentImage, []byte("services:\n core:\n image: thothii-core:verified\n"), 0o600); err != nil { t.Fatal(err) } var stdout, stderr bytes.Buffer if code := run(context.Background(), []string{"--installation", fixture.installationPath, "start"}, &stdout, &stderr); code != 0 { t.Fatalf("start exit = %d, stderr = %s", code, stderr.String()) } assertInvocationContains(t, fixture.invocations(t), "-f", currentImage, "up", "--detach", "--remove-orphans") } func TestRunExplainsWhenDockerIsNotAvailable(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) t.Setenv("PATH", t.TempDir()) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "status"}, &stdout, &stderr) if exitCode != 127 { t.Errorf("run() exit code = %d, want 127", exitCode) } if !strings.Contains(stderr.String(), "Docker is not installed or is not on PATH") { t.Errorf("stderr = %q, want Docker-not-found guidance", stderr.String()) } if strings.Contains(stderr.String(), "executable file") { t.Errorf("stderr leaked a process implementation detail: %q", stderr.String()) } } func TestRunDoctorValidatesTheRenderedInstallation(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if stdout.String() != "Doctor checks passed.\n" { t.Errorf("stdout = %q, want doctor success", stdout.String()) } } func TestRunDoctorDoesNotDereferenceSymlinksDuringLineEndingCheck(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) testsupport.SymlinkOrSkip( t, filepath.Join(fixture.projectDirectory, "missing-workspace.yaml"), filepath.Join(fixture.projectDirectory, "legacy-workspace.yaml"), ) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if stdout.String() != "Doctor checks passed.\n" { t.Errorf("stdout = %q, want doctor success", stdout.String()) } } func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) t.Setenv( "THT_FAKE_PS", "{\"Service\":\"core\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+ "{\"Service\":\"frontend\",\"State\":\"running\",\"Health\":\"healthy\"}", ) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if stdout.String() != "Doctor checks passed.\n" { t.Errorf("stdout = %q, want doctor success", stdout.String()) } } func TestRunPreservesChildExitCodes(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) t.Setenv("THT_FAKE_EXIT", "42") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "update", "--check-only"}, &stdout, &stderr) if exitCode != 42 { t.Errorf("run() exit code = %d, want 42", exitCode) } if stderr.String() != "fake Docker failure\n" { t.Errorf("stderr = %q, want child stderr", stderr.String()) } } func TestRunPiStatusUsesImageBundledPi(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "status"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if stdout.String() != "Pi version: 0.80.3\n" { t.Errorf("stdout = %q, want image-bundled Pi version", stdout.String()) } assertInvocationContains(t, fixture.invocations(t), "exec", "-T", "core", "pi", "--version") } func TestRunPiUpdateRequiresExplicitConfirmationWithoutInvokingDocker(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "update", "--version", "0.81.0", "--source", "build"}, &stdout, &stderr) if exitCode != 2 { t.Errorf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "requires --yes") { t.Errorf("stderr = %q, want explicit confirmation guidance", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunPiRestartRequiresExplicitConfirmationWithoutInvokingDocker(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "restart", }, &stdout, &stderr) if exitCode != 2 { t.Errorf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "pi restart requires --yes") { t.Errorf("stderr = %q, want explicit confirmation guidance", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunPiRestartSanitizesSuccessOutput(t *testing.T) { fixture := newCLIFixture(t, "PI_TOKEN_FILE=%s\nTHT_LLM_URL=https://llm.example.invalid\n") secretPath := filepath.Join(fixture.root, "pi-restart-secret") if err := os.WriteFile(secretPath, []byte("pi-restart-secret"), 0o600); err != nil { t.Fatal(err) } fixture.setEnvironment(t, secretPath) t.Setenv("THT_FAKE_PI_VERSION", "pi-restart-secret") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "restart", "--yes", }, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if !strings.Contains(stdout.String(), "Pi core restarted with the existing image;") { t.Fatalf("stdout = %q, want restart success", stdout.String()) } if strings.Contains(stdout.String()+stderr.String(), "pi-restart-secret") { t.Fatalf("Pi restart exposed a secret: stdout=%q stderr=%q", stdout.String(), stderr.String()) } } func TestRunPiUpdateRequiresExplicitSourceWithoutInvokingDocker(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "update", "--version", "0.81.0", "--yes", }, &stdout, &stderr) if exitCode != 2 { t.Errorf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "requires explicit --source build or pull") { t.Errorf("stderr = %q, want source guidance", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunPiConfigureReportsTheActualHostAuthFile(t *testing.T) { fixture := newCLIFixture(t, "") authFile := filepath.Join(fixture.root, "pi-auth.json") if err := os.WriteFile(authFile, []byte(`{"provider":"credential"}`), 0o600); err != nil { t.Fatal(err) } fixture.setEnvContents(t, "THT_LLM_URL=https://llm.example.invalid\nPI_AUTH_FILE="+authFile+"\n") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "configure", "--provider", "provider", "--model", "model", "--thinking", "medium", }, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit = %d, stderr=%s", exitCode, stderr.String()) } if !strings.Contains(stdout.String(), authFile) { t.Fatalf("stdout = %q, want host auth path", stdout.String()) } if strings.Contains(stdout.String(), "/home/thoth/.pi") { t.Fatalf("stdout exposed container-only auth path: %q", stdout.String()) } } func TestRunPiMaintenanceStatusAndRecoverConfirmationContract(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer if code := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "maintenance", "status", }, &stdout, &stderr); code != 0 { t.Fatalf("maintenance status exit = %d, stderr = %s", code, stderr.String()) } if stdout.String() != "Pi maintenance active: true (admissions: 0)\n" { t.Fatalf("maintenance status output = %q", stdout.String()) } second := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") second.setEnvironment(t) stdout.Reset() stderr.Reset() if code := run(context.Background(), []string{ "--installation", second.installationPath, "pi", "maintenance", "recover", }, &stdout, &stderr); code != 2 { t.Fatalf("maintenance recover without --yes exit = %d, want 2", code) } assertDockerNotInvoked(t, second) } func TestRunPiMaintenanceRecoverClearsRestartLifecycleState(t *testing.T) { fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n") fixture.setEnvironment(t) installation, err := config.Load(fixture.installationPath) if err != nil { t.Fatal(err) } const restartState = `{ "version": 4, "transaction": "restart-recovery", "phase": "preflight", "target": {"version": "0.80.3", "source": "restart"}, "previous": { "id": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "reference": "thothii-core:local", "mounts": [], "mount_fingerprint": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "configuration_sha256": "config" } }` if err := os.MkdirAll(filepath.Dir(installation.RestartStatePath()), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(installation.RestartStatePath(), []byte(restartState), 0o600); err != nil { t.Fatal(err) } var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "pi", "maintenance", "recover", "--yes", }, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if _, err := os.Stat(installation.RestartStatePath()); !os.IsNotExist(err) { t.Fatalf("restart state still exists: %v", err) } } func TestRunWorkspaceInspectDispatchesThroughTheMaintenanceService(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) t.Setenv("THT_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("a", 64)+`","operation":"inspect","completedStages":[]}`) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "abc", "--json"}, &stdout, &stderr) if exitCode != 0 { t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) } if !strings.Contains(stdout.String(), `"workspaceId":"abc"`) || !strings.Contains(stdout.String(), `"operation":"inspect"`) { t.Fatalf("stdout = %q", stdout.String()) } assertInvocationContains(t, fixture.invocations(t), "run", "--rm", "--no-deps", "--no-TTY", "--name") assertInvocationContains(t, fixture.invocations(t), "workspace-maintenance", "inspect") } func TestRunWorkspaceBlockedResultsExitThreeAndRenderHumanOutput(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) t.Setenv("THT_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"blocked","code":"manual_review_required","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("b", 64)+`","operation":"schema-suggest-fks","completedStages":["dwh"],"warnings":["review required"]}`) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "schema", "suggest-fks", "--workspace", "abc"}, &stdout, &stderr) if exitCode != 3 { t.Fatalf("run() exit code = %d, want 3", exitCode) } for _, expected := range []string{"workspace: abc", "status: blocked", "warning: review required"} { if !strings.Contains(stdout.String(), expected) { t.Fatalf("stdout = %q, missing %q", stdout.String(), expected) } } if strings.Contains(stdout.String(), "descriptorBlob") || strings.Contains(stdout.String(), strings.Repeat("b", 64)) { t.Fatalf("stdout leaked non-allowlisted fields: %q", stdout.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q", stderr.String()) } } func TestRunWorkspaceRequiresWorkspaceFlagBeforeDocker(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "workspace", "inspect", }, &stdout, &stderr) if exitCode != 2 { t.Fatalf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "--workspace") || !strings.Contains(stderr.String(), "required") { t.Fatalf("stderr = %q", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunWorkspaceRejectsDuplicateWorkspaceFlagsBeforeDocker(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "abc", "--workspace", "def", }, &stdout, &stderr) if exitCode != 2 { t.Fatalf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "--workspace") || !strings.Contains(stderr.String(), "exactly once") { t.Fatalf("stderr = %q", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunWorkspaceRejectsInvalidResumeRunIDsBeforeDocker(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "workspace", "preprocess", "dwh", "--workspace", "abc", "--resume", "not-a-run-id", }, &stdout, &stderr) if exitCode != 2 { t.Fatalf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "--resume") || !strings.Contains(stderr.String(), "32 lowercase hex") { t.Fatalf("stderr = %q", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunWorkspaceSchemaCheckRequiresReviewedCandidatesWithAnnotations(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) annotations := filepath.Join(fixture.root, "annotations.yaml") if err := os.WriteFile(annotations, []byte("reviewed: []\n"), 0o600); err != nil { t.Fatal(err) } var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{ "--installation", fixture.installationPath, "workspace", "schema", "check", "--workspace", "abc", "--annotations", annotations, }, &stdout, &stderr) if exitCode != 2 { t.Fatalf("run() exit code = %d, want 2", exitCode) } if !strings.Contains(stderr.String(), "--reviewed-candidates") { t.Fatalf("stderr = %q", stderr.String()) } assertDockerNotInvoked(t, fixture) } func TestRunPiStatusPreservesDockerExitCodeAndRedactsDiagnostics(t *testing.T) { fixture := newCLIFixture(t, "PI_TOKEN_FILE=%s\n") secretPath := filepath.Join(fixture.root, "pi-secret") if err := os.WriteFile(secretPath, []byte("pi-status-secret"), 0o600); err != nil { t.Fatal(err) } fixture.setEnvironment(t, secretPath) t.Setenv("THT_FAKE_FAIL_ON", "version") t.Setenv("THT_FAKE_FAILURE", "pi-status-secret") var stdout, stderr bytes.Buffer exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "status"}, &stdout, &stderr) if exitCode != 41 { t.Errorf("run() exit code = %d, want 41", exitCode) } if strings.Contains(stdout.String()+stderr.String(), "pi-status-secret") { t.Errorf("Pi status exposed a secret: stdout=%q stderr=%q", stdout.String(), stderr.String()) } } type cliFixture struct { root string installationPath string projectDirectory string envFile string argsFile string pathDirectory string envTemplate string } func newProjectWithoutInstallation(t *testing.T) string { t.Helper() root := t.TempDir() for _, directory := range []string{"backend", "frontend", "harness", "tools", "deploy", ".git"} { if err := os.MkdirAll(filepath.Join(root, directory), 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(root, "compose.yaml"), []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } return root } func newCLIFixture(t *testing.T, envTemplate string) cliFixture { t.Helper() temporaryRoot, err := filepath.EvalSymlinks(os.TempDir()) if err != nil { t.Fatal(err) } root, err := os.MkdirTemp(temporaryRoot, "tht-test-") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(root) }) projectDirectory := filepath.Join(root, "project") if err := os.MkdirAll(filepath.Join(projectDirectory, "deploy"), 0o755); err != nil { t.Fatal(err) } for _, path := range []string{filepath.Join(projectDirectory, "compose.yaml"), filepath.Join(projectDirectory, "deploy", "compose.local.yaml")} { if err := os.WriteFile(path, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } } envFile := filepath.Join(root, "installation.env") installationPath := filepath.Join(root, "thothii-installation.yaml") contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\n" if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil { t.Fatal(err) } pathDirectory := filepath.Join(root, "bin") if err := os.Mkdir(pathDirectory, 0o755); err != nil { t.Fatal(err) } argsFile := filepath.Join(root, "docker-args") fakeDocker := `#!/bin/sh printf '%s\n' "$@" >> "$THT_FAKE_ARGS" printf '%s\n' -- >> "$THT_FAKE_ARGS" case " $* " in *" ps --all --format json core frontend "*) printf '%s\n' "${THT_FAKE_STOPPED_PS:-[]}" ;; *" config --format json "*) if [ -n "${THT_FAKE_CONFIG:-}" ]; then printf '%s\n' "$THT_FAKE_CONFIG" else printf '%s\n' '{"volumes":{"settings":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}' fi ;; *" run --rm --no-deps --no-TTY session-migrate "*) if [ "${THT_FAKE_MIGRATION_EXIT:-0}" -ne 0 ]; then printf '%s\n' "$THT_FAKE_MIGRATION_FAILURE" >&2 exit "$THT_FAKE_MIGRATION_EXIT" fi printf '%s\n' '{"applied":[],"drifted":[],"pending":[]}' ;; *" ps --format json "*) printf '%s\n' "$THT_FAKE_PS" ;; *" ps -q core "*) printf '%s\n' 'core-id' ;; *" image inspect --format {{.Id}} "*) printf '%s\n' "${THT_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;; *"inspect --format {{.Image}} core-id"*) printf '%s\n' "${THT_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;; *"inspect --format {{json .Mounts}} core-id"*) printf '%s\n' '[{"Type":"volume","Name":"settings","Source":"settings","Destination":"/home/thoth/.pi","RW":true}]' ;; *"io.thothii.pi.version"*) printf '%s\n' "${THT_FAKE_PI_VERSION:-0.80.3}" ;; *"PI_VERSION"*) printf '%s\n' "${THT_FAKE_PI_VERSION:-0.80.3}" ;; *" pi --version "*) printf '%s\n' "${THT_FAKE_PI_VERSION:-0.80.3}" ;; *"/pi-management/options "*) printf '%s\n' '{"providers":["provider"],"models":[{"provider":"provider","id":"model"}],"reasoning":["low","medium","high"]}' ;; *"settings-cli.js --snapshot"*) printf '%s\n' '{"exists":false,"rawBase64":""}' ;; *"/settings "*) printf '%s\n' '{"provider":"provider","model":"model","thinking":"medium"}' ;; *"/pi-management/test "*) printf '%s\n' '{"ready":true}' ;; *"/sessions?scope="*) printf '%s\n' '[]' ;; *"/internal/maintenance/activate "*) printf '%s\n' '{"active":true,"admissions":0}' ;; *"/internal/maintenance/deactivate "*) printf '%s\n' '{"active":false,"admissions":0}' ;; *"/internal/maintenance/status "*) printf '%s\n' '{"active":true,"admissions":0}' ;; *" logs "*) printf '%s\n' "$THT_FAKE_LOG" ;; *" run --rm --no-deps --no-TTY "*" workspace-maintenance "*) printf '%s\n' "$THT_FAKE_WORKSPACE_RESULT" ;; esac if [ "${THT_FAKE_FAIL_ON:-}" = "version" ]; then printf '%s\n' "${THT_FAKE_FAILURE:-fake Docker failure}" >&2 exit 41 fi if [ "${THT_FAKE_EXIT:-0}" -ne 0 ]; then printf '%s\n' "${THT_FAKE_FAILURE:-fake Docker failure}" >&2 fi exit "${THT_FAKE_EXIT:-0}" ` if err := os.WriteFile(filepath.Join(pathDirectory, "docker"), []byte(fakeDocker), 0o700); err != nil { t.Fatal(err) } return cliFixture{root: root, installationPath: installationPath, projectDirectory: projectDirectory, envFile: envFile, argsFile: argsFile, pathDirectory: pathDirectory, envTemplate: envTemplate} } func (f cliFixture) setEnvironment(t *testing.T, values ...string) { t.Helper() env := f.envTemplate if len(values) > 0 { env = strings.Replace(env, "%s", values[0], 1) } f.setEnvContents(t, env) } func (f cliFixture) setEnvContents(t *testing.T, env string) { t.Helper() if err := os.WriteFile(f.envFile, []byte(env), 0o600); err != nil { t.Fatal(err) } t.Setenv("PATH", f.pathDirectory) t.Setenv("THT_FAKE_ARGS", f.argsFile) t.Setenv("THT_FAKE_EXIT", "0") t.Setenv("THT_FAKE_LOG", "") t.Setenv("THT_FAKE_FAILURE", "") t.Setenv("THT_FAKE_FAIL_ON", "") t.Setenv("THT_FAKE_STOPPED_PS", "[]") t.Setenv("THT_FAKE_PS", `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]`) t.Setenv("THT_FAKE_CONFIG", "") t.Setenv("THT_FAKE_MIGRATION_FAILURE", "") t.Setenv("THT_FAKE_MIGRATION_EXIT", "0") t.Setenv("THT_FAKE_WORKSPACE_RESULT", "") t.Setenv("THT_FAKE_IMAGE_ID", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") t.Setenv("THT_FAKE_PI_VERSION", "0.80.3") } func (f cliFixture) setProfile(t *testing.T, profile string) { t.Helper() composePath := filepath.Join(f.projectDirectory, "deploy", "compose."+profile+".yaml") if err := os.WriteFile(composePath, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } contents := "profile: " + profile + "\nprojectDirectory: " + f.projectDirectory + "\nenvFile: " + f.envFile + "\n" if err := os.WriteFile(f.installationPath, []byte(contents), 0o600); err != nil { t.Fatal(err) } } func (f cliFixture) invocations(t *testing.T) [][]string { t.Helper() contents, err := os.ReadFile(f.argsFile) if err != nil { t.Fatal(err) } var invocations [][]string var invocation []string for _, line := range strings.Split(strings.TrimSuffix(string(contents), "\n"), "\n") { if line == "--" { invocations = append(invocations, invocation) invocation = nil continue } invocation = append(invocation, line) } return invocations } func assertDockerNotInvoked(t *testing.T, fixture cliFixture) { t.Helper() if _, err := os.Stat(fixture.argsFile); !os.IsNotExist(err) { t.Errorf("Docker was invoked: stat error = %v", err) } } func assertInvocationContains(t *testing.T, invocations [][]string, want ...string) { t.Helper() for _, invocation := range invocations { for start := range invocation { if len(invocation)-start < len(want) { continue } if strings.Join(invocation[start:start+len(want)], "\x00") == strings.Join(want, "\x00") { return } } } t.Fatalf("invocations = %#v, want %#v", invocations, want) }