209 lines
6.6 KiB
Ruby
209 lines
6.6 KiB
Ruby
require_relative "test_helper"
|
|
require "digest"
|
|
|
|
class MigratorTest < Minitest::Test
|
|
include AdrTestHelpers
|
|
|
|
FULL_SOURCE = <<~MD
|
|
# 0001 — PocketBase backend
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
Need a backend.
|
|
|
|
## Decision
|
|
|
|
PocketBase.
|
|
|
|
## Consequences
|
|
|
|
Single binary.
|
|
|
|
## Alternatives considered
|
|
|
|
Rails — too heavy.
|
|
MD
|
|
|
|
PARTIAL_SOURCE = <<~MD
|
|
# bin/lint-fix
|
|
|
|
Status: Confirmed
|
|
Decided: 2026-02-01
|
|
|
|
## Context
|
|
|
|
Lint config drifted per repo.
|
|
|
|
## Decision
|
|
|
|
One lint entry point.
|
|
|
|
## Why
|
|
|
|
Consistency across repos.
|
|
MD
|
|
|
|
BARE_SOURCE = <<~MD
|
|
# Something happened
|
|
|
|
Status: Accepted
|
|
|
|
We did a thing, narratively.
|
|
MD
|
|
|
|
def project(sources)
|
|
Dir.mktmpdir do |root|
|
|
decisions = File.join(root, "docs", "decisions")
|
|
FileUtils.mkdir_p(decisions)
|
|
sources.each { |name, body| File.write(File.join(decisions, name), body) }
|
|
repo = Adr::Repository.new(root: root)
|
|
migrator = Adr::Migrator.new(repository: repo, detector: Adr::Detector.new(root: root))
|
|
yield root, repo, migrator
|
|
end
|
|
end
|
|
|
|
def test_sources_stay_byte_identical
|
|
project("0001-full.md" => FULL_SOURCE, "partial.md" => PARTIAL_SOURCE) do |root, _repo, migrator|
|
|
before = Dir.glob(File.join(root, "docs", "decisions", "*"))
|
|
.to_h { |p| [p, Digest::SHA256.file(p).hexdigest] }
|
|
migrator.migrate!
|
|
before.each { |path, sha| assert_equal sha, Digest::SHA256.file(path).hexdigest }
|
|
end
|
|
end
|
|
|
|
def test_unambiguous_fields_fill_mechanically_high_confidence
|
|
project("0001-full.md" => FULL_SOURCE) do |_root, repo, migrator|
|
|
manifest = migrator.migrate!
|
|
entry = repo.entries.first
|
|
record = entry.record
|
|
assert_equal "Accepted", record.status
|
|
assert_equal "PocketBase backend", record.title
|
|
assert_equal "Need a backend.", record.sections["Context"]
|
|
assert_equal "Rails — too heavy.", record.sections["Alternatives rejected"]
|
|
assert_equal "high", record.frontmatter["migration_confidence"]
|
|
assert_equal "docs/decisions/0001-full.md", record.frontmatter["migration_source"]
|
|
assert_empty manifest["migrated"].first["llm_fields"]
|
|
end
|
|
end
|
|
|
|
def test_interpretive_fields_get_pending_markers_and_manifest
|
|
project("partial.md" => PARTIAL_SOURCE) do |_root, repo, migrator|
|
|
manifest = migrator.migrate!
|
|
record = repo.entries.first.record
|
|
assert_equal "Accepted", record.status, "Confirmed maps mechanically to Accepted"
|
|
assert_equal Date.new(2026, 2, 1), record.date
|
|
assert_equal Adr::Migrator::PENDING_MARKER, record.sections["Consequences"]
|
|
assert_equal Adr::Migrator::PENDING_MARKER, record.sections["Alternatives rejected"]
|
|
assert_equal "medium", record.frontmatter["migration_confidence"]
|
|
entry = manifest["migrated"].first
|
|
assert_equal %w[consequences alternatives], entry["llm_fields"]
|
|
assert_includes entry["source_material"], "Consistency across repos."
|
|
end
|
|
end
|
|
|
|
def test_fallback_heading_mapping_caps_confidence_at_medium
|
|
source = <<~MD
|
|
# Topic
|
|
|
|
Status: Accepted
|
|
|
|
## Decision
|
|
|
|
Do the thing.
|
|
|
|
## Why
|
|
|
|
Because reasons.
|
|
|
|
## Consequences
|
|
|
|
Some.
|
|
|
|
## Alternatives rejected
|
|
|
|
None seriously.
|
|
MD
|
|
project("topic.md" => source) do |_root, repo, migrator|
|
|
migrator.migrate!
|
|
record = repo.entries.first.record
|
|
assert_equal "Because reasons.", record.sections["Context"], "Why maps to Context"
|
|
assert_equal "medium", record.frontmatter["migration_confidence"],
|
|
"heuristic heading mapping is flagged"
|
|
end
|
|
end
|
|
|
|
def test_missing_core_fields_get_not_stated_markers_and_low_confidence
|
|
project("bare.md" => BARE_SOURCE) do |_root, repo, migrator|
|
|
migrator.migrate!
|
|
record = repo.entries.first.record
|
|
assert_equal Adr::Migrator::NOT_STATED_MARKER, record.sections["Decision"]
|
|
assert_equal Adr::Migrator::NOT_STATED_MARKER, record.sections["Context"]
|
|
assert_equal "low", record.frontmatter["migration_confidence"]
|
|
end
|
|
end
|
|
|
|
def test_apply_fills_only_touches_pending_fields
|
|
project("partial.md" => PARTIAL_SOURCE, "bare.md" => BARE_SOURCE) do |root, repo, migrator|
|
|
manifest = migrator.migrate!
|
|
pending_file = manifest["migrated"].find { |m| m["source"].include?("partial") }["file"]
|
|
bare_file = manifest["migrated"].find { |m| m["source"].include?("bare") }["file"]
|
|
|
|
migrator.apply_fills!(
|
|
pending_file => { "consequences" => "Fewer lint debates.",
|
|
"alternatives" => "Per-repo configs — rejected, drift." },
|
|
bare_file => { "decision" => "INVENTED", "context" => "INVENTED" }
|
|
)
|
|
|
|
filled = Adr::Record.parse(File.read(File.join(root, pending_file)))
|
|
assert_equal "Fewer lint debates.", filled.sections["Consequences"]
|
|
assert_equal "medium", filled.frontmatter["migration_confidence"], "LLM-filled stays flagged"
|
|
|
|
bare = Adr::Record.parse(File.read(File.join(root, bare_file)))
|
|
assert_equal Adr::Migrator::NOT_STATED_MARKER, bare.sections["Decision"],
|
|
"core fields are never LLM-inventable"
|
|
assert_equal "low", bare.frontmatter["migration_confidence"]
|
|
end
|
|
end
|
|
|
|
def test_report_lists_files_confidence_sources_and_flag_rate
|
|
project("0001-full.md" => FULL_SOURCE, "bare.md" => BARE_SOURCE) do |root, _repo, migrator|
|
|
manifest = migrator.migrate!
|
|
report = File.read(File.join(root, "docs", "adr", "migration-report.md"))
|
|
assert_includes report, "docs/decisions/0001-full.md"
|
|
assert_includes report, "| low |"
|
|
assert_includes report, "| high |"
|
|
assert_includes report, "flag rate: 50.0%"
|
|
assert_equal 0.5, manifest["flag_rate"]
|
|
end
|
|
end
|
|
|
|
def test_unrecognized_content_is_reported_never_converted
|
|
project("notes.md" => "# Notes\n\nchatter\n") do |root, repo, migrator|
|
|
manifest = migrator.migrate!
|
|
assert_empty repo.entries
|
|
assert_equal "docs/decisions/notes.md", manifest["unrecognized"].first["source"]
|
|
assert_includes File.read(File.join(root, "docs", "adr", "migration-report.md")),
|
|
"docs/decisions/notes.md"
|
|
end
|
|
end
|
|
|
|
def test_migration_is_idempotent_across_reruns
|
|
project("0001-full.md" => FULL_SOURCE) do |_root, repo, migrator|
|
|
migrator.migrate!
|
|
migrator.migrate!
|
|
assert_equal 1, repo.entries.size
|
|
end
|
|
end
|
|
|
|
def test_index_is_regenerated_with_migrated_adrs
|
|
project("0001-full.md" => FULL_SOURCE) do |_root, repo, migrator|
|
|
migrator.migrate!
|
|
assert_includes File.read(repo.index_path), "PocketBase backend"
|
|
end
|
|
end
|
|
end
|