Files
kanban_cpp/backend/frontmatter_edit_test.go
T
agent 0b93a985d6 feat(backend): issues/flows sync layer (issue 0119)
Read dev/issues/*.md and dev/flows/*.md as kanban cards via new
/api/boards/{issues|flows}/cards endpoints. PATCH writes status back
to the frontmatter atomically (tmp + rename), POST .../launch proxies
to agent_runner_api.

- issues_source.go: scan + parse frontmatter (yaml.v3) into IssueCard.
  Skips README/INDEX/AGENT_GUIDE. Malformed YAML yields parse-error
  cards (no crash). Description = first 5 body lines (no full body).
- flows_source.go: same shape, distinct status->column mapping
  (pending/running/done/deferred -> Pending/Running/Done/Deferred).
- frontmatter_edit.go: PatchFrontmatterField — atomic, preserves the
  rest of the file byte-for-byte, inserts key if missing.
- handlers_boards.go: list + patch + launch endpoints, taxonomy 0103
  enforced. Cache 30s in memory, thread-safe (mutex), invalidated on
  PATCH. Launch returns 502 with suggestion when runner is down.
- main.go: SkipPaths += "/api/boards/" so the C++ frontend hits the
  read endpoints without a kanban_web session.

Smoke (FN_REGISTRY_ROOT pointed at the worktree, 87 issues + 9 flows
on disk):
  GET  /api/boards/issues/cards -> 200, 87 cards
  GET  /api/boards/flows/cards  -> 200, 9 cards
  PATCH /api/boards/issues/cards/0119 {status:en-curso} -> 200,
    file mtime changes, frontmatter rewritten, rest preserved
  POST /api/boards/issues/cards/0119/launch -> 502
    agent_runner_unreachable (expected, runner not yet implemented)

Tests: issues_source_test (3 cases incl. malformed + missing status),
flows_source_test (3 cases), frontmatter_edit_test (4 cases incl.
atomic rename + no tmp leftovers). Pre-existing tools_test failure
on TestExecuteTool_MoveCard_BetweenColumns_OpensHistory is unrelated
(CardHistoryResponse type assert, not touched here).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:56:22 +02:00

105 lines
2.7 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestPatchFrontmatterField_UpdateExistingKey(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "0119-x.md")
original := "---\n" +
"id: \"0119\"\n" +
"title: \"Test\"\n" +
"status: pendiente\n" +
"priority: alta\n" +
"tags: [a, b]\n" +
"---\n" +
"\n" +
"# Body heading\n" +
"\n" +
"Some body.\n"
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
if err := PatchFrontmatterField(path, "status", "en-curso"); err != nil {
t.Fatalf("patch: %v", err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
gotStr := string(got)
if !strings.Contains(gotStr, "status: en-curso") {
t.Fatalf("expected status: en-curso, got:\n%s", gotStr)
}
// Preserve everything else.
for _, line := range []string{
`id: "0119"`,
`title: "Test"`,
`priority: alta`,
`tags: [a, b]`,
`# Body heading`,
`Some body.`,
} {
if !strings.Contains(gotStr, line) {
t.Fatalf("line %q lost after patch, got:\n%s", line, gotStr)
}
}
// Ensure original status line is gone (no duplicate).
if strings.Count(gotStr, "status:") != 1 {
t.Fatalf("expected exactly one status: line, got %d:\n%s", strings.Count(gotStr, "status:"), gotStr)
}
}
func TestPatchFrontmatterField_InsertMissingKey(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "0119-x.md")
original := "---\n" +
"id: \"0119\"\n" +
"title: \"Test\"\n" +
"---\n" +
"body\n"
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
if err := PatchFrontmatterField(path, "status", "done"); err != nil {
t.Fatalf("patch: %v", err)
}
got, _ := os.ReadFile(path)
gotStr := string(got)
if !strings.Contains(gotStr, "status: done") {
t.Fatalf("missing inserted status line:\n%s", gotStr)
}
if !strings.Contains(gotStr, "body") {
t.Fatalf("body lost:\n%s", gotStr)
}
}
func TestPatchFrontmatterField_NoFrontmatter(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "plain.md")
_ = os.WriteFile(path, []byte("just a body\n"), 0o644)
err := PatchFrontmatterField(path, "status", "done")
if err == nil {
t.Fatalf("expected error for missing frontmatter")
}
}
func TestPatchFrontmatterField_AtomicNoLeftovers(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "0119.md")
_ = os.WriteFile(path, []byte("---\nid: \"0119\"\nstatus: pendiente\n---\n"), 0o644)
if err := PatchFrontmatterField(path, "status", "en-curso"); err != nil {
t.Fatalf("patch: %v", err)
}
entries, _ := os.ReadDir(dir)
for _, e := range entries {
if strings.HasPrefix(e.Name(), ".fm.") || strings.HasSuffix(e.Name(), ".tmp") {
t.Fatalf("leftover tmp file: %s", e.Name())
}
}
}