package template_test import ( "bytes" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "chat-agent-system/internal/template" ) func repoRoot(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller") } return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")) } func TestDockerfileCopiesTemplatesAndConfigsToApp(t *testing.T) { raw, err := os.ReadFile(filepath.Join(repoRoot(t), "deploy", "app", "Dockerfile")) if err != nil { t.Fatal(err) } text := string(raw) for _, line := range []string{ "COPY templates /app/templates", "COPY configs /app/configs", "WORKDIR /app", } { if !strings.Contains(text, line) { t.Fatalf("deploy/app/Dockerfile must contain %q so the image runtime can read official docs", line) } } } func TestDockerignoreKeepsOfficialTemplateDocs(t *testing.T) { root := repoRoot(t) patterns := readDockerignore(t, filepath.Join(root, ".dockerignore")) docs, err := filepath.Glob(filepath.Join(root, "templates", "docs", "*.md")) if err != nil || len(docs) == 0 { t.Fatalf("templates/docs/*.md missing on disk: %v", err) } for _, abs := range docs { rel, err := filepath.Rel(root, abs) if err != nil { t.Fatal(err) } rel = filepath.ToSlash(rel) if dockerIgnored(patterns, rel) { t.Fatalf(".dockerignore excludes %s; COPY templates and go:embed docs/*.md cannot see it in the image", rel) } } if dockerIgnored(patterns, "templates/embed.go") { t.Fatal("templates/embed.go must stay in the go-builder context") } if dockerIgnored(patterns, "configs/agents/writer.md") { t.Fatal("configs/agents/*.md must stay in the image for GetAgentInstruction") } } func TestGetDocTemplate_AppRootLayout(t *testing.T) { root := repoRoot(t) app := filepath.Join(t.TempDir(), "app") dest := filepath.Join(app, "templates", "docs") if err := os.MkdirAll(dest, 0o755); err != nil { t.Fatal(err) } src := filepath.Join(root, "templates", "docs", "bd_prelim_design.md") body, err := os.ReadFile(src) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dest, "bd_prelim_design.md"), body, 0o644); err != nil { t.Fatal(err) } agentDir := filepath.Join(app, "configs", "agents") if err := os.MkdirAll(agentDir, 0o755); err != nil { t.Fatal(err) } writer, err := os.ReadFile(filepath.Join(root, "configs", "agents", "writer.md")) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(agentDir, "writer.md"), writer, 0o644); err != nil { t.Fatal(err) } t.Setenv("APP_ROOT", app) t.Chdir(app) got, err := template.GetDocTemplate("prelim_design") if err != nil { t.Fatal(err) } if !strings.Contains(got, "系统初步设计方案") || !strings.Contains(got, "子系统划分") { t.Fatalf(" /app layout must yield the official template, got %d bytes", len(got)) } if strings.Contains(got, "## 1. 结论") && !strings.Contains(got, "子系统划分") { t.Fatal("must not use the two-section stub") } inst, err := template.GetAgentInstruction("writer") if err != nil { t.Fatalf("image /app/configs/agents must be readable: %v", err) } if strings.TrimSpace(inst) == "" { t.Fatal("writer instruction empty") } } func TestDockerBuildContext_TemplatesDocsReadable(t *testing.T) { if _, err := exec.LookPath("docker"); err != nil { t.Skip("docker CLI not available") } root := repoRoot(t) df := filepath.Join(t.TempDir(), "Dockerfile") // Same context and COPY destinations as deploy/app/Dockerfile final stage. if err := os.WriteFile(df, []byte("FROM debian:bookworm-slim\nWORKDIR /app\nCOPY templates /app/templates\nCOPY configs /app/configs\n"), 0o644); err != nil { t.Fatal(err) } tag := "agent-system-templates-smoke:local" build := exec.Command("docker", "build", "-f", df, "-t", tag, ".") build.Dir = root out, err := runTimed(build, 3*time.Minute) if err != nil { t.Fatalf("docker build context failed: %v\n%s", err, out) } t.Cleanup(func() { _ = exec.Command("docker", "rmi", "-f", tag).Run() }) cat := exec.Command("docker", "run", "--rm", "--entrypoint", "cat", tag, "/app/templates/docs/bd_prelim_design.md") body, err := runTimed(cat, 30*time.Second) if err != nil { t.Fatalf("image missing /app/templates/docs/bd_prelim_design.md: %v\n%s", err, body) } if !bytes.Contains(body, []byte("系统初步设计方案")) || !bytes.Contains(body, []byte("子系统划分")) { t.Fatalf("image template is not the official prelim_design (%d bytes)", len(body)) } ls := exec.Command("docker", "run", "--rm", "--entrypoint", "ls", tag, "/app/configs/agents") agents, err := runTimed(ls, 30*time.Second) if err != nil { t.Fatalf("image missing /app/configs/agents: %v\n%s", err, agents) } if !bytes.Contains(agents, []byte("writer.md")) { t.Fatalf("image configs/agents incomplete: %s", agents) } } func runTimed(cmd *exec.Cmd, d time.Duration) ([]byte, error) { var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &buf if err := cmd.Start(); err != nil { return buf.Bytes(), err } done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case err := <-done: return buf.Bytes(), err case <-time.After(d): _ = cmd.Process.Kill() return buf.Bytes(), os.ErrDeadlineExceeded } } func readDockerignore(t *testing.T, path string) []string { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return strings.Split(string(raw), "\n") } func dockerIgnored(patterns []string, rel string) bool { rel = filepath.ToSlash(rel) ignored := false for _, raw := range patterns { p := strings.TrimSpace(raw) if p == "" || strings.HasPrefix(p, "#") { continue } negate := false if strings.HasPrefix(p, "!") { negate = true p = strings.TrimPrefix(p, "!") } if matchDockerPattern(p, rel) { ignored = !negate } } return ignored } func matchDockerPattern(pattern, rel string) bool { pattern = filepath.ToSlash(pattern) if pattern == rel { return true } if strings.HasSuffix(pattern, "/**/*.md") { prefix := strings.TrimSuffix(pattern, "/**/*.md") return strings.HasPrefix(rel, prefix+"/") && strings.HasSuffix(rel, ".md") } if pattern == "*.md" { return strings.HasSuffix(rel, ".md") } if !strings.Contains(pattern, "/") && !strings.Contains(pattern, "*") { return rel == pattern || strings.HasPrefix(rel, pattern+"/") } ok, _ := filepath.Match(pattern, rel) if ok { return true } ok, _ = filepath.Match(pattern, filepath.Base(rel)) return ok }