Files
pad/internal/artifact/decode_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

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))
}
}