cc-os/plugins/os-adr/tests/migrator_test.rb

209 lines
6.6 KiB
Ruby
Raw Normal View History

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