176 lines
4.0 KiB
Ruby
176 lines
4.0 KiB
Ruby
|
|
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
|