mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
7ea552fa9b
* 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
121 lines
3.0 KiB
Go
121 lines
3.0 KiB
Go
package artifact
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRoundTripPlaybook(t *testing.T) {
|
|
want := samplePlaybook()
|
|
data, err := Encode(want)
|
|
if err != nil {
|
|
t.Fatalf("Encode: %v", err)
|
|
}
|
|
got, err := Decode(data)
|
|
if err != nil {
|
|
t.Fatalf("Decode: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("round-trip mismatch:\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRoundTripConvention(t *testing.T) {
|
|
want := sampleConvention()
|
|
data, err := Encode(want)
|
|
if err != nil {
|
|
t.Fatalf("Encode: %v", err)
|
|
}
|
|
got, err := Decode(data)
|
|
if err != nil {
|
|
t.Fatalf("Decode: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("round-trip mismatch:\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDecodeMissingFrontmatter(t *testing.T) {
|
|
cases := []string{
|
|
"no frontmatter here\n",
|
|
"---\nonly opening fence\n",
|
|
"---\n---\n\nbody\n", // empty frontmatter
|
|
}
|
|
for _, in := range cases {
|
|
_, err := Decode([]byte(in))
|
|
if !errors.Is(err, ErrMalformed) {
|
|
t.Errorf("input %q: got %v, want ErrMalformed", in, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodeUnsupportedVersion(t *testing.T) {
|
|
in := "---\npad_artifact: playbook\nformat_version: 2\ntitle: x\nprovenance:\n format_version: 1\n---\n\nbody\n"
|
|
_, err := Decode([]byte(in))
|
|
if !errors.Is(err, ErrUnsupportedVersion) {
|
|
t.Errorf("got %v, want ErrUnsupportedVersion", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeUnknownKind(t *testing.T) {
|
|
in := "---\npad_artifact: nonsense\nformat_version: 1\ntitle: x\n---\n\nbody\n"
|
|
_, err := Decode([]byte(in))
|
|
if !errors.Is(err, ErrUnknownKind) {
|
|
t.Errorf("got %v, want ErrUnknownKind", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeBodyPreserved(t *testing.T) {
|
|
body := "Line one\n\nLine three\n indented\n"
|
|
a := sampleConvention()
|
|
a.Body = body
|
|
data, err := Encode(a)
|
|
if err != nil {
|
|
t.Fatalf("Encode: %v", err)
|
|
}
|
|
got, err := Decode(data)
|
|
if err != nil {
|
|
t.Fatalf("Decode: %v", err)
|
|
}
|
|
if got.Body != body {
|
|
t.Errorf("body not preserved:\ngot: %q\nwant: %q", got.Body, body)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCRLFTolerant(t *testing.T) {
|
|
// An artifact authored/transported on Windows uses CRLF. Decoding it must
|
|
// yield the same result as the LF form: fences detected, body LF-normalized.
|
|
a := sampleConvention()
|
|
a.Body = "Line one\n\nLine three\n"
|
|
lf, err := Encode(a)
|
|
if err != nil {
|
|
t.Fatalf("Encode: %v", err)
|
|
}
|
|
crlf := strings.ReplaceAll(string(lf), "\n", "\r\n")
|
|
got, err := Decode([]byte(crlf))
|
|
if err != nil {
|
|
t.Fatalf("Decode(CRLF): %v", err)
|
|
}
|
|
if got.Kind != a.Kind || got.Title != a.Title {
|
|
t.Errorf("CRLF decode mismatch: got kind=%q title=%q", got.Kind, got.Title)
|
|
}
|
|
if got.Body != a.Body {
|
|
t.Errorf("CRLF body not LF-normalized:\ngot: %q\nwant: %q", got.Body, a.Body)
|
|
}
|
|
}
|
|
|
|
func TestFieldKeysForKind(t *testing.T) {
|
|
if _, err := FieldKeysForKind("nonsense"); !errors.Is(err, ErrUnknownKind) {
|
|
t.Errorf("got %v, want ErrUnknownKind", err)
|
|
}
|
|
pb, err := FieldKeysForKind(KindPlaybook)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(pb) != 5 {
|
|
t.Errorf("playbook keys: got %d, want 5", len(pb))
|
|
}
|
|
}
|