feat(0130): kanban_cpp v2 — backend Go + 5 registry parser fns + epic/sub-issues
Registry (issue 0130a):
- 5 fns infra: parse_issue_md, write_issue_md, scan_issues_dir,
scan_flows_dir, watch_dir_fsnotify
- 3 tipos: Issue, Flow, FsEvent
- Tests round-trip + scan reales + watcher fsnotify (all PASS)
- Capability group 'kanban' nuevo (docs/capabilities/kanban.md)
Apps:
- apps/kanban_cpp/ (sub-repo) — frontend ImGui: board drag-drop,
flows, filters, detail con CSV editors
- apps/kanban_cpp/backend/ — Go service port 8487: REST + SSE +
fsnotify watcher, parser bidireccional MD<->SQLite cache
Issues:
- dev/issues/0130-kanban-cpp-v2.md (epic)
- 0130a parser, 0130b backend, 0130c frontend
CMakeLists.txt: add_subdirectory apps/kanban_cpp (registrado por
init_cpp_app scaffolder).
End-to-end verde: backend devuelve 189 issues + 9 flows; PATCH a
/api/issues/{id} reescribe .md (solo frontmatter, body intacto);
frontend --self-test exit 0; tests Go infra 5/5 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteIssueMd(t *testing.T) {
|
||||
root := registryRoot()
|
||||
|
||||
t.Run("round-trip parse-write-parse preserva struct", func(t *testing.T) {
|
||||
fixturePath := filepath.Join(root, "functions", "infra", "testdata", "issue_fixture.fixture")
|
||||
|
||||
// Parse original
|
||||
iss1, body1, err := ParseIssueMd(fixturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIssueMd: %v", err)
|
||||
}
|
||||
|
||||
// Write a TempDir
|
||||
tmpPath := filepath.Join(t.TempDir(), "issue_roundtrip.md")
|
||||
if err := WriteIssueMd(tmpPath, iss1, body1); err != nil {
|
||||
t.Fatalf("WriteIssueMd: %v", err)
|
||||
}
|
||||
|
||||
// Parse de nuevo
|
||||
iss2, body2, err := ParseIssueMd(tmpPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIssueMd after write: %v", err)
|
||||
}
|
||||
|
||||
// Comparar campos (ignorar FilePath y MtimeNs que son runtime)
|
||||
if iss1.ID != iss2.ID {
|
||||
t.Errorf("ID: %q != %q", iss1.ID, iss2.ID)
|
||||
}
|
||||
if iss1.Title != iss2.Title {
|
||||
t.Errorf("Title: %q != %q", iss1.Title, iss2.Title)
|
||||
}
|
||||
if iss1.Status != iss2.Status {
|
||||
t.Errorf("Status: %q != %q", iss1.Status, iss2.Status)
|
||||
}
|
||||
if iss1.Flow != iss2.Flow {
|
||||
t.Errorf("Flow: %q != %q", iss1.Flow, iss2.Flow)
|
||||
}
|
||||
if len(iss1.Domain) != len(iss2.Domain) {
|
||||
t.Errorf("Domain len: %d != %d", len(iss1.Domain), len(iss2.Domain))
|
||||
}
|
||||
if len(iss1.Depends) != len(iss2.Depends) {
|
||||
t.Errorf("Depends len: %d != %d", len(iss1.Depends), len(iss2.Depends))
|
||||
}
|
||||
if len(iss1.Tags) != len(iss2.Tags) {
|
||||
t.Errorf("Tags len: %d != %d", len(iss1.Tags), len(iss2.Tags))
|
||||
}
|
||||
|
||||
// El body debe preservarse exactamente
|
||||
if string(body1) != string(body2) {
|
||||
t.Errorf("body mismatch:\ngot: %q\nwant: %q", string(body2), string(body1))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("archivo resultante empieza con ---", func(t *testing.T) {
|
||||
iss := Issue{
|
||||
ID: "0001",
|
||||
Title: "Test issue",
|
||||
Status: "pendiente",
|
||||
}
|
||||
tmpPath := filepath.Join(t.TempDir(), "test.md")
|
||||
if err := WriteIssueMd(tmpPath, iss, []byte("# Body\n")); err != nil {
|
||||
t.Fatalf("WriteIssueMd: %v", err)
|
||||
}
|
||||
data, _ := os.ReadFile(tmpPath)
|
||||
if len(data) < 4 || string(data[:4]) != "---\n" {
|
||||
t.Errorf("file should start with '---\\n', got: %q", string(data[:min(10, len(data))]))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error en path inexistente", func(t *testing.T) {
|
||||
iss := Issue{ID: "0001", Title: "x", Status: "pendiente"}
|
||||
err := WriteIssueMd("/nonexistent/dir/issue.md", iss, []byte("body"))
|
||||
if err == nil {
|
||||
t.Error("expected error writing to nonexistent dir")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user