Files
ThothII/tools/tht/internal/config/installation_test.go

493 lines
21 KiB
Go

package config
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
func TestLoadSelectsLocalComposeFilesForAnInstallationInPathsWithSpaces(t *testing.T) {
t.Parallel()
installationPath, projectDirectory, envFile, override := writeInstallation(t, "local")
installation, err := Load(installationPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if installation.ProjectDirectory != projectDirectory {
t.Errorf("ProjectDirectory = %q, want %q", installation.ProjectDirectory, projectDirectory)
}
if installation.EnvFile != envFile {
t.Errorf("EnvFile = %q, want %q", installation.EnvFile, envFile)
}
if !strings.Contains(installationPath, "installation folder with spaces") {
t.Fatalf("test setup must exercise a path with spaces: %q", installationPath)
}
want := []string{
filepath.Join(projectDirectory, "compose.yaml"),
filepath.Join(projectDirectory, "deploy", "compose.local.yaml"),
override,
installation.ModelProjectionComposePath(),
}
assertStringsEqual(t, installation.ComposeFiles(), want)
}
func TestLoadSelectsServerComposeFiles(t *testing.T) {
t.Parallel()
installationPath, projectDirectory, _, override := writeInstallation(t, "server")
installation, err := Load(installationPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
want := []string{
filepath.Join(projectDirectory, "compose.yaml"),
filepath.Join(projectDirectory, "deploy", "compose.server.yaml"),
override,
installation.ModelProjectionComposePath(),
}
assertStringsEqual(t, installation.ComposeFiles(), want)
}
func TestLoadAcceptsServerRuntimeProjectionAndPlacesAutomaticOverrideBeforeCurrentImage(t *testing.T) {
installationPath, projectDirectory, envFile, override := writeInstallation(t, "server")
root := filepath.Dir(installationPath)
authDirectory := filepath.Join(root, "canonical-auth")
runtimeDirectory := filepath.Join(root, "runtime-auth")
automaticOverride := filepath.Join(projectDirectory, "deploy", "compose.auth-runtime-projection.yaml")
if err := os.WriteFile(automaticOverride, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
candidate := Installation{Path: installationPath, ProjectDirectory: projectDirectory}
currentImage := candidate.CurrentImageOverridePath()
if err := os.MkdirAll(filepath.Dir(currentImage), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(currentImage, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
writeRuntimeProjectionFixture(t, installationPath, envFile, "server", authDirectory, runtimeDirectory, runtimeDirectory, 10001, 10001, []string{override})
installation, err := Load(installationPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if !installation.HasRuntimeAuthProjection() {
t.Fatal("HasRuntimeAuthProjection() = false, want true")
}
projection := installation.RuntimeAuthProjection()
if projection == nil || projection.Directory != runtimeDirectory || projection.UID != 10001 || projection.GID != 10001 {
t.Fatalf("RuntimeAuthProjection() = %#v", projection)
}
projection.Directory = "mutated"
if got := installation.RuntimeAuthProjection(); got == nil || got.Directory != runtimeDirectory {
t.Fatalf("RuntimeAuthProjection() did not return an independent copy: %#v", got)
}
want := []string{
filepath.Join(projectDirectory, "compose.yaml"),
filepath.Join(projectDirectory, "deploy", "compose.server.yaml"),
override,
installation.ModelProjectionComposePath(),
automaticOverride,
currentImage,
}
assertStringsEqual(t, installation.ComposeFiles(), want)
}
func TestLoadRejectsInvalidRuntimeProjection(t *testing.T) {
for _, test := range []struct {
name string
profile string
configDirectory func(root string) string
runtimeDirectory func(root string) string
environmentRoot func(root string) string
uid, gid uint32
manualOverride bool
}{
{name: "local profile", profile: "local", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10001},
{name: "relative runtime directory", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(string) string { return "relative-runtime-auth" }, environmentRoot: func(string) string { return "relative-runtime-auth" }, uid: 10001, gid: 10001},
{name: "noncanonical runtime directory", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return root + "/runtime-auth/../runtime-auth" }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10001},
{name: "equal canonical and runtime directories", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "canonical-auth") }, uid: 10001, gid: 10001},
{name: "uid mismatch", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10000, gid: 10001},
{name: "gid mismatch", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10000},
{name: "missing runtime environment", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return "" }, uid: 10001, gid: 10001},
{name: "mismatched runtime environment", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "different-runtime-auth") }, uid: 10001, gid: 10001},
{name: "manual automatic override", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10001, manualOverride: true},
} {
t.Run(test.name, func(t *testing.T) {
installationPath, projectDirectory, envFile, override := writeInstallation(t, test.profile)
root := filepath.Dir(installationPath)
automaticOverride := filepath.Join(projectDirectory, "deploy", "compose.auth-runtime-projection.yaml")
if err := os.WriteFile(automaticOverride, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
overrides := []string{override}
if test.manualOverride {
overrides = append(overrides, automaticOverride)
}
writeRuntimeProjectionFixture(t, installationPath, envFile, test.profile, test.configDirectory(root), test.runtimeDirectory(root), test.environmentRoot(root), test.uid, test.gid, overrides)
if _, err := Load(installationPath); err == nil {
t.Fatal("Load() unexpectedly accepted an invalid runtime authentication projection")
}
})
}
}
func TestLoadRejectsRuntimeProjectionEnvironmentWithoutDescriptor(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "server")
authDirectory := filepath.Join(filepath.Dir(installationPath), "auth")
runtimeDirectory := filepath.Join(filepath.Dir(installationPath), "runtime-auth")
contents := "THT_AUTH_CONFIG_ROOT=" + strconv.Quote(authDirectory) + "\nTHT_AUTH_RUNTIME_ROOT=" + strconv.Quote(runtimeDirectory) + "\n"
if err := os.WriteFile(envFile, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
if _, err := Load(installationPath); err == nil {
t.Fatal("Load() unexpectedly accepted THT_AUTH_RUNTIME_ROOT without runtimeProjection")
}
}
func TestLoadRequiresAndReturnsTypedWorkspaceRepositoryForGitInstallations(t *testing.T) {
installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local")
gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-ssh.yaml")
if err := os.WriteFile(gitOverride, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
secretRoot := filepath.Dir(envFile)
privateKey := filepath.Join(secretRoot, "git-key")
knownHosts := filepath.Join(secretRoot, "known-hosts")
for _, file := range []string{privateKey, knownHosts} {
if err := os.WriteFile(file, []byte("fixture\n"), 0o600); err != nil {
t.Fatal(err)
}
}
remote := "git@gitea.example.org:clinical/workspaces.git"
environment := strings.Join([]string{
"THT_AUTH_CONFIG_ROOT=" + strconv.Quote(filepath.Join(filepath.Dir(envFile), "auth")),
"THT_WORKSPACE_GIT_REMOTE=" + remote,
"THT_WORKSPACE_GIT_BRANCH=main",
"THT_WORKSPACE_GIT_SSH_KEY_FILE=" + privateKey,
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=" + knownHosts,
}, "\n") + "\n"
if err := os.WriteFile(envFile, []byte(environment), 0o600); err != nil {
t.Fatal(err)
}
contents := "schemaVersion: 2\nprofile: local\nprojectDirectory: " + projectDirectory +
"\nenvFile: " + envFile +
"\nauthentication:\n configDirectory: " + filepath.Join(filepath.Dir(envFile), "auth") +
"\nworkspaceRepository:\n remote: " + remote +
"\n branch: main\n access: ssh\noverrides:\n - " + gitOverride + "\n" + minimalModelCatalogYAML()
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
installation, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
if installation.WorkspaceRepository.Remote != remote ||
installation.WorkspaceRepository.Branch != "main" ||
installation.WorkspaceRepository.Access != "ssh" {
t.Fatalf("WorkspaceRepository = %#v", installation.WorkspaceRepository)
}
}
func TestLoadRejectsGitOverrideWithoutTypedWorkspaceRepository(t *testing.T) {
installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local")
gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-https.yaml")
if err := os.WriteFile(gitOverride, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
contents := "schemaVersion: 2\nprofile: local\nprojectDirectory: " + projectDirectory +
"\nenvFile: " + envFile + "\noverrides:\n - " + gitOverride + "\n"
contents = strings.Replace(contents, "\noverrides:", "\nauthentication:\n configDirectory: "+filepath.Join(filepath.Dir(envFile), "auth")+"\noverrides:", 1)
contents += minimalModelCatalogYAML()
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), "workspaceRepository") {
t.Fatalf("Load() error = %v, want workspaceRepository error", err)
}
}
func TestComposeArgsAutomaticallyIncludeTheInstallationCurrentImageOverride(t *testing.T) {
t.Parallel()
installationPath, _, _, _ := writeInstallation(t, "local")
seed, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
currentImage := seed.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: candidate\n"), 0o600); err != nil {
t.Fatal(err)
}
installation, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
args := installation.ComposeArgs("up", "--detach")
want := []string{"-f", currentImage, "up", "--detach"}
if !containsSequence(args, want) {
t.Fatalf("ComposeArgs() = %#v, want durable override immediately before command", args)
}
}
func TestComposeArgsWithFinalOverridePreservesCurrentImagePrecedence(t *testing.T) {
installationPath, _, _, _ := writeInstallation(t, "server")
seed, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(seed.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(seed.CurrentImageOverridePath(), []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
final := filepath.Join(seed.ControlDirectory(), "migration.yaml")
if err := os.WriteFile(final, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
installation, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
args, err := installation.ComposeArgsWithFinalOverride(final, "--profile", "session-migrate", "config")
if err != nil {
t.Fatal(err)
}
want := []string{"-f", installation.CurrentImageOverridePath(), "-f", final, "--profile", "session-migrate", "config"}
if !containsSequence(args, want) {
t.Fatalf("ComposeArgsWithFinalOverride() = %#v, want %#v", args, want)
}
}
func TestPreservationPathsReturnsCanonicalBindRootsBackupsAndSecretFiles(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "server")
root := filepath.Dir(envFile)
var wanted []string
var lines []string
for _, item := range []struct{ key, name string }{
{"THT_DATA_ROOT", "data"},
{"THT_PI_STATE_ROOT", "pi-state"},
{"THT_WORKSPACE_REGISTRY_ROOT", "workspace-registry"},
{"THT_BACKUP_ROOT", "backups"},
} {
path := filepath.Join(root, item.name)
if err := os.Mkdir(path, 0o700); err != nil {
t.Fatal(err)
}
wanted = append(wanted, path)
lines = append(lines, item.key+"="+path)
}
secret := filepath.Join(root, "secret")
if err := os.WriteFile(secret, []byte("secret"), 0o600); err != nil {
t.Fatal(err)
}
wanted = append(wanted, secret)
lines = append(lines, "APP_TOKEN_FILE="+secret)
lines = append(lines, "THT_AUTH_CONFIG_ROOT="+strconv.Quote(filepath.Join(root, "auth")))
if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
t.Fatal(err)
}
installation, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
got, err := installation.PreservationPaths()
if err != nil {
t.Fatal(err)
}
assertStringsEqual(t, got, wanted)
}
func TestInstallationControlPathsAreIsolatedForDescriptorsSharingOneCheckout(t *testing.T) {
projectDirectory := t.TempDir()
first := Installation{Path: filepath.Join(t.TempDir(), installationFileName), ProjectDirectory: projectDirectory}
second := Installation{Path: filepath.Join(t.TempDir(), installationFileName), ProjectDirectory: projectDirectory}
if first.CurrentImageOverridePath() == second.CurrentImageOverridePath() {
t.Fatalf("shared-checkout installations reused %q", first.CurrentImageOverridePath())
}
for _, installation := range []Installation{first, second} {
if filepath.Dir(filepath.Dir(installation.CurrentImageOverridePath())) != filepath.Join(projectDirectory, ".tht") {
t.Fatalf("current-image path %q is not installation-specific under .tht", installation.CurrentImageOverridePath())
}
if filepath.Dir(installation.UpdateStatePath()) != filepath.Dir(installation.CurrentImageOverridePath()) {
t.Fatalf("state %q and selector %q do not share one installation control directory", installation.UpdateStatePath(), installation.CurrentImageOverridePath())
}
if got, want := installation.RestartStatePath(), filepath.Join(installation.ControlDirectory(), "restart-state.json"); got != want {
t.Fatalf("RestartStatePath() = %q, want %q", got, want)
}
}
}
func TestLoadRejectsRelativeInstallationPaths(t *testing.T) {
t.Parallel()
_, err := Load("thothii-installation.yaml")
if err == nil || !strings.Contains(err.Error(), "absolute") {
t.Fatalf("Load() error = %v, want an absolute-path error", err)
}
}
func TestLoadRequiresCanonicalAuthenticationDirectoryMatchingEnvironment(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
authDirectory := filepath.Join(filepath.Dir(installationPath), "auth")
contents, err := os.ReadFile(installationPath)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(envFile, []byte("THT_AUTH_CONFIG_ROOT="+strconv.Quote(authDirectory)+"\n"), 0o600); err != nil {
t.Fatal(err)
}
installation, err := Load(installationPath)
if err != nil {
t.Fatal(err)
}
if got := installation.AuthenticationDirectory(); got != authDirectory {
t.Fatalf("AuthenticationDirectory() = %q, want %q", got, authDirectory)
}
for _, invalid := range []string{"relative/auth", authDirectory + "/../auth"} {
bad := strings.Replace(string(contents), authDirectory, invalid, 1)
if err := os.WriteFile(installationPath, []byte(bad), 0o600); err != nil {
t.Fatal(err)
}
if _, err := Load(installationPath); err == nil {
t.Fatalf("Load accepted unsafe auth directory %q", invalid)
}
}
}
func TestParseAuthenticationDirectoryEnvironment(t *testing.T) {
values, err := parseComposeDotenv([]byte("THT_AUTH_CONFIG_ROOT=\"/tmp/auth\"\n"))
if err != nil || values["THT_AUTH_CONFIG_ROOT"] != "/tmp/auth" {
t.Fatalf("values=%#v err=%v", values, err)
}
}
func writeRuntimeProjectionFixture(t *testing.T, installationPath, envFile, profile, configDirectory, runtimeDirectory, environmentRoot string, uid, gid uint32, overrides []string) {
t.Helper()
lines := []string{"THT_AUTH_CONFIG_ROOT=" + strconv.Quote(configDirectory)}
if environmentRoot != "" {
lines = append(lines, "THT_AUTH_RUNTIME_ROOT="+strconv.Quote(environmentRoot))
}
if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
t.Fatal(err)
}
projectDirectory := filepath.Join(filepath.Dir(installationPath), "project directory with spaces")
contents := "schemaVersion: 2\nprofile: " + profile + "\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + configDirectory + "\n runtimeProjection:\n directory: " + runtimeDirectory + "\n uid: " + strconv.FormatUint(uint64(uid), 10) + "\n gid: " + strconv.FormatUint(uint64(gid), 10) + "\noverrides:\n"
for _, override := range overrides {
contents += " - " + override + "\n"
}
contents += minimalModelCatalogYAML()
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
func writeInstallation(t *testing.T, profile string) (string, string, string, string) {
t.Helper()
temporaryRoot, err := filepath.EvalSymlinks(os.TempDir())
if err != nil {
t.Fatal(err)
}
physicalRoot, err := os.MkdirTemp(temporaryRoot, "tht-config-")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(physicalRoot) })
root := filepath.Join(physicalRoot, "installation folder with spaces")
projectDirectory := filepath.Join(root, "project directory with spaces")
if err := os.MkdirAll(filepath.Join(projectDirectory, "deploy"), 0o755); err != nil {
t.Fatal(err)
}
for _, name := range []string{"compose.yaml", filepath.Join("deploy", "compose.local.yaml"), filepath.Join("deploy", "compose.server.yaml")} {
if err := os.WriteFile(filepath.Join(projectDirectory, name), []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
}
envFile := filepath.Join(root, "environment file.env")
authDirectory := filepath.Join(root, "auth")
if err := os.WriteFile(envFile, []byte("SAFE_VALUE=1\nTHT_AUTH_CONFIG_ROOT="+strconv.Quote(authDirectory)+"\n"), 0o600); err != nil {
t.Fatal(err)
}
override := filepath.Join(root, "extra override.yaml")
if err := os.WriteFile(override, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
installationPath := filepath.Join(root, "thothii-installation.yaml")
contents := "schemaVersion: 2\nprofile: " + profile + "\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + authDirectory + "\noverrides:\n - " + override + "\n" + minimalModelCatalogYAML()
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return installationPath, projectDirectory, envFile, override
}
func minimalModelCatalogYAML() string {
return `modelCatalog:
defaults:
session: deepseek/deepseek-v4-pro
embedding:
id: ollama/qwen3-embedding:0.6b
dimensions: 1024
providers:
deepseek:
authentication:
mode: pi_auth
session:
mode: pi_builtin
models:
deepseek-v4-pro:
session: {}
`
}
func assertStringsEqual(t *testing.T, got, want []string) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("length = %d, want %d: got %#v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("value[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func containsSequence(values, wanted []string) bool {
for start := range values {
if len(values)-start < len(wanted) {
continue
}
matched := true
for offset := range wanted {
if values[start+offset] != wanted[offset] {
matched = false
break
}
}
if matched {
return true
}
}
return false
}