b9716a7cd6
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>
271 lines
8.8 KiB
C++
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);
|
|
}
|