package service import ( "context" "errors" "io" "strings" "testing" "github.com/aritmolab/thothii/tools/tht/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/config" ) // Catches --build being ignored, which would run stale images after a checkout update. func TestStartBuildsBeforeStartingAndCheckingHealth(t *testing.T) { stubModelProjection(t, nil) runner := &recordingRunner{} installation := testInstallation() if err := Start(context.Background(), installation, runner, true); err != nil { t.Fatal(err) } if got, want := strings.Join(runner.stages, " -> "), "build -> up -> health"; got != want { t.Fatalf("Start(..., build=true) stages = %q, want %q", got, want) } } // Catches a normal start unnecessarily rebuilding images. func TestStartSkipsBuildUnlessRequested(t *testing.T) { stubModelProjection(t, nil) runner := &recordingRunner{} installation := testInstallation() if err := Start(context.Background(), installation, runner, false); err != nil { t.Fatal(err) } if got, want := strings.Join(runner.stages, " -> "), "up -> health"; got != want { t.Fatalf("Start(..., build=false) stages = %q, want %q", got, want) } } func TestStartRefusesProjectedAuthenticationBeforeComposeWhenNotReady(t *testing.T) { stubModelProjection(t, nil) for _, state := range []string{"missing", "blocked", "divergent"} { t.Run(state, func(t *testing.T) { previous := requireRuntimeAuthProjectionReady requireRuntimeAuthProjectionReady = func(installation config.Installation) error { if !installation.HasRuntimeAuthProjection() { t.Fatal("readiness gate received an unprojected installation") } return errors.New("synthetic " + state + " projection") } t.Cleanup(func() { requireRuntimeAuthProjectionReady = previous }) runner := &recordingRunner{} err := Start(context.Background(), projectedTestInstallation(), runner, true) if err == nil { t.Fatalf("Start() accepted %s runtime projection", state) } if len(runner.stages) != 0 { t.Fatalf("Start() reached Compose for %s projection: %v", state, runner.stages) } }) } } func TestStartRefusesProjectionFailureBeforeCompose(t *testing.T) { stubModelProjection(t, errors.New("synthetic projection failure")) runner := &recordingRunner{} err := Start(context.Background(), testInstallation(), runner, false) if err == nil || !strings.Contains(err.Error(), "model runtime projection") { t.Fatalf("Start() error = %v", err) } if len(runner.stages) != 0 { t.Fatalf("Start() reached Compose: %v", runner.stages) } } func stubModelProjection(t *testing.T, result error) { t.Helper() previous := generateModelProjections generateModelProjections = func(config.Installation) error { return result } t.Cleanup(func() { generateModelProjections = previous }) } type recordingRunner struct{ stages []string } func (r *recordingRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) { joined := strings.Join(args, " ") switch { case strings.HasSuffix(joined, " build"): r.stages = append(r.stages, "build") case strings.HasSuffix(joined, " up --detach --remove-orphans"): r.stages = append(r.stages, "up") case strings.HasSuffix(joined, " ps --all --format json"): r.stages = append(r.stages, "health") return compose.Result{Stdout: healthyServices}, nil } return compose.Result{}, nil } func testInstallation() config.Installation { return config.Installation{ Path: "/tmp/thothii/deploy/local/thothii-installation.yaml", Profile: "local", ProjectDirectory: "/tmp/thothii", EnvFile: "/tmp/thothii/deploy/local/operator.env", } } func projectedTestInstallation() config.Installation { installation := testInstallation() installation.Profile = "server" installation.Authentication.RuntimeProjection = &config.RuntimeProjection{ Directory: "/runtime-auth", UID: 10001, GID: 10001, } return installation } const healthyServices = `[ {"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} ]`