ai-code-battle/cmd/acb-index-builder/sitebuild_test.go
jedarden 88bd70640a fix(types): add missing ReplayPlayer import and type annotation for transcript feature
- Add ReplayPlayer to type imports in replay-viewer.ts
- Add explicit type annotation for entry parameter in replay.ts transcript map
- Fixes TypeScript compilation errors for §15.3 screen reader transcript feature
2026-04-22 18:20:56 -04:00

363 lines
11 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestExtractRegistry(t *testing.T) {
tests := []struct {
image string
want string
}{
{"forgejo.ardenone.com/ai-code-battle/acb-site-build:latest", "forgejo.ardenone.com"},
{"forgejo.ardenone.com/ai-code-battle/acb-site-build", "forgejo.ardenone.com"},
{"docker.io/library/nginx:latest", "docker.io"},
{"nginx:latest", "https://index.docker.io/v1/"},
{"localhost:5000/myimage:tag", "https://index.docker.io/v1/"},
{"", "https://index.docker.io/v1/"},
}
for _, tt := range tests {
got := extractRegistry(tt.image)
if got != tt.want {
t.Errorf("extractRegistry(%q) = %q, want %q", tt.image, got, tt.want)
}
}
}
func TestReadWriteCachedDigest(t *testing.T) {
dir := t.TempDir()
if d := readCachedDigest(dir); d != "" {
t.Errorf("expected empty digest for missing file, got %q", d)
}
writeCachedDigest(dir, "sha256:abc123\n")
if d := readCachedDigest(dir); d != "sha256:abc123" {
t.Errorf("readCachedDigest = %q, want %q", d, "sha256:abc123")
}
writeCachedDigest(dir, "sha256:def456\n")
if d := readCachedDigest(dir); d != "sha256:def456" {
t.Errorf("readCachedDigest after overwrite = %q, want %q", d, "sha256:def456")
}
}
func TestReadCachedDigest_InvalidPath(t *testing.T) {
d := readCachedDigest("/nonexistent/path/that/does/not/exist")
if d != "" {
t.Errorf("expected empty digest for nonexistent dir, got %q", d)
}
}
func TestWriteCachedDigest_InvalidPath(t *testing.T) {
// Should not panic, just log a warning
writeCachedDigest("/nonexistent/path", "sha256:abc")
}
func TestExtractedDistPath(t *testing.T) {
cfg := &Config{SiteBuildPath: "dist"}
got := extractedDistPath(cfg)
want := filepath.Join(siteBuildExtractDir, "dist")
if got != want {
t.Errorf("extractedDistPath() = %q, want %q", got, want)
}
cfg2 := &Config{SiteBuildPath: "build/output"}
got2 := extractedDistPath(cfg2)
want2 := filepath.Join(siteBuildExtractDir, "build/output")
if got2 != want2 {
t.Errorf("extractedDistPath() = %q, want %q", got2, want2)
}
}
func TestFallbackWebDir_NothingExists(t *testing.T) {
oldBakedIn := bakedInWebDist
bakedInWebDist = filepath.Join(t.TempDir(), "baked-in")
defer func() { bakedInWebDist = oldBakedIn }()
cfg := &Config{
SiteBuildPath: "dist",
OutputDir: t.TempDir(),
}
got := fallbackWebDir(cfg)
if got != bakedInWebDist {
t.Errorf("fallbackWebDir() = %q, want %q (baked-in)", got, bakedInWebDist)
}
}
func TestFallbackWebDir_ExtractedExists(t *testing.T) {
oldExtractDir := siteBuildExtractDir
siteBuildExtractDir = filepath.Join(t.TempDir(), "extract")
defer func() { siteBuildExtractDir = oldExtractDir }()
cfg := &Config{
SiteBuildPath: "dist",
OutputDir: t.TempDir(),
}
extractedPath := extractedDistPath(cfg)
if err := os.MkdirAll(extractedPath, 0755); err != nil {
t.Fatal(err)
}
got := fallbackWebDir(cfg)
if got != extractedPath {
t.Errorf("fallbackWebDir() = %q, want %q (extracted)", got, extractedPath)
}
}
func TestFallbackWebDir_BakedInExists(t *testing.T) {
bakedInDir := filepath.Join(t.TempDir(), "baked-in")
oldBakedIn := bakedInWebDist
bakedInWebDist = bakedInDir
defer func() { bakedInWebDist = oldBakedIn }()
oldExtractDir := siteBuildExtractDir
siteBuildExtractDir = filepath.Join(t.TempDir(), "extract")
defer func() { siteBuildExtractDir = oldExtractDir }()
if err := os.MkdirAll(bakedInDir, 0755); err != nil {
t.Fatal(err)
}
cfg := &Config{
SiteBuildPath: "dist",
OutputDir: t.TempDir(),
}
got := fallbackWebDir(cfg)
if got != bakedInDir {
t.Errorf("fallbackWebDir() = %q, want %q (baked-in)", got, bakedInDir)
}
}
func TestInitCraneAuth_NoAuth(t *testing.T) {
cfg := &Config{RegistryUsername: "", SiteBuildImage: ""}
if err := initCraneAuth(cfg); err != nil {
t.Errorf("initCraneAuth with no auth should be no-op, got %v", err)
}
}
func TestInitCraneAuth_WithAuth(t *testing.T) {
tmpDir := t.TempDir()
oldDir := craneConfigDir
craneConfigDir = filepath.Join(tmpDir, "crane-cfg")
defer func() { craneConfigDir = oldDir }()
cfg := &Config{
RegistryUsername: "testuser",
RegistryPassword: "testpass",
SiteBuildImage: "forgejo.example.com/ns/image:tag",
}
if err := initCraneAuth(cfg); err != nil {
t.Fatalf("initCraneAuth: %v", err)
}
data, err := os.ReadFile(filepath.Join(craneConfigDir, "config.json"))
if err != nil {
t.Fatalf("read config.json: %v", err)
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
t.Fatalf("parse config.json: %v", err)
}
auths, ok := config["auths"].(map[string]interface{})
if !ok {
t.Fatal("config.json missing 'auths' key")
}
if _, ok := auths["forgejo.example.com"]; !ok {
t.Error("config.json missing registry entry for forgejo.example.com")
}
}
func TestInitCraneAuth_PasswordOnly(t *testing.T) {
cfg := &Config{RegistryUsername: "", RegistryPassword: "pass", SiteBuildImage: "img"}
if err := initCraneAuth(cfg); err != nil {
t.Errorf("initCraneAuth with no username should be no-op, got %v", err)
}
}
func TestCraneEnviron_NoConfig(t *testing.T) {
env := craneEnviron()
hasDockerConfig := false
for _, e := range env {
if strings.HasPrefix(e, "DOCKER_CONFIG=") {
hasDockerConfig = true
}
}
if hasDockerConfig {
t.Error("craneEnviron should not set DOCKER_CONFIG when config.json doesn't exist")
}
}
func TestCopyWebAssets(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
os.MkdirAll(filepath.Join(srcDir, "js"), 0755)
os.MkdirAll(filepath.Join(srcDir, "css"), 0755)
os.WriteFile(filepath.Join(srcDir, "index.html"), []byte("<html></html>"), 0644)
os.WriteFile(filepath.Join(srcDir, "js", "app.js"), []byte("// app"), 0644)
os.WriteFile(filepath.Join(srcDir, "css", "style.css"), []byte("body{}"), 0644)
cfg := &Config{OutputDir: dstDir}
if err := copyWebAssets(cfg, srcDir); err != nil {
t.Fatalf("copyWebAssets: %v", err)
}
assertFileContent(t, filepath.Join(dstDir, "index.html"), "<html></html>")
assertFileContent(t, filepath.Join(dstDir, "js", "app.js"), "// app")
assertFileContent(t, filepath.Join(dstDir, "css", "style.css"), "body{}")
}
func TestCopyWebAssets_OverlaysOnExistingData(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
// Pre-existing data files in output directory
os.MkdirAll(filepath.Join(dstDir, "data"), 0755)
os.WriteFile(filepath.Join(dstDir, "data", "leaderboard.json"), []byte(`{"old": true}`), 0644)
// Site build has its own data placeholder
os.MkdirAll(filepath.Join(srcDir, "data"), 0755)
os.WriteFile(filepath.Join(srcDir, "index.html"), []byte("<html>"), 0644)
os.WriteFile(filepath.Join(srcDir, "data", "leaderboard.json"), []byte(`{"placeholder": true}`), 0644)
cfg := &Config{OutputDir: dstDir}
if err := copyWebAssets(cfg, srcDir); err != nil {
t.Fatalf("copyWebAssets: %v", err)
}
// Should have the site build's data (will be overwritten by generateAllIndexes later)
assertFileContent(t, filepath.Join(dstDir, "index.html"), "<html>")
assertFileContent(t, filepath.Join(dstDir, "data", "leaderboard.json"), `{"placeholder": true}`)
}
func TestCopyWebAssets_EmptySource(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
cfg := &Config{OutputDir: dstDir}
if err := copyWebAssets(cfg, srcDir); err != nil {
t.Fatalf("copyWebAssets with empty source: %v", err)
}
}
func TestCopyWebAssets_NonexistentSource(t *testing.T) {
cfg := &Config{OutputDir: t.TempDir()}
err := copyWebAssets(cfg, "/nonexistent/path")
if err == nil {
t.Error("expected error for nonexistent source")
}
}
func assertFileContent(t *testing.T, path, want string) {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
if string(data) != want {
t.Errorf("content of %s = %q, want %q", path, string(data), want)
}
}
func TestCleanStaleWebAssets(t *testing.T) {
outputDir := t.TempDir()
// Create a realistic output directory with mixed content
os.MkdirAll(filepath.Join(outputDir, "data", "bots"), 0755)
os.MkdirAll(filepath.Join(outputDir, "js"), 0755)
os.MkdirAll(filepath.Join(outputDir, "css"), 0755)
os.WriteFile(filepath.Join(outputDir, "index.html"), []byte("<html>old</html>"), 0644)
os.WriteFile(filepath.Join(outputDir, "js", "app.oldhash.js"), []byte("// old"), 0644)
os.WriteFile(filepath.Join(outputDir, "css", "style.oldhash.css"), []byte("body{}"), 0644)
os.WriteFile(filepath.Join(outputDir, "data", "leaderboard.json"), []byte(`{"entries":[]}`), 0644)
os.WriteFile(filepath.Join(outputDir, "data", "bots", "index.json"), []byte(`[]`), 0644)
os.WriteFile(filepath.Join(outputDir, ".site-build-digest"), []byte("sha256:abc123\n"), 0644)
cfg := &Config{OutputDir: outputDir}
if err := cleanStaleWebAssets(cfg); err != nil {
t.Fatalf("cleanStaleWebAssets: %v", err)
}
// data/ directory should be preserved
assertFileContent(t, filepath.Join(outputDir, "data", "leaderboard.json"), `{"entries":[]}`)
assertFileContent(t, filepath.Join(outputDir, "data", "bots", "index.json"), `[]`)
// .site-build-digest should be preserved
assertFileContent(t, filepath.Join(outputDir, ".site-build-digest"), "sha256:abc123\n")
// Old web assets should be removed
if _, err := os.Stat(filepath.Join(outputDir, "index.html")); err == nil {
t.Error("index.html should have been removed")
}
if _, err := os.Stat(filepath.Join(outputDir, "js")); err == nil {
t.Error("js/ directory should have been removed")
}
if _, err := os.Stat(filepath.Join(outputDir, "css")); err == nil {
t.Error("css/ directory should have been removed")
}
}
func TestCleanStaleWebAssets_EmptyDir(t *testing.T) {
outputDir := t.TempDir()
cfg := &Config{OutputDir: outputDir}
if err := cleanStaleWebAssets(cfg); err != nil {
t.Fatalf("cleanStaleWebAssets on empty dir: %v", err)
}
}
func TestVerifyMergedOutput(t *testing.T) {
outputDir := t.TempDir()
// Create minimal valid merged output
os.MkdirAll(filepath.Join(outputDir, "data"), 0755)
os.WriteFile(filepath.Join(outputDir, "index.html"), []byte("<html></html>"), 0644)
os.WriteFile(filepath.Join(outputDir, "data", "leaderboard.json"), []byte(`{}`), 0644)
cfg := &Config{OutputDir: outputDir}
if err := verifyMergedOutput(cfg); err != nil {
t.Errorf("verifyMergedOutput should succeed with valid output: %v", err)
}
}
func TestVerifyMergedOutput_MissingSPA(t *testing.T) {
outputDir := t.TempDir()
os.MkdirAll(filepath.Join(outputDir, "data"), 0755)
os.WriteFile(filepath.Join(outputDir, "data", "leaderboard.json"), []byte(`{}`), 0644)
cfg := &Config{OutputDir: outputDir}
if err := verifyMergedOutput(cfg); err == nil {
t.Error("expected error when SPA shell (index.html) is missing")
}
}
func TestVerifyMergedOutput_MissingDataDir(t *testing.T) {
outputDir := t.TempDir()
os.WriteFile(filepath.Join(outputDir, "index.html"), []byte("<html></html>"), 0644)
cfg := &Config{OutputDir: outputDir}
if err := verifyMergedOutput(cfg); err == nil {
t.Error("expected error when data/ directory is missing")
}
}
func TestVerifyMergedOutput_PartialDataOK(t *testing.T) {
outputDir := t.TempDir()
// Has SPA and data dir, but no leaderboard.json yet (first build)
os.MkdirAll(filepath.Join(outputDir, "data"), 0755)
os.WriteFile(filepath.Join(outputDir, "index.html"), []byte("<html></html>"), 0644)
cfg := &Config{OutputDir: outputDir}
// Should succeed — leaderboard missing is just a warning, not an error
if err := verifyMergedOutput(cfg); err != nil {
t.Errorf("verifyMergedOutput should succeed with partial data: %v", err)
}
}