370 lines
8.9 KiB
Go
370 lines
8.9 KiB
Go
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.
|
|
links:
|
|
- assets/ui
|
|
- path: https://example.com/lottery
|
|
role: reference
|
|
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.
|
|
links: assets/ui/button.txt
|
|
`)
|
|
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.
|
|
`)
|
|
assets := filepath.Join(root, "assets", "ui")
|
|
if err := os.MkdirAll(assets, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeTestFile(t, assets, "button.txt", `button reference`)
|
|
|
|
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")
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|