aboutsummaryrefslogtreecommitdiff
path: root/tests/integration/pipeline.bats
diff options
context:
space:
mode:
authorAlexander M Pickering <alex@cogarr.net>2026-07-21 20:19:46 -0500
committerAlexander M Pickering <alex@cogarr.net>2026-07-21 20:19:46 -0500
commit657fb0007f39f07cc0401e0c5d03e25df6234aa4 (patch)
treef73fe23232dc80802f39caed738c38464a60ec6d /tests/integration/pipeline.bats
downloadtrbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.gz
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.bz2
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.zip
Inital commit.
Diffstat (limited to 'tests/integration/pipeline.bats')
-rw-r--r--tests/integration/pipeline.bats842
1 files changed, 842 insertions, 0 deletions
diff --git a/tests/integration/pipeline.bats b/tests/integration/pipeline.bats
new file mode 100644
index 0000000..2f43304
--- /dev/null
+++ b/tests/integration/pipeline.bats
@@ -0,0 +1,842 @@
+#!/usr/bin/env bats
+# tests/integration/pipeline.bats
+# End-to-end tests for trbldoc.sh.
+#
+# External markdown rendering is stubbed via helpers/common.bash.
+# Tests inspect both .trblcache/namespace intermediate output and the assembled
+# .trblcache/built pages produced by trbldoc itself.
+#
+# Tests marked `skip` document known bugs from todo.md. Remove `skip` after
+# the corresponding bug is fixed.
+
+load "../helpers/common.bash"
+
+FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
+# REPO_ROOT is exported by run_tests.sh; fall back to relative path when bats
+# is invoked directly on this file.
+_REPO_ROOT="${REPO_ROOT:-$(cd "$BATS_TEST_DIRNAME/../.." && pwd)}"
+TRBLDOC="$_REPO_ROOT/trbldoc.sh"
+
+setup() {
+ setup_workspace
+ CACHE=".trblcache"
+ NS="$CACHE/namespace"
+}
+
+teardown() {
+ teardown_workspace
+}
+
+# ── helpers ─────────────────────────────────────────────────────────────────
+
+# Copy a fixture file into a temporary source tree and run the pipeline.
+# Usage: run_pipeline SRC_DIR [additional source dirs...]
+run_pipeline() {
+ bash "$TRBLDOC" "$@"
+}
+
+# Copy a fixture into a local source dir for a clean run.
+stage_fixture() {
+ local src="$1" # path to fixture file or dir
+ local dest="$2" # destination relative path in tmp workspace
+ mkdir -p "$(dirname "$dest")"
+ cp -r "$src" "$dest"
+}
+
+install_busybox_tool_wrapper() {
+ local tool="$1"
+ local busybox_bin="$2"
+ cat > "$STUB_BIN/$tool" <<EOF
+#!/bin/sh
+if [ -n "\${TRBLDOC_TEST_TOOL_LOG:-}" ]; then
+ printf '%s\n' "$tool" >> "\$TRBLDOC_TEST_TOOL_LOG"
+fi
+exec "$busybox_bin" $tool "\$@"
+EOF
+ chmod +x "$STUB_BIN/$tool"
+}
+
+# ── cache structure ──────────────────────────────────────────────────────────
+
+@test "pipeline: .trblcache directory is created on run" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+run_pipeline -s src
+ assert_dir_exists ".trblcache"
+}
+
+@test "pipeline: namespace subdirectory is created on run" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_dir_exists ".trblcache/namespace"
+}
+
+@test "pipeline: chunks subdirectory is created on run" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_dir_exists ".trblcache/chunks"
+}
+
+@test "pipeline: global.meta is created" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_file_exists "$NS/global.meta"
+}
+
+@test "pipeline: main.layout is created" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_file_exists "$NS/main.layout"
+}
+
+@test "pipeline: main.layout passes HTML validation" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_valid_html "$NS/main.layout"
+}
+
+@test "pipeline: main.layout has well-formed HTML structure" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ local layout="$NS/main.layout"
+ # <html> must appear before <head>
+ local html_line head_line body_line footer_line close_body_line
+ html_line=$(grep -n '<html' "$layout" | head -1 | cut -d: -f1)
+ head_line=$(grep -n '<head' "$layout" | head -1 | cut -d: -f1)
+ body_line=$(grep -n '<body' "$layout" | head -1 | cut -d: -f1)
+ footer_line=$(grep -n '<footer' "$layout" | head -1 | cut -d: -f1)
+ close_body_line=$(grep -n '</body>' "$layout" | head -1 | cut -d: -f1)
+ # <html> before <head>
+ [ "$html_line" -lt "$head_line" ]
+ # exactly one <head>
+ local head_count
+ head_count=$(grep -c '<head>' "$layout")
+ assert_equal "$head_count" "1"
+ # <footer> inside <body>: footer before </body>
+ [ "$footer_line" -lt "$close_body_line" ]
+}
+
+@test "pipeline: nav.partial is created" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_file_exists "$NS/nav.partial"
+}
+
+# ── namespace output ─────────────────────────────────────────────────────────
+
+@test "pipeline: named chunk creates namespace output folder" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_dir_exists "$NS/test/simple"
+}
+
+@test "pipeline: namespace output folder contains index.html" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_file_exists "$NS/test/simple/index.html"
+}
+
+@test "pipeline: built page exists for rendered namespace" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_file_exists "$CACHE/built/test/simple/index.html"
+}
+
+@test "pipeline: built page applies layout tokens and contains rendered body" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$CACHE/built/test/simple/index.html" "<!DOCTYPE html>"
+ assert_contains "$CACHE/built/test/simple/index.html" "<title>test/simple</title>"
+ assert_contains "$CACHE/built/test/simple/index.html" "Hello from a C comment"
+ assert_contains "$CACHE/built/test/simple/index.html" "href='/test/simple'"
+}
+
+@test "pipeline: index.html contains rendered chunk body" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/test/simple/index.html" "Hello from a C comment"
+}
+
+@test "pipeline: runs with BusyBox awk and xargs from PATH" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+
+ if ! command -v busybox >/dev/null 2>&1; then
+ skip "busybox is not installed"
+ fi
+
+ local busybox_bin
+ busybox_bin="$(command -v busybox)"
+ export TRBLDOC_TEST_TOOL_LOG="$TEST_TMPDIR/busybox-tools.log"
+ : > "$TRBLDOC_TEST_TOOL_LOG"
+
+ install_busybox_tool_wrapper awk "$busybox_bin"
+ install_busybox_tool_wrapper xargs "$busybox_bin"
+
+ run bash "$TRBLDOC" -s src 2>&1
+ [ "$status" -eq 0 ]
+
+ assert_file_exists "$NS/test/simple/index.html"
+ assert_contains "$NS/test/simple/index.html" "Hello from a C comment"
+
+ local awk_count xargs_count
+ awk_count=$(grep -c '^awk$' "$TRBLDOC_TEST_TOOL_LOG" || true)
+ xargs_count=$(grep -c '^xargs$' "$TRBLDOC_TEST_TOOL_LOG" || true)
+ [ "$awk_count" -gt 0 ]
+ [ "$xargs_count" -gt 0 ]
+}
+
+@test "pipeline: rendered namespace index.html passes HTML validation" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_valid_html_fragment "$NS/test/simple/index.html"
+}
+
+@test "pipeline: multi-chunk file creates two separate namespace folders" {
+ stage_fixture "$FIXTURES/c/multi_chunk.c" "src/multi_chunk.c"
+ run_pipeline -s src
+ assert_dir_exists "$NS/test/alpha"
+ assert_dir_exists "$NS/test/beta"
+}
+
+@test "pipeline: each namespace folder has its own index.html" {
+ stage_fixture "$FIXTURES/c/multi_chunk.c" "src/multi_chunk.c"
+ run_pipeline -s src
+ assert_file_exists "$NS/test/alpha/index.html"
+ assert_file_exists "$NS/test/beta/index.html"
+}
+
+@test "pipeline: each namespace index.html contains the correct body" {
+ stage_fixture "$FIXTURES/c/multi_chunk.c" "src/multi_chunk.c"
+ run_pipeline -s src
+ assert_contains "$NS/test/alpha/index.html" "First chunk content"
+ assert_contains "$NS/test/beta/index.html" "Second chunk content"
+}
+
+@test "pipeline: two chunks with same @name both appear in the shared index.html" {
+ stage_fixture "$FIXTURES/c/shared_namespace.c" "src/shared_namespace.c"
+ run_pipeline -s src
+ assert_contains "$NS/test/shared/index.html" "Part one"
+ assert_contains "$NS/test/shared/index.html" "Part two"
+}
+
+@test "pipeline: @priority orders chunks before namespace/source ordering" {
+ mkdir -p src
+ cat > src/priority_high.c <<'EOF'
+/* md
+@name test/priority-order
+@priority 100
+Priority high
+*/
+int high() { return 0; }
+EOF
+ cat > src/priority_low.c <<'EOF'
+/* md
+@name test/priority-order
+@priority 1
+Priority low
+*/
+int low() { return 0; }
+EOF
+ cat > src/priority_default.c <<'EOF'
+/* md
+@name test/priority-order
+Priority default
+*/
+int def() { return 0; }
+EOF
+ run_pipeline -s src
+ local out="$NS/test/priority-order/index.html"
+ local line_high line_low line_default first_render second_render
+ line_high=$(grep -n "Priority high" "$out" | head -1 | cut -d: -f1)
+ line_low=$(grep -n "Priority low" "$out" | head -1 | cut -d: -f1)
+ line_default=$(grep -n "Priority default" "$out" | head -1 | cut -d: -f1)
+ [ "$line_high" -lt "$line_low" ]
+ [ "$line_low" -lt "$line_default" ]
+ first_render="$(cat "$out")"
+ run_pipeline -s src
+ second_render="$(cat "$out")"
+ assert_equal "$first_render" "$second_render"
+}
+
+@test "pipeline: Lua file chunk creates correct namespace output" {
+ stage_fixture "$FIXTURES/lua/simple.lua" "src/simple.lua"
+ run_pipeline -s src
+ assert_file_exists "$NS/test/lua-simple/index.html"
+}
+
+@test "pipeline: Python file chunk creates correct namespace output" {
+ stage_fixture "$FIXTURES/py/simple.py" "src/simple.py"
+ run_pipeline -s src
+ assert_file_exists "$NS/test/py-simple/index.html"
+}
+
+@test "pipeline: Python chunk body appears in namespace output" {
+ stage_fixture "$FIXTURES/py/simple.py" "src/simple.py"
+ run_pipeline -s src
+ assert_contains "$NS/test/py-simple/index.html" "Hello from a Python doc comment"
+}
+
+@test "pipeline: Markdown file chunk creates correct namespace output" {
+ stage_fixture "$FIXTURES/md/simple.md" "src/simple.md"
+ run_pipeline -s src
+ assert_file_exists "$NS/test/md-simple/index.html"
+}
+
+@test "pipeline: @exec rst chunk is rendered using TRBLDOC_RST override" {
+ stage_fixture "$FIXTURES/c/rst_exec.c" "src/rst_exec.c"
+ cat > custom_rst.sh <<'EOF'
+#!/usr/bin/env bash
+if [[ $# -gt 0 ]]; then
+ content="$(cat "$1")"
+else
+ content="$(cat)"
+fi
+echo "<section class=\"stub-rst\">$content</section>"
+EOF
+ chmod +x custom_rst.sh
+ TRBLDOC_RST="./custom_rst.sh" run_pipeline -s src
+ assert_file_exists "$NS/test/rst-exec/index.html"
+ assert_contains "$NS/test/rst-exec/index.html" "stub-rst"
+ assert_contains "$NS/test/rst-exec/index.html" "RST Heading"
+}
+
+@test "pipeline: file with no doc comments creates no namespace folders" {
+ stage_fixture "$FIXTURES/c/no_comments.c" "src/no_comments.c"
+ run_pipeline -s src
+ # Only the fixed namespace files (global.meta, nav.partial, etc.) should exist;
+ # no test/* content namespace folder should be present.
+ if [[ -d "$NS/test" ]]; then
+ echo "FAILED: unexpected namespace folder created for file with no doc comments" >&2
+ ls "$NS/test" >&2
+ return 1
+ fi
+}
+
+# ── global.meta ─────────────────────────────────────────────────────────────
+
+@test "pipeline: global.meta contains entry for the processed namespace" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/global.meta" "test/simple"
+}
+
+# ── nav.partial ──────────────────────────────────────────────────────────────
+
+@test "pipeline: nav.partial contains link for processed namespace" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/nav.partial" "test/simple"
+}
+
+@test "pipeline: nav.partial does not contain duplicate entries" {
+ stage_fixture "$FIXTURES/c/shared_namespace.c" "src/shared_namespace.c"
+ run_pipeline -s src
+ # Count occurrences of the shared namespace link; must be exactly 1.
+ local count
+ count=$(grep -c "test/shared" "$NS/nav.partial" || true)
+ assert_equal "$count" "1"
+}
+
+@test "pipeline: nav.partial groups related namespaces as a tree" {
+ mkdir -p doc
+ cat > doc/doc.md <<'EOF'
+# Doc
+EOF
+ cat > doc/index.md <<'EOF'
+# Index
+EOF
+ cat > doc/love_loader.md <<'EOF'
+# Love Loader
+EOF
+ cat > doc/ui_notes.md <<'EOF'
+# UI Notes
+EOF
+ run_pipeline -s doc
+ assert_contains "$NS/nav.partial" "<li>doc/<ul>"
+ assert_contains "$NS/nav.partial" "href='/doc/doc.md'>doc.md</a>"
+ assert_contains "$NS/nav.partial" "href='/doc/index.md'>index.md</a>"
+ assert_contains "$NS/nav.partial" "href='/doc/love_loader.md'>love_loader.md</a>"
+ assert_contains "$NS/nav.partial" "href='/doc/ui_notes.md'>ui_notes.md</a>"
+}
+
+@test "pipeline: dotted @name values are allowed" {
+ mkdir -p src
+ cat > src/dotted.c <<'EOF'
+/* md
+@name ui.element.Element
+Dotted namespace.
+*/
+int dotted() { return 0; }
+EOF
+ run_pipeline -s src
+ assert_dir_exists "$NS/ui.element.Element"
+ assert_contains "$NS/nav.partial" "href='/ui.element.Element'"
+}
+
+@test "pipeline: explicit slash-separated @name builds correct nav tree" {
+ mkdir -p src
+ cat > src/ui_parent.c <<'EOF'
+/* md
+@name ui
+UI root documentation.
+*/
+int ui_root() { return 0; }
+EOF
+ cat > src/ui_element.c <<'EOF'
+/* md
+@name ui/element/Element
+Element details.
+*/
+int ui_element() { return 0; }
+EOF
+ run_pipeline -s src
+ assert_contains "$NS/nav.partial" "href='/ui'>ui</a>/<ul>"
+ assert_contains "$NS/nav.partial" "href='/ui/element/Element'>Element</a>"
+ assert_dir_exists "$NS/ui/element/Element"
+}
+
+# ── inline reference resolution ─────────────────────────────────────────────
+
+@test "pipeline: @lua/<path> tokens are resolved to Markdown links" {
+ stage_fixture "$FIXTURES/c/inline_ref.c" "src/inline_ref.c"
+ run_pipeline -s src
+ # ref_resolvers/lua.sh "Coroutines" returns https://www.lua.org/manual/5.2/manual.html#2.6
+ assert_contains "$NS/test/inline-ref/index.html" "lua.org/manual"
+ assert_not_contains "$NS/test/inline-ref/index.html" "@lua/Coroutines"
+}
+
+@test "pipeline: -R adds a custom resolver prefix" {
+ mkdir -p src
+ cat > src/custom_ref.c <<'EOF'
+/* md
+@name test/custom-ref
+Link: @custom/topic
+*/
+int noop() {}
+EOF
+ cat > custom_resolver.sh <<'EOF'
+#!/usr/bin/env bash
+opt="$1"
+path="$(cat)"
+printf "https://example.test/%s/%s" "$opt" "$path"
+EOF
+ chmod +x custom_resolver.sh
+ run_pipeline -s src -R 'custom=./custom_resolver.sh;modeA'
+ assert_contains "$NS/test/custom-ref/index.html" "https://example.test/modeA/topic"
+ assert_not_contains "$NS/test/custom-ref/index.html" "@custom/topic"
+}
+
+@test "pipeline: -R overrides an existing resolver prefix" {
+ mkdir -p src
+ cat > src/lua_override.c <<'EOF'
+/* md
+@name test/lua-override
+Link: @lua/Coroutines
+*/
+int noop() {}
+EOF
+ cat > lua_override.sh <<'EOF'
+#!/usr/bin/env bash
+path="$(cat)"
+printf "https://override.test/%s" "$path"
+EOF
+ chmod +x lua_override.sh
+ run_pipeline -s src -R 'lua=./lua_override.sh'
+ assert_contains "$NS/test/lua-override/index.html" "https://override.test/Coroutines"
+ assert_not_contains "$NS/test/lua-override/index.html" "lua.org/manual"
+}
+
+@test "pipeline: @exec renderer receives TRBLCACHE environment variable (repro)" {
+ mkdir -p src
+ cat > src/renderer_cache_env.c <<'EOF'
+/* ./require_trblcache.sh
+@name test/renderer-cache-env
+Renderer env test.
+*/
+int noop() { return 0; }
+EOF
+ cat > require_trblcache.sh <<'EOF'
+#!/usr/bin/env bash
+if [[ -z "${TRBLCACHE:-}" ]]; then
+ echo "TRBLCACHE is missing in renderer" >&2
+ exit 23
+fi
+echo "<p>cache=$TRBLCACHE</p>"
+EOF
+ chmod +x require_trblcache.sh
+ run bash "$TRBLDOC" -s src 2>&1
+ [ "$status" -eq 0 ]
+ assert_contains "$NS/test/renderer-cache-env/index.html" "cache=./.trblcache"
+ [[ "$output" != *"TRBLCACHE is missing in renderer"* ]]
+ [[ "$output" != *"renderer failed (exit 23)"* ]]
+}
+
+@test "pipeline: @exec renderer receives FILEPATH environment variable (repro)" {
+ mkdir -p src
+ cat > src/renderer_filepath_env.c <<'EOF'
+/* ./require_filepath.sh
+@name test/renderer-filepath-env
+Renderer FILEPATH env test.
+*/
+int noop() { return 0; }
+EOF
+ cat > require_filepath.sh <<'EOF'
+#!/usr/bin/env bash
+if [[ -z "${FILEPATH:-}" ]]; then
+ echo "FILEPATH is missing in renderer" >&2
+ exit 24
+fi
+if [[ "${FILEPATH}" != "src/renderer_filepath_env.c" ]]; then
+ echo "FILEPATH is incorrect in renderer: $FILEPATH" >&2
+ exit 25
+fi
+echo "<p>filepath=$FILEPATH</p>"
+EOF
+ chmod +x require_filepath.sh
+ run bash "$TRBLDOC" -s src 2>&1
+ [ "$status" -eq 0 ]
+ assert_contains "$NS/test/renderer-filepath-env/index.html" "filepath=src/renderer_filepath_env.c"
+ [[ "$output" != *"FILEPATH is missing in renderer"* ]]
+ [[ "$output" != *"FILEPATH is incorrect in renderer"* ]]
+ [[ "$output" != *"renderer failed (exit 24)"* ]]
+ [[ "$output" != *"renderer failed (exit 25)"* ]]
+}
+
+@test "pipeline: @exec renderer receives LINENUM environment variable (repro)" {
+ mkdir -p src
+ cat > src/renderer_linenum_env.c <<'EOF'
+int prelude = 0;
+
+/* ./require_linenum.sh
+@name test/renderer-linenum-env
+Renderer LINENUM env test.
+*/
+int noop() { return 0; }
+EOF
+ cat > require_linenum.sh <<'EOF'
+#!/usr/bin/env bash
+if [[ -z "${LINENUM:-}" ]]; then
+ echo "LINENUM is missing in renderer" >&2
+ exit 26
+fi
+if [[ "${LINENUM}" != "3" ]]; then
+ echo "LINENUM is incorrect in renderer: $LINENUM" >&2
+ exit 27
+fi
+echo "<p>linenum=$LINENUM</p>"
+EOF
+ chmod +x require_linenum.sh
+ run bash "$TRBLDOC" -s src 2>&1
+ [ "$status" -eq 0 ]
+ assert_contains "$NS/test/renderer-linenum-env/index.html" "linenum=3"
+ [[ "$output" != *"LINENUM is missing in renderer"* ]]
+ [[ "$output" != *"LINENUM is incorrect in renderer"* ]]
+ [[ "$output" != *"renderer failed (exit 26)"* ]]
+ [[ "$output" != *"renderer failed (exit 27)"* ]]
+}
+@test "pipeline: inline @exec function receives TRBLCACHE FILEPATH and LINENUM (repro)" {
+ mkdir -p src
+ cat > src/renderer_inline_function_env.c <<'EOF'
+/* render_inline(){ if [[ -z "${TRBLCACHE:-}" || -z "${FILEPATH:-}" || -z "${LINENUM:-}" ]]; then echo "missing env in inline renderer function" >&2; return 28; fi; printf "<p>cache=%s filepath=%s linenum=%s</p>\n" "$TRBLCACHE" "$FILEPATH" "$LINENUM"; }; render_inline
+@name test/renderer-inline-function-env
+Renderer inline function env test.
+*/
+int noop() { return 0; }
+EOF
+ run bash "$TRBLDOC" -s src 2>&1
+ [ "$status" -eq 0 ]
+ assert_contains "$NS/test/renderer-inline-function-env/index.html" "cache=./.trblcache"
+ assert_contains "$NS/test/renderer-inline-function-env/index.html" "filepath=src/renderer_inline_function_env.c"
+ assert_contains "$NS/test/renderer-inline-function-env/index.html" "linenum=1"
+ [[ "$output" != *"missing env in inline renderer function"* ]]
+ [[ "$output" != *"renderer failed (exit 28)"* ]]
+}
+
+@test "pipeline: @name tokens resolve to internal refs before external resolvers" {
+ # Internal refs: @<name> resolves to the first chunk with @ref <name>
+ mkdir -p src
+ cat > src/target.c <<'EOF'
+/* md
+@name api/target
+@ref my-target
+Target documentation.
+*/
+int target() {}
+EOF
+ cat > src/source.c <<'EOF'
+/* md
+@name api/source
+See also: @my-target for details.
+*/
+int source() {}
+EOF
+ run_pipeline -s src
+ # @my-target should resolve to the internal link from global.meta
+ assert_contains "$NS/api/source/index.html" "href='/api/target#my-target'"
+ assert_not_contains "$NS/api/source/index.html" "@my-target"
+}
+
+# ── template variable substitution ─────────────────────────────────────────
+
+@test "pipeline: {{ src_repo }} is substituted with TRBLDOC_SRC_REPO value" {
+ stage_fixture "$FIXTURES/c/src_repo.c" "src/src_repo.c"
+ TRBLDOC_SRC_REPO="https://example.com/repo" run_pipeline -s src
+ assert_contains "$NS/test/src-repo/index.html" "https://example.com/repo"
+ assert_not_contains "$NS/test/src-repo/index.html" '{{ src_repo }}'
+}
+
+@test "pipeline: {{ ref_string }} is substituted with the resolved reference link" {
+ stage_fixture "$FIXTURES/c/ref_string.c" "src/ref_string.c"
+ run_pipeline -s src
+ # @ref lua/Coroutines is resolved via ref_resolvers/lua.sh; the output
+ # contains the full ref name as link text and the resolved URL.
+ assert_contains "$NS/test/ref-string/index.html" "lua/Coroutines"
+ assert_contains "$NS/test/ref-string/index.html" "lua.org/manual"
+ assert_not_contains "$NS/test/ref-string/index.html" '{{ ref_string }}'
+}
+@test "pipeline: From attribution is linked to source when TRBLDOC_SRC_REPO is set" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ TRBLDOC_SRC_REPO="https://example.com/repo/blob/main" run_pipeline -s src
+ assert_contains "$NS/test/simple/index.html" "From <a href='https://example.com/repo/blob/main/src/simple.c#L1'>src/simple.c:1</a>"
+}
+
+@test "pipeline: {{ toc }} is substituted with nav links after rendering" {
+ stage_fixture "$FIXTURES/c/toc.c" "src/toc.c"
+ run_pipeline -s src
+ # After substitution, the chunk output should contain the nav link for the
+ # namespace itself, and the literal marker should be gone.
+ assert_contains "$NS/test/toc-chunk/index.html" "/test/toc-chunk"
+ assert_not_contains "$NS/test/toc-chunk/index.html" '{{ toc }}'
+}
+
+@test "pipeline: main.layout uses {{ title }} two-bracket format" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/main.layout" '{{ title }}'
+ assert_not_contains "$NS/main.layout" '{{title}}'
+}
+
+@test "pipeline: main.layout uses {{ nav }} format instead of {{> nav}}" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/main.layout" '{{ nav }}'
+ assert_not_contains "$NS/main.layout" '{{> nav}}'
+}
+
+@test "pipeline: main.layout uses {{ yield }} format instead of {{{yield}}}" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/main.layout" '{{ yield }}'
+ assert_not_contains "$NS/main.layout" '{{{yield}}}'
+}
+
+@test "pipeline: main.layout uses {{ breadcrumb }} format" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_contains "$NS/main.layout" '{{ breadcrumb }}'
+ assert_not_contains "$NS/main.layout" '{{breadcrumb}}'
+}
+
+@test "pipeline: old-style {{src_repo}} without spaces is not substituted" {
+ mkdir -p src
+ cat > src/old_fmt.c <<'EOF'
+/* md
+@name test/old-fmt-src-repo
+Old format: {{src_repo}}
+*/
+void f() {}
+EOF
+ TRBLDOC_SRC_REPO="https://example.com" run_pipeline -s src
+ assert_contains "$NS/test/old-fmt-src-repo/index.html" '{{src_repo}}'
+}
+
+@test "pipeline: old-style {{toc}} without spaces is not substituted" {
+ mkdir -p src
+ cat > src/old_fmt_toc.c <<'EOF'
+/* md
+@name test/old-fmt-toc
+Old format: {{toc}}
+*/
+void f() {}
+EOF
+ run_pipeline -s src
+ assert_contains "$NS/test/old-fmt-toc/index.html" '{{toc}}'
+}
+
+# ── known defects (skipped – convert to active tests after bug fixes) ────────
+
+@test "pipeline: chunk without @name uses source file path as namespace" {
+ # A chunk with no @name should use its own @file value (the source file path),
+ # not whatever file was last iterated in the scan loop.
+ mkdir -p src
+ cat > src/anon.c <<'EOF'
+/* md
+Chunk with no @name directive.
+*/
+void anon() {}
+EOF
+ run_pipeline -s src
+ # Expect a namespace folder derived from src/anon.c.
+ assert_dir_exists "$NS/src/anon.c"
+}
+
+@test "pipeline: second run does not duplicate nav entries" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ run_pipeline -s src # second run
+ local count
+ count=$(grep -c "test/simple" "$NS/nav.partial" || true)
+ assert_equal "$count" "1"
+}
+
+# ── logic bug regression tests ────────────────────────────────────────────
+
+@test "pipeline: source folder with spaces in name is discovered via -s" {
+ # Bug: infolders is a space-separated string; folder names with spaces break discovery.
+ mkdir -p "src folder"
+ stage_fixture "$FIXTURES/c/simple.c" "src folder/simple.c"
+ run_pipeline -s "src folder"
+ assert_dir_exists "$NS/test/simple"
+}
+
+@test "pipeline: source filename with spaces is processed correctly" {
+ # Bug: source_files and chunk_files use unquoted iteration which breaks on spaces.
+ # This fixture has no @name directive, so namespace is derived from source path.
+ mkdir -p src
+ cat > 'src/simple file.c' <<'EOF'
+/* md
+Hello from a C comment.
+*/
+int main() { return 0; }
+EOF
+ run_pipeline -s src
+ # Namespace is derived from source file path (no @name means use file path)
+ assert_file_exists "$NS/src/simple file.c/index.html"
+ assert_contains "$NS/src/simple file.c/index.html" "Hello from a C comment"
+}
+
+@test "pipeline: nav entries with ampersands in {{ toc }} are substituted correctly" {
+ # Bug: toc_oneline sed substitution treats & as matched text.
+ mkdir -p src
+ cat > 'src/a.c' <<'EOF'
+/* md
+@name test/a
+Navigation: {{ toc }}
+*/
+int a() {}
+EOF
+ cat > 'src/b.c' <<'EOF'
+/* md
+@name test/b&c
+Chunk with ampersand.
+*/
+int b() {}
+EOF
+ run_pipeline -s src
+ # Verify the nav.partial contains the entries
+ assert_contains "$NS/nav.partial" "test/b&c"
+ # Verify the rendered output preserves this (not garbled by sed)
+ assert_contains "$NS/test/a/index.html" "test/b&c"
+ assert_not_contains "$NS/test/a/index.html" '{{ toc }}'
+}
+
+# ── readme.md → top-level index ─────────────────────────────────────────────
+
+@test "pipeline: readme.md creates root-level index.html" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_file_exists "$NS/index.html"
+}
+
+@test "pipeline: readme.md content appears in root index.html" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_contains "$NS/index.html" "Hello from the readme"
+}
+
+@test "pipeline: README.md (uppercase) creates root-level index.html" {
+ mkdir -p src
+ cat > src/README.md <<'EOF'
+# Overview
+Uppercase README content.
+EOF
+ run_pipeline -s src
+ assert_file_exists "$NS/index.html"
+ assert_contains "$NS/index.html" "Uppercase README content"
+}
+
+@test "pipeline: readme.md with explicit @name uses custom namespace, not root" {
+ mkdir -p src
+ cat > src/readme.md <<'EOF'
+@name custom/readme
+# Custom Name
+Content with custom name.
+EOF
+ run_pipeline -s src
+ assert_file_exists "$NS/custom/readme/index.html"
+ if [[ -f "$NS/index.html" ]]; then
+ echo "ASSERTION FAILED: unexpected $NS/index.html created for readme with @name" >&2
+ return 1
+ fi
+}
+
+@test "pipeline: root index.html from readme.md contains rendered HTML tags" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_contains "$NS/index.html" "<p"
+}
+
+@test "pipeline: root index.html from readme.md passes HTML fragment validation" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_valid_html_fragment "$NS/index.html"
+}
+
+@test "pipeline: nav.partial contains root href when readme.md is present" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_contains "$NS/nav.partial" "href='/'"
+}
+
+@test "pipeline: global.meta contains entry for index when readme.md is present" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_contains "$NS/global.meta" "index:"
+}
+
+@test "pipeline: readme.md alongside other source files produces both root index and other pages" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ assert_file_exists "$NS/index.html"
+ assert_file_exists "$NS/test/simple/index.html"
+ assert_contains "$NS/index.html" "Hello from the readme"
+ assert_contains "$NS/test/simple/index.html" "Hello from a C comment"
+}
+
+@test "pipeline: root index.html source attribution points to readme.md" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ assert_contains "$NS/index.html" "readme.md"
+}
+
+@test "pipeline: readme.md is assembled to the built root, not a subdirectory" {
+ stage_fixture "$FIXTURES/md/readme.md" "src/readme.md"
+ run_pipeline -s src
+ # Must land at the built root, not inside an "index" subdirectory.
+ assert_file_exists ".trblcache/built/index.html"
+ if [[ -f ".trblcache/built/index/index.html" ]]; then
+ echo "ASSERTION FAILED: readme was built to built/index/index.html instead of built/index.html" >&2
+ return 1
+ fi
+}
+
+# ── known defects
+
+@test "pipeline: second run does not duplicate index.html content" {
+ stage_fixture "$FIXTURES/c/simple.c" "src/simple.c"
+ run_pipeline -s src
+ run_pipeline -s src
+ local count
+ count=$(grep -c "Hello from a C comment" "$NS/test/simple/index.html" || true)
+ assert_equal "$count" "1"
+}