0b93a985d6
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>
105 lines
2.7 KiB
Go
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())
|
|
}
|
|
}
|
|
}
|