package main import ( "bytes" "context" "encoding/json" "io" "os" "path/filepath" "strings" "testing" ) // Catches version regressing into a descriptor-dependent operator command. func TestVersionCommandIsDescriptorFreeAndMachineReadable(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() { _ = os.Chdir(previousDirectory) }) var stdout, stderr bytes.Buffer if code := run(context.Background(), []string{"version", "--json"}, &stdout, &stderr); code != 0 { t.Fatalf("run(version --json) = %d, stderr = %q", code, stderr.String()) } var value struct { Version string `json:"version"` Commit string `json:"commit"` OS string `json:"os"` Arch string `json:"arch"` } decoder := json.NewDecoder(&stdout) if err := decoder.Decode(&value); err != nil || value.Version == "" || value.Commit == "" || value.OS == "" || value.Arch == "" { t.Fatalf("version JSON = %q, decoded %#v, err = %v", stdout.String(), value, err) } if err := decoder.Decode(&struct{}{}); err != io.EOF { t.Fatalf("version stdout contains more than one JSON document: %q", stdout.String()) } } // Catches doctor --json writing a banner, progress, or Docker diagnostics to stdout. func TestDoctorJSONWritesOnlyOneReportDocument(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) fullHealth := `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"},{"Service":"qdrant","State":"running","Health":"healthy"},{"Service":"embedding","State":"running","Health":"healthy"},{"Service":"embedding-model-init","State":"exited","ExitCode":0}]` t.Setenv("THT_FAKE_PS", fullHealth) var stdout, stderr bytes.Buffer code := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor", "--json"}, &stdout, &stderr) if code != 0 && code != 1 { t.Fatalf("run(doctor --json) = %d, stderr = %q", code, stderr.String()) } var report struct { OK *bool `json:"ok"` Checks []json.RawMessage `json:"checks"` } decoder := json.NewDecoder(&stdout) if err := decoder.Decode(&report); err != nil || report.OK == nil || report.Checks == nil { t.Fatalf("doctor JSON = %q, decoded %#v, err = %v", stdout.String(), report, err) } if err := decoder.Decode(&struct{}{}); err != io.EOF { t.Fatalf("doctor stdout contains more than one JSON document: %q", stdout.String()) } if strings.Contains(stdout.String(), "Doctor checks passed") || strings.Contains(stdout.String(), "tht:") { t.Fatalf("doctor JSON stdout contains presentation text: %q", stdout.String()) } } // Catches start --build being parsed as an argument but never reaching the shared lifecycle runner. func TestStartBuildOptionBuildsBeforeUpWhilePlainStartDoesNotBuild(t *testing.T) { fullHealth := `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"},{"Service":"qdrant","State":"running","Health":"healthy"},{"Service":"embedding","State":"running","Health":"healthy"},{"Service":"embedding-model-init","State":"exited","ExitCode":0}]` for _, test := range []struct { name string args []string wantBuild bool }{ {name: "plain", args: []string{"start"}, wantBuild: false}, {name: "build", args: []string{"start", "--build"}, wantBuild: true}, } { t.Run(test.name, func(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) t.Setenv("THT_FAKE_PS", fullHealth) var stdout, stderr bytes.Buffer if code := run(context.Background(), append([]string{"--installation", fixture.installationPath}, test.args...), &stdout, &stderr); code != 0 { t.Fatalf("run(%v) = %d, stderr = %q", test.args, code, stderr.String()) } calls := fixture.invocations(t) joined := make([]string, 0, len(calls)) for _, call := range calls { joined = append(joined, strings.Join(call, " ")) } sequence := strings.Join(joined, "\n") hasBuild := strings.Contains(sequence, " build") if hasBuild != test.wantBuild { t.Fatalf("start calls = %s, build present = %t, want %t", sequence, hasBuild, test.wantBuild) } if test.wantBuild && strings.Index(sequence, " build") > strings.Index(sequence, " up --detach --remove-orphans") { t.Fatalf("start calls = %s, want build before up", sequence) } }) } }