Files
fn_registry/cpp/tests/test_parse_md_frontmatter.cpp
egutierrez 7eb7b3d0c8 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>
2026-05-18 18:17:08 +02:00

271 lines
8.8 KiB
C++

// 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);
}