c468b24d2b
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>
93 lines
2.5 KiB
Go
93 lines
2.5 KiB
Go
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
|
|
}
|