Files
pad/internal/artifact/encode_test.go
T
xarmian 7ea552fa9b feat(artifact): MD+frontmatter export/import core for playbooks & conventions (#754)
* feat(artifact): add MD+frontmatter export/import core for playbooks & conventions

Pure-Go internal/artifact package: Encode/Decode a single playbook or
convention item to a portable Markdown + YAML-frontmatter artifact with
deterministic key order and a provenance block. Field-key tables map item
fields to frontmatter per kind; FieldKeysForKind is exported for the
server layer. Round-trip + golden + determinism + error-case tests.

Server-side validation, forgiving coercion, slug-collision handling, and
YAML input limits are deferred to Phase 2 (HTTP boundary).

Implements Phase 1 of PLAN-1867 (TASK-1868, TASK-1869, TASK-1870).

Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ

* fix(artifact): tolerate CRLF line endings on decode

Normalize CRLF to LF at Decode's entry point so artifacts authored or
transported on Windows decode identically (fence detection + body
preservation operate on canonical LF). Addresses Codex P2 round 1.

Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
2026-06-22 11:07:37 -04:00

128 lines
3.1 KiB
Go

package artifact
import (
"bytes"
"flag"
"os"
"path/filepath"
"testing"
)
var update = flag.Bool("update", false, "update golden files")
func samplePlaybook() Artifact {
return Artifact{
Kind: KindPlaybook,
FormatVersion: FormatVersion,
Title: "Ship a change",
Fields: map[string]any{
"status": "active",
"trigger": "on-intent",
"scope": "workspace",
"invocation_slug": "ship",
"arguments": []map[string]any{
{"name": "ref", "type": "ref", "required": true, "description": "the item to ship"},
{"name": "merge-strategy", "type": "enum", "required": false, "default": "squash"},
},
},
Body: "## Steps\n\n1. Run the tests\n2. Open a PR\n",
Provenance: Provenance{
Workspace: "demo",
ExportedAt: "2026-06-22T00:00:00Z",
Author: "xarmian",
FormatVersion: FormatVersion,
},
}
}
func sampleConvention() Artifact {
return Artifact{
Kind: KindConvention,
FormatVersion: FormatVersion,
Title: "Run the test suite before pushing",
Fields: map[string]any{
"status": "active",
"trigger": "on-commit",
"scope": "repo",
"priority": "high",
"role": "engineer",
},
Body: "Always run `make test` before pushing to main.\n",
Provenance: Provenance{
Workspace: "demo",
ExportedAt: "2026-06-22T00:00:00Z",
Author: "xarmian",
FormatVersion: FormatVersion,
},
}
}
func TestEncodeGolden(t *testing.T) {
cases := []struct {
name string
artifact Artifact
golden string
}{
{"playbook", samplePlaybook(), "playbook.golden.md"},
{"convention", sampleConvention(), "convention.golden.md"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Encode(tc.artifact)
if err != nil {
t.Fatalf("Encode: %v", err)
}
path := filepath.Join("testdata", tc.golden)
if *update {
if err := os.WriteFile(path, got, 0o644); err != nil {
t.Fatalf("write golden: %v", err)
}
return
}
want, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read golden (run -update to create): %v", err)
}
if !bytes.Equal(got, want) {
t.Errorf("Encode output mismatch with %s\n--- got ---\n%s\n--- want ---\n%s", tc.golden, got, want)
}
})
}
}
func TestEncodeDeterministic(t *testing.T) {
for _, a := range []Artifact{samplePlaybook(), sampleConvention()} {
first, err := Encode(a)
if err != nil {
t.Fatalf("Encode 1: %v", err)
}
second, err := Encode(a)
if err != nil {
t.Fatalf("Encode 2: %v", err)
}
if !bytes.Equal(first, second) {
t.Errorf("Encode not deterministic for %s:\n%s\nvs\n%s", a.Kind, first, second)
}
}
}
func TestEncodeUnknownKind(t *testing.T) {
_, err := Encode(Artifact{Kind: "nonsense"})
if err == nil {
t.Fatal("expected error for unknown kind")
}
}
func TestEncodeDefaultsVersion(t *testing.T) {
a := samplePlaybook()
a.FormatVersion = 0
a.Provenance.FormatVersion = 0
out, err := Encode(a)
if err != nil {
t.Fatalf("Encode: %v", err)
}
if !bytes.Contains(out, []byte("format_version: 1")) {
t.Errorf("expected default format_version: 1 in output:\n%s", out)
}
}