require_relative "test_helper" class DetectorTest < Minitest::Test include AdrTestHelpers NUMBERED = <<~MD # 0001 — PocketBase backend ## Status Accepted ## Context Need a backend. ## Decision PocketBase. ## Consequences Single binary. ## Alternatives considered Rails — too heavy. MD DATED = <<~MD # JSX conversion pivot **Date:** 2024-12-15 **Status:** Accepted ## Context Conversion stalled. ## Decision Pivot to JSX. MD TOPIC = <<~MD # bin/lint-fix Status: Confirmed Decided: 2026-02-01 ## Decision One lint entry point. ## Why Consistency across repos. MD MONOLITHIC = <<~MD # Decision log ## ADR-001: Use SQLite Status: Accepted ### Context Need storage. ### Decision SQLite. ## ADR-002: Drop SQLite Status: Accepted ### Decision Postgres instead. MD PROSE = <<~MD # Gaps and decisions (LOCKED) Long narrative preamble. Decision 26: Single entry point We route everything through one command. Decision 27: No config file Defaults only. MD UNRECOGNIZED = "# Meeting notes\n\nWe talked about stuff.\n" MULTI_STATUS_NO_HEADINGS = <<~MD # DECISIONS 2026-01-05 — Hosting Status: Confirmed We host on X. 2026-02-10 — Pricing Status: Deferred Revisit in Q3. MD def fixture_project Dir.mktmpdir do |root| decisions = File.join(root, "docs", "decisions") adr = File.join(root, "docs", "adr") FileUtils.mkdir_p(decisions) FileUtils.mkdir_p(adr) File.write(File.join(adr, "0001-pocketbase-backend.md"), NUMBERED) File.write(File.join(adr, "README.md"), "index") File.write(File.join(decisions, "2024-12-15-jsx-pivot.md"), DATED) File.write(File.join(decisions, "bin-lint-fix.md"), TOPIC) File.write(File.join(decisions, "decision-log.md"), MONOLITHIC) File.write(File.join(decisions, "gaps-and-decisions.md"), PROSE) File.write(File.join(decisions, "notes.md"), UNRECOGNIZED) File.write(File.join(root, "DECISIONS.md"), MULTI_STATUS_NO_HEADINGS) yield root, Adr::Detector.new(root: root).units end end def shapes_by_source(units) units.group_by(&:source_path).transform_values { |us| us.map(&:shape).uniq } end def test_classifies_each_surveyed_shape fixture_project do |_root, units| shapes = shapes_by_source(units) assert_equal [:numbered_per_file], shapes["docs/adr/0001-pocketbase-backend.md"] assert_equal [:dated_single_file], shapes["docs/decisions/2024-12-15-jsx-pivot.md"] assert_equal [:topic_single_file], shapes["docs/decisions/bin-lint-fix.md"] assert_equal [:monolithic], shapes["docs/decisions/decision-log.md"] assert_equal [:prose_embedded], shapes["docs/decisions/gaps-and-decisions.md"] assert_equal [:unrecognized], shapes["docs/decisions/notes.md"] end end def test_monolithic_units_are_enumerated_per_heading fixture_project do |_root, units| mono = units.select { |u| u.source_path == "docs/decisions/decision-log.md" } assert_equal 2, mono.size assert_equal ["Use SQLite", "Drop SQLite"], mono.map(&:title) assert_includes mono.first.body, "SQLite." refute_includes mono.first.body, "Postgres" end end def test_prose_embedded_units_are_enumerated fixture_project do |_root, units| prose = units.select { |u| u.source_path == "docs/decisions/gaps-and-decisions.md" } assert_equal ["Single entry point", "No config file"], prose.map(&:title) end end def test_multi_decision_file_without_unit_headings_is_unrecognized fixture_project do |_root, units| assert_equal [:unrecognized], shapes_by_source(units)["DECISIONS.md"] end end def test_index_and_already_migrated_files_are_excluded with_project("0001-a.md" => sample_adr) do |root| File.write(File.join(root, "docs", "adr", "README.md"), "index") assert_empty Adr::Detector.new(root: root).units end end end