Files
ThothII/tools/tht/internal/modelprojection/shell_projection_test.go
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

140 lines
5.2 KiB
Go

package modelprojection
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"gopkg.in/yaml.v3"
)
func TestRenderPublicShellConfiguration(t *testing.T) {
for _, test := range []struct {
name string
shell config.Shell
want string
}{
{"legacy", config.Shell{}, `{"backendBaseUrl":"/api","shell":{"mode":"embedded","defaultLocale":"en","adapter":"omics-portal"}}`},
{"full", config.Shell{Mode: "full", Adapter: "omics-portal"}, `{"backendBaseUrl":"/api","shell":{"mode":"full","defaultLocale":"en"}}`},
{"Italian", config.Shell{Mode: "embedded", DefaultLocale: "it"}, `{"backendBaseUrl":"/api","shell":{"mode":"embedded","defaultLocale":"it","adapter":"omics-portal"}}`},
{"locale without catalog", config.Shell{Mode: "full", DefaultLocale: "fr-CA"}, `{"backendBaseUrl":"/api","shell":{"mode":"full","defaultLocale":"fr-CA"}}`},
} {
t.Run(test.name, func(t *testing.T) {
installation := projectionFixture(t)
installation.Shell = test.shell
files, err := Render(installation)
if err != nil {
t.Fatal(err)
}
// The entire public payload is an allowlist: no model endpoints, auth or host paths.
js := string(files["frontend/config.js"])
if !strings.HasPrefix(js, "window.__THOTHII_CONFIG__ = ") || !strings.HasSuffix(js, ";\n") {
t.Fatalf("invalid public config script: %q", js)
}
payload := strings.TrimSuffix(strings.TrimPrefix(js, "window.__THOTHII_CONFIG__ = "), ";\n")
var got, want any
if err := json.Unmarshal([]byte(payload), &got); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal([]byte(test.want), &want); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("public config = %s, want %s", payload, test.want)
}
var compose struct {
Services map[string]struct {
Volumes []struct {
Type string `yaml:"type"`
Source string `yaml:"source"`
Target string `yaml:"target"`
ReadOnly bool `yaml:"read_only"`
} `yaml:"volumes"`
} `yaml:"services"`
}
if err := yaml.Unmarshal(files[ComposeFile], &compose); err != nil {
t.Fatal(err)
}
mounts := compose.Services["frontend"].Volumes
if len(mounts) != 1 || mounts[0].Type != "bind" || !mounts[0].ReadOnly ||
mounts[0].Source != filepath.Join(installation.GeneratedDirectory(), "frontend", "config.js") ||
mounts[0].Target != "/usr/share/nginx/html/config.js" {
t.Fatalf("frontend mounts = %#v; want only public config.js, read-only", mounts)
}
})
}
}
func TestGenerateRefreshesPublicShellConfiguration(t *testing.T) {
installation := projectionFixture(t)
if err := Generate(installation); err != nil {
t.Fatal(err)
}
publicPath := filepath.Join(installation.GeneratedDirectory(), "frontend", "config.js")
before, err := os.ReadFile(publicPath)
if err != nil {
t.Fatal(err)
}
composeBefore, err := os.ReadFile(installation.ModelProjectionComposePath())
if err != nil {
t.Fatal(err)
}
installation.Shell = config.Shell{Mode: "full", DefaultLocale: "it"}
drift, err := Check(installation)
if err != nil || !reflect.DeepEqual(drift, []string{"compose.models.yaml", "frontend/config.js"}) {
t.Fatalf("shell drift = %v, %v", drift, err)
}
if err := Generate(installation); err != nil {
t.Fatal(err)
}
after, err := os.ReadFile(publicPath)
if err != nil || bytes.Equal(before, after) || !bytes.Contains(after, []byte(`"mode": "full"`)) || bytes.Contains(after, []byte(`"adapter"`)) {
t.Fatalf("updated public config = %s, %v", after, err)
}
composeAfter, err := os.ReadFile(installation.ModelProjectionComposePath())
if err != nil {
t.Fatal(err)
}
// A config revision changes Compose's service definition, ensuring active bind mounts refresh.
var oldCompose, newCompose map[string]map[string]map[string]any
if err := yaml.Unmarshal(composeBefore, &oldCompose); err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal(composeAfter, &newCompose); err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(oldCompose["services"]["frontend"]["environment"], newCompose["services"]["frontend"]["environment"]) {
t.Fatal("shell change did not revise the frontend service")
}
if !reflect.DeepEqual(oldCompose["services"]["core"], newCompose["services"]["core"]) {
t.Fatal("shell-only change revised core configuration")
}
if drift, err := Check(installation); err != nil || len(drift) != 0 {
t.Fatalf("fresh generation drift = %v, %v", drift, err)
}
}
func TestGeneratedPublicShellPassesFrontendImageSmoke(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("frontend image smoke requires a POSIX host shell")
}
for _, shell := range []config.Shell{{}, {Mode: "full"}, {Mode: "full", DefaultLocale: "it"}, {Mode: "embedded", DefaultLocale: "it"}, {Mode: "full", DefaultLocale: "fr-CA"}, {Mode: "embedded", DefaultLocale: "zh-Hant-TW-u-nu-hanidec"}} {
installation := projectionFixture(t)
installation.Shell = shell
if err := Generate(installation); err != nil {
t.Fatal(err)
}
command := exec.Command("sh", "../../../../docker/smoke/frontend-smoke.sh", installation.GeneratedFrontendConfigPath())
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("frontend smoke for %#v: %v\n%s", shell, err, output)
}
}
}