Files
GSP/toolkit/internal/gsp/project_test.go

370 lines
8.9 KiB
Go
Raw Normal View History

2026-05-06 18:40:37 +08:00
package gsp
import (
2026-05-06 20:00:50 +08:00
"encoding/json"
2026-05-06 18:40:37 +08:00
"os"
"path/filepath"
2026-05-06 20:00:50 +08:00
"strings"
2026-05-06 18:40:37 +08:00
"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
2026-05-06 20:00:50 +08:00
title: Lottery Page
2026-05-06 18:40:37 +08:00
type: page
resolution: L3
context: Lottery page.
links:
- assets/ui
- path: https://example.com/lottery
role: reference
2026-05-06 18:40:37 +08:00
with:
- id: ui.button.primary
context: Main action.
- feedback.positive
refines: page.lottery.base
`)
writeTestFile(t, design, "button.gsp", `id: ui.button.primary
2026-05-06 18:40:37 +08:00
type: ui
resolution: L3
context: Primary button.
links: assets/ui/button.txt
2026-05-06 18:40:37 +08:00
`)
writeTestFile(t, design, "feedback.gsp", `id: feedback.positive
2026-05-06 18:40:37 +08:00
type: feedback
resolution: L2
context: Positive feedback.
`)
writeTestFile(t, design, "base.gsp", `id: page.lottery.base
2026-05-06 18:40:37 +08:00
resolution: L2
context: Base lottery page.
`)
assets := filepath.Join(root, "assets", "ui")
if err := os.MkdirAll(assets, 0755); err != nil {
t.Fatal(err)
}
writeTestFile(t, assets, "button.txt", `button reference`)
2026-05-06 18:40:37 +08:00
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)
}
2026-05-06 20:00:50 +08:00
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")
}
if pack.Summary.LinkCount != 3 {
t.Fatalf("pack link count = %d", pack.Summary.LinkCount)
}
links := project.Links("page.lottery.main", -1)
if links.Summary.Total != 3 {
t.Fatalf("links total = %d", links.Summary.Total)
}
if links.Summary.ByKind["folder"] != 1 || links.Summary.ByKind["file"] != 1 || links.Summary.ByKind["url"] != 1 {
t.Fatalf("links kind summary = %+v", links.Summary.ByKind)
}
if !strings.Contains(links.Markdown(), "GSP Links") {
t.Fatal("expected links 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")
}
2026-05-06 18:40:37 +08:00
}
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
2026-05-06 18:40:37 +08:00
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
2026-05-06 18:40:37 +08:00
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)
}
}
2026-05-06 19:40:55 +08:00
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)
}
}
2026-05-06 18:40:37 +08:00
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
}