package gsp import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestLoadValidateAndFlatten(t *testing.T) { root := t.TempDir() design := filepath.Join(root, "design") if err := os.MkdirAll(design, 0755); err != nil { t.Fatal(err) } writeTestFile(t, design, "page.gsp", `id: page.lottery.main title: Lottery Page type: page resolution: L3 context: Lottery page. with: - id: ui.button.primary context: Main action. - feedback.positive refines: page.lottery.base `) writeTestFile(t, design, "button.gsp", `id: ui.button.primary type: ui resolution: L3 context: Primary button. `) writeTestFile(t, design, "feedback.gsp", `id: feedback.positive type: feedback resolution: L2 context: Positive feedback. `) writeTestFile(t, design, "base.gsp", `id: page.lottery.base resolution: L2 context: Base lottery page. `) project, err := LoadProject(root) if err != nil { t.Fatal(err) } report := project.Validate("") if !report.OK { t.Fatalf("expected valid project, got errors: %+v", report.Errors) } flat := project.Flatten("page.lottery.main", -1, Filter{}) got := ids(flat.Units) want := []string{"page.lottery.main", "page.lottery.base", "feedback.positive", "ui.button.primary"} if !sameStrings(got, want) { t.Fatalf("flatten ids = %v, want %v", got, want) } index := project.Index() if index[2].Title != "Lottery Page" { t.Fatalf("index title = %q", index[2].Title) } graph := project.Graph("page.lottery.main", -1) if !strings.Contains(graph.Mermaid(), `["Lottery Page"]`) { t.Fatalf("expected Mermaid to use title, got:\n%s", graph.Mermaid()) } if !strings.Contains(graph.Markdown(), "```mermaid") { t.Fatalf("expected Markdown Mermaid block") } canvasData, err := graph.Canvas() if err != nil { t.Fatal(err) } var canvas struct { Nodes []struct { Text string `json:"text"` } `json:"nodes"` Edges []struct { Label string `json:"label"` } `json:"edges"` } if err := json.Unmarshal(canvasData, &canvas); err != nil { t.Fatal(err) } foundTitle := false for _, node := range canvas.Nodes { if strings.Contains(node.Text, "Lottery Page") { foundTitle = true } } if !foundTitle { t.Fatalf("expected canvas node text to include title, got %+v", canvas.Nodes) } pack := project.PackFor("page.lottery.main", "implement", "implement", -1, 0, Filter{}) if pack.Summary.UnitCount != 4 { t.Fatalf("pack unit count = %d", pack.Summary.UnitCount) } if !strings.Contains(pack.Markdown(), "GSP Context Pack") { t.Fatal("expected pack markdown") } impact := project.Impact("feedback.positive", -1) if impact.Summary.DirectCount != 1 { t.Fatalf("impact direct count = %d", impact.Summary.DirectCount) } if impact.Direct[0].ID != "page.lottery.main" { t.Fatalf("impact direct = %+v", impact.Direct) } if !strings.Contains(impact.Markdown(), "GSP Impact") { t.Fatal("expected impact markdown") } } func TestValidateMissingReference(t *testing.T) { root := t.TempDir() design := filepath.Join(root, "design") if err := os.MkdirAll(design, 0755); err != nil { t.Fatal(err) } writeTestFile(t, design, "page.gsp", `id: page.missing with: - ui.missing `) project, err := LoadProject(root) if err != nil { t.Fatal(err) } report := project.Validate("") if report.OK { t.Fatal("expected validation failure") } found := false for _, issue := range report.Errors { if issue.Code == "missing_with" { found = true } } if !found { t.Fatalf("expected missing_with error, got %+v", report.Errors) } } func TestStageCheck(t *testing.T) { root := t.TempDir() design := filepath.Join(root, "design") if err := os.MkdirAll(design, 0755); err != nil { t.Fatal(err) } writeTestFile(t, design, "page.gsp", `id: page.low resolution: L2 `) project, err := LoadProject(root) if err != nil { t.Fatal(err) } report := project.StageCheck("implement") if report.OK { t.Fatal("expected implement stage check to fail") } if report.Errors[0].Code != "low_resolution" { t.Fatalf("expected low_resolution, got %+v", report.Errors) } } func TestValidateUnsupportedGSPVersion(t *testing.T) { root := t.TempDir() design := filepath.Join(root, "design") if err := os.MkdirAll(design, 0755); err != nil { t.Fatal(err) } writeTestFile(t, root, "gsp.manifest", `gspVersion: 9.9 scan: - design `) writeTestFile(t, design, "entry.gsp", `id: project.entry `) project, err := LoadProject(root) if err != nil { t.Fatal(err) } report := project.Validate("") if report.OK { t.Fatal("expected unsupported version failure") } if report.Errors[0].Code != "unsupported_gsp_version" { t.Fatalf("expected unsupported_gsp_version, got %+v", report.Errors) } } func TestInitProjectCreatesManifestAndEntry(t *testing.T) { root := filepath.Join(t.TempDir(), "sample-project") result, err := InitProject(root, InitOptions{ Name: "sample", Entry: "page.sample.main", GSPVersion: "0.1", ToolkitVersion: "0.1.0", }) if err != nil { t.Fatal(err) } if len(result.Created) == 0 { t.Fatal("expected created paths") } project, err := LoadProject(root) if err != nil { t.Fatal(err) } if project.Manifest == nil { t.Fatal("expected manifest") } if project.Manifest.Entry[0] != "page.sample.main" { t.Fatalf("manifest entry = %v", project.Manifest.Entry) } if _, ok := project.ByID["page.sample.main"]; !ok { t.Fatal("expected initialized entry GSP") } report := project.Validate("") if !report.OK { t.Fatalf("expected valid initialized project, got %+v", report.Errors) } } func TestInitProjectRefusesOverwrite(t *testing.T) { root := t.TempDir() if _, err := InitProject(root, InitOptions{}); err != nil { t.Fatal(err) } if _, err := InitProject(root, InitOptions{}); err == nil { t.Fatal("expected overwrite refusal") } } func TestInitAIUsageCreatesCoreFiles(t *testing.T) { root := t.TempDir() if _, err := InitProject(root, InitOptions{ Name: "sample", Entry: "page.sample.main", GSPVersion: "0.1", }); err != nil { t.Fatal(err) } result, err := InitAIUsage(root, AIInitOptions{}) if err != nil { t.Fatal(err) } if !sameStrings(result.Created, []string{"README.md", "AI_USAGE.md"}) { t.Fatalf("created = %v", result.Created) } if _, err := os.Stat(filepath.Join(root, "README.md")); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(root, "AI_USAGE.md")); err != nil { t.Fatal(err) } } func TestInitAIUsageOptionalAdapters(t *testing.T) { root := t.TempDir() if _, err := InitProject(root, InitOptions{}); err != nil { t.Fatal(err) } if _, err := InitAIUsage(root, AIInitOptions{ Agents: true, Skill: "generic", }); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(root, "AGENTS.md")); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(root, "skills", "gsp", "SKILL.md")); err != nil { t.Fatal(err) } } func TestInitAIUsageRequiresManifest(t *testing.T) { root := t.TempDir() if _, err := InitAIUsage(root, AIInitOptions{}); err == nil { t.Fatal("expected missing manifest error") } } func TestValidateMessage(t *testing.T) { root := t.TempDir() if _, err := InitProject(root, InitOptions{ Entry: "page.sample.main", }); err != nil { t.Fatal(err) } writeTestFile(t, root, "message.gspmsg", `gspMessageVersion: 0.1 id: msg.sample from: planner to: implementer intent: implement entry: page.sample.main stage: implement requires: - page.sample.main contextPack: mode: implement depth: -1 `) project, err := LoadProject(root) if err != nil { t.Fatal(err) } message, err := ReadMessage(project.Root, "message.gspmsg") if err != nil { t.Fatal(err) } report := project.ValidateMessage(message) if !report.OK { t.Fatalf("expected valid message, got %+v", report.Errors) } } func writeTestFile(t *testing.T, root, name, content string) { t.Helper() path := filepath.Join(root, name) if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } } func ids(units []*Unit) []string { out := make([]string, 0, len(units)) for _, unit := range units { out = append(out, unit.ID) } return out } func sameStrings(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }