chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1 del flow 0008 (kanban_cpp + agent_runner_api + DoD schema). Incluye: - dev/flows/0008-kanban-cpp-and-agent-workflows.md - dev/issues/0112-0119*.md (7 sub-issues) - WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
// Tests for parse_md_frontmatter (cpp/functions/core/parse_md_frontmatter).
|
||||
// Pure function — no ImGui context, no I/O (except the golden-run test which
|
||||
// reads dev/issues/ using FN_TEST_REPO_ROOT defined at compile time).
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/parse_md_frontmatter.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace fn_md;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string str(const YamlValue& v) {
|
||||
if (auto* s = std::get_if<std::string>(&v)) return *s;
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::vector<std::string> lst(const YamlValue& v) {
|
||||
if (auto* l = std::get_if<std::vector<std::string>>(&v)) return *l;
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool has(const Frontmatter& fm, const std::string& key) {
|
||||
return fm.fields.count(key) > 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 1 — No frontmatter
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_no_frontmatter") {
|
||||
const std::string content = "# Just a markdown file\n\nSome body text.\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE_FALSE(fm.has_frontmatter);
|
||||
REQUIRE(fm.body == content);
|
||||
REQUIRE(fm.fields.empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 2 — Simple key:value
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_simple_key_value") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"title: Hello\n"
|
||||
"status: pending\n"
|
||||
"---\n"
|
||||
"Body here.\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("title")) == "Hello");
|
||||
REQUIRE(str(fm.fields.at("status")) == "pending");
|
||||
REQUIRE(fm.body == "Body here.\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 3 — Quoted string (double and single quotes)
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_quoted_strings") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"description: \"foo bar\"\n"
|
||||
"note: 'baz qux'\n"
|
||||
"id: \"0109\"\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("description")) == "foo bar");
|
||||
REQUIRE(str(fm.fields.at("note")) == "baz qux");
|
||||
REQUIRE(str(fm.fields.at("id")) == "0109");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 4 — Inline list
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_inline_list") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"tags: [meta, cpp, imgui]\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
const auto v = lst(fm.fields.at("tags"));
|
||||
REQUIRE(v.size() == 3);
|
||||
REQUIRE(v[0] == "meta");
|
||||
REQUIRE(v[1] == "cpp");
|
||||
REQUIRE(v[2] == "imgui");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 5 — Multiline list
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_multiline_list") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"domain:\n"
|
||||
" - meta\n"
|
||||
" - cpp-stack\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
const auto v = lst(fm.fields.at("domain"));
|
||||
REQUIRE(v.size() == 2);
|
||||
REQUIRE(v[0] == "meta");
|
||||
REQUIRE(v[1] == "cpp-stack");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 6 — Body after frontmatter
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_body_after_frontmatter") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"key: v\n"
|
||||
"---\n"
|
||||
"body text\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("key")) == "v");
|
||||
REQUIRE(fm.body == "body text\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 7 — Empty list
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_empty_inline_list") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"blocks: []\n"
|
||||
"depends: []\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(lst(fm.fields.at("blocks")).empty());
|
||||
REQUIRE(lst(fm.fields.at("depends")).empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 8 — Trailing comments stripped
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_strips_trailing_comment") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"key: value # this is a comment\n"
|
||||
"other: hello # another\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("key")) == "value");
|
||||
REQUIRE(str(fm.fields.at("other")) == "hello");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 9 — Real issue sample (issue 0109)
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_real_issue_0109") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"id: \"0109\"\n"
|
||||
"title: \"App skill_tree: mapa interactivo de issues+flows en anillos concentricos por estado (roadmap)\"\n"
|
||||
"status: in-progress\n"
|
||||
"type: epic\n"
|
||||
"domain:\n"
|
||||
" - meta\n"
|
||||
" - cpp-stack\n"
|
||||
"scope: cross-stack\n"
|
||||
"priority: media\n"
|
||||
"depends: []\n"
|
||||
"blocks: []\n"
|
||||
"related:\n"
|
||||
" - \"0069\"\n"
|
||||
" - \"0085\"\n"
|
||||
"created: 2026-05-17\n"
|
||||
"updated: 2026-05-17\n"
|
||||
"tags:\n"
|
||||
" - skill-tree\n"
|
||||
" - roadmap\n"
|
||||
" - meta\n"
|
||||
" - cpp\n"
|
||||
" - imgui\n"
|
||||
" - gamification\n"
|
||||
"---\n"
|
||||
"\n"
|
||||
"# Body\n";
|
||||
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE_FALSE(fm.fields.empty());
|
||||
|
||||
REQUIRE(str(fm.fields.at("id")) == "0109");
|
||||
REQUIRE(str(fm.fields.at("status")) == "in-progress");
|
||||
|
||||
const auto domain = lst(fm.fields.at("domain"));
|
||||
REQUIRE(domain.size() == 2);
|
||||
REQUIRE(domain[0] == "meta");
|
||||
REQUIRE(domain[1] == "cpp-stack");
|
||||
|
||||
const auto tags = lst(fm.fields.at("tags"));
|
||||
REQUIRE(tags.size() == 6);
|
||||
REQUIRE(tags[0] == "skill-tree");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 10 — Golden run: parse ALL dev/issues/*.md and dev/flows/*.md
|
||||
//
|
||||
// Requires the compile-time definition FN_TEST_REPO_ROOT pointing to the
|
||||
// root of the fn_registry repo (set by CMakeLists.txt via
|
||||
// target_compile_definitions(... PRIVATE FN_TEST_REPO_ROOT="...") ).
|
||||
//
|
||||
// The test reads every .md file it finds, parses it, and verifies:
|
||||
// 1. No crash / no exception.
|
||||
// 2. If the file starts with `---`, has_frontmatter == true.
|
||||
// 3. fields is not empty.
|
||||
// 4. Cumulative parse_errors == 0.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#ifndef FN_TEST_REPO_ROOT
|
||||
#define FN_TEST_REPO_ROOT ""
|
||||
#endif
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
TEST_CASE("parses_real_issues_golden") {
|
||||
const std::string repo_root = FN_TEST_REPO_ROOT;
|
||||
if (repo_root.empty()) {
|
||||
WARN("FN_TEST_REPO_ROOT not set — skipping golden run");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> md_files;
|
||||
for (const auto& dir : {"dev/issues", "dev/flows"}) {
|
||||
const auto p = std::filesystem::path(repo_root) / dir;
|
||||
if (!std::filesystem::is_directory(p)) continue;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(p)) {
|
||||
if (entry.path().extension() == ".md")
|
||||
md_files.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE_FALSE(md_files.empty());
|
||||
|
||||
int parse_errors = 0;
|
||||
for (const auto& path : md_files) {
|
||||
std::ifstream f(path);
|
||||
REQUIRE(f.is_open());
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
const std::string content = ss.str();
|
||||
|
||||
Frontmatter fm;
|
||||
// Must not throw
|
||||
REQUIRE_NOTHROW(fm = parse_md_frontmatter(content));
|
||||
|
||||
if (content.rfind("---", 0) == 0) {
|
||||
// File starts with `---`: expect frontmatter was found
|
||||
INFO("File: " << path.string());
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE_FALSE(fm.fields.empty());
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(parse_errors == 0);
|
||||
}
|
||||
Reference in New Issue
Block a user