172 lines
5.7 KiB
Go
172 lines
5.7 KiB
Go
package modelmigration
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestRunEmitsValidatedCandidateWithoutChangingLegacyInputs(t *testing.T) {
|
|
fixture := migrationFixture(t, false)
|
|
original, _ := os.ReadFile(fixture.legacy)
|
|
request := fixture.request()
|
|
if err := Run(request); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
after, _ := os.ReadFile(fixture.legacy)
|
|
if string(after) != string(original) {
|
|
t.Fatal("legacy installation descriptor changed")
|
|
}
|
|
// The strict v2 loader deliberately refuses legacy Pi sources; removing them is the
|
|
// operator's explicit cut-over after reviewing the candidate.
|
|
var raw legacyDescriptor
|
|
decodeYAMLFile(t, fixture.legacy, &raw)
|
|
if err := os.Remove(filepath.Join(raw.ProjectDirectory, "deploy", "pi", "models.json")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Remove(filepath.Join(raw.ProjectDirectory, "deploy", "pi", "settings.json")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
candidate, err := config.Load(request.OutputPath)
|
|
if err != nil {
|
|
t.Fatalf("candidate config.Load() error = %v", err)
|
|
}
|
|
if candidate.SchemaVersion != 2 || candidate.ModelCatalog.Defaults.Interaction != "zai/glm-5.3" {
|
|
t.Fatalf("candidate = %#v", candidate.ModelCatalog)
|
|
}
|
|
if got := candidate.ModelCatalog.Defaults.Interaction; got != "zai/glm-5.3" {
|
|
t.Fatalf("metadata default = %q", got)
|
|
}
|
|
if candidate.ModelCatalog.Providers["deepseek"].Session.Mode != "pi_builtin" {
|
|
t.Fatal("built-in Pi model was not migrated")
|
|
}
|
|
}
|
|
|
|
func decodeYAMLFile(t *testing.T, path string, target any) {
|
|
t.Helper()
|
|
source, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := yaml.Unmarshal(source, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRunReportsProviderAuthenticationConflictAndPublishesNothing(t *testing.T) {
|
|
fixture := migrationFixture(t, true)
|
|
err := Run(fixture.request())
|
|
if err == nil || !strings.Contains(err.Error(), "metadataGeneration.models[0]") ||
|
|
!strings.Contains(err.Error(), "authentication") {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
if _, statErr := os.Stat(fixture.request().OutputPath); !os.IsNotExist(statErr) {
|
|
t.Fatalf("candidate was published after conflict: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestRunAcceptsVersionedYAMLCandidateFilename(t *testing.T) {
|
|
fixture := migrationFixture(t, false)
|
|
request := fixture.request()
|
|
request.OutputPath = filepath.Join(filepath.Dir(request.OutputPath), "thothii-installation.v2.yaml")
|
|
|
|
if err := Run(request); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
if info, err := os.Stat(request.OutputPath); err != nil || !info.Mode().IsRegular() {
|
|
t.Fatalf("versioned candidate: info=%v err=%v", info, err)
|
|
}
|
|
}
|
|
|
|
type migrationTestFixture struct {
|
|
legacy string
|
|
output string
|
|
}
|
|
|
|
func (f migrationTestFixture) request() Request {
|
|
return Request{
|
|
InstallationPath: f.legacy,
|
|
OutputPath: f.output,
|
|
SessionDefault: "zai/glm-5.3",
|
|
EmbeddingID: "ollama/qwen3-embedding:0.6b",
|
|
EmbeddingDimensions: 1024,
|
|
}
|
|
}
|
|
|
|
func migrationFixture(t *testing.T, conflict bool) migrationTestFixture {
|
|
t.Helper()
|
|
base, err := filepath.EvalSymlinks(os.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root, err := os.MkdirTemp(base, "tht-model-migration-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(root) })
|
|
project := filepath.Join(root, "project")
|
|
if err := os.MkdirAll(filepath.Join(project, "deploy", "pi"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{filepath.Join(project, "compose.yaml"), filepath.Join(project, "deploy", "compose.local.yaml")} {
|
|
if err := os.WriteFile(path, []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
env := filepath.Join(root, "operator.env")
|
|
secrets := filepath.Join(root, "secrets.env")
|
|
auth := filepath.Join(root, "auth")
|
|
if err := os.WriteFile(secrets, []byte("ZAI_API_KEY=test\nDEEPSEEK_API_KEY=test\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(env, []byte("THT_SECRETS_FILE="+secrets+"\nTHT_AUTH_CONFIG_ROOT="+auth+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(auth, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
metadata := ` default: glm-53
|
|
models:
|
|
- id: glm-53
|
|
label: GLM 5.3
|
|
litellm:
|
|
provider: openai
|
|
model: glm-5.3
|
|
endpoint:
|
|
baseUrl: https://api.z.ai/v1
|
|
apiKeyEnv: ZAI_API_KEY
|
|
`
|
|
if conflict {
|
|
metadata = ` default: deepseek-v4-pro
|
|
models:
|
|
- id: deepseek-v4-pro
|
|
label: DeepSeek
|
|
litellm: {provider: deepseek, model: deepseek-v4-pro}
|
|
apiKeyEnv: DEEPSEEK_API_KEY
|
|
`
|
|
}
|
|
legacy := filepath.Join(root, "legacy", "thothii-installation.yaml")
|
|
if err := os.MkdirAll(filepath.Dir(legacy), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
source := "profile: local\nprojectDirectory: " + project + "\nenvFile: " + env +
|
|
"\nmetadataGeneration:\n" + metadata +
|
|
"authentication:\n configDirectory: " + auth + "\n"
|
|
if err := os.WriteFile(legacy, []byte(source), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
models := `{"providers":{"zai":{"baseUrl":"https://api.z.ai/v1","api":"openai-completions","apiKey":"$ZAI_API_KEY","models":[{"id":"glm-5.3","name":"GLM 5.3","reasoning":true,"contextWindow":200000,"maxTokens":131072}]}}}`
|
|
settings := `{"defaultProjectTrust":"always","enabledModels":["deepseek/deepseek-v4-pro","zai/glm-5.3"]}`
|
|
if err := os.WriteFile(filepath.Join(project, "deploy", "pi", "models.json"), []byte(models), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(project, "deploy", "pi", "settings.json"), []byte(settings), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return migrationTestFixture{legacy: legacy, output: filepath.Join(root, "candidate", "thothii-installation.yaml")}
|
|
}
|