Files
Dan MilneandClaude Fable 5 6dd57e84f1 Version 0.3.0: resolver/merge/cache refactor, signing design notes
- Split the monolith into package/provider/fetch/merge/cache modules with
  the Resolver deciding install/adopt/update/merge/conflict outcomes
- Three-way merges via git merge-file/diff3 against a content-addressed
  merge-base cache; conflicts go to a .picopackage-merge sibling
- Tests for package, provider, resolver, merge, and cache
- notes.md: update UX and signing design — diff-by-default, SSH signature
  identity pinning (TOFU), key changes as a hard stop, exit-code contract
- Add CLAUDE.md; remove the pre-refactor exe/pppkg monolith and scratch files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EiyJC73Mz8xZyCTvCEY8qn
2026-08-13 21:18:19 +10:00

341 lines
13 KiB
Ruby

# frozen_string_literal: true
require "test_helper"
class TestResolver < Minitest::Test
include PicopackageTest
V1 = <<~RUBY
module Poller
INTERVAL = 30
def self.run
fetch
end
end
RUBY
# Upstream carrying its own block is optional, not required — but when it does,
# only some of the block is upstream's to assert. This one is stale, as any
# author who edits without re-running the tool will ship.
PACKAGED = V1 + <<~RUBY
# @PICOPACKAGE_START
# ---
# url: https://example.com/canonical/poller.rb
# filename: upstream_name.rb
# payload_version: 1.4.0
# payload_checksum: sha256:#{"de" * 32}
# licence: MIT
# @PICOPACKAGE_END
RUBY
def resolve(upstream_path, project, cache, force: false, filename: nil)
Picopackage::Fetch.fetch(upstream_path, project, force: force, filename: filename, cache: cache)
end
def test_installs_when_nothing_is_there
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
result = resolve(path, project, cache)
assert_equal :installed, result.state
installed = Picopackage::Package.from_file(File.join(project, "poller.rb"))
refute_predicate installed, :bare?
assert_equal path, installed.url
assert installed.verify_payload
end
end
# A bare upstream file is a valid picopackage source: nobody has to adopt the
# format for their code to be installable. Everything the block would have
# said gets derived from the fetch instead.
def test_installs_from_an_upstream_with_no_metadata_block
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
assert_predicate upstream_package(path), :bare?, "precondition: upstream carries no block"
assert_equal :installed, resolve(path, project, cache).state
installed = Picopackage::Package.from_file(File.join(project, "poller.rb"))
assert installed.verify_payload
assert_equal path, installed.url
end
end
def test_publisher_claims_are_adopted_and_local_records_are_recomputed
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", PACKAGED)
assert_equal :installed, resolve(path, project, cache).state
# Claims: upstream's, and worth more than anything we could infer. The
# declared filename beats the url's basename, so the file lands as the
# author named it rather than as whatever the mirror called it.
local_path = File.join(project, "upstream_name.rb")
assert_path_exists local_path
installed = Picopackage::Package.from_file(local_path)
assert_equal "https://example.com/canonical/poller.rb", installed.url,
"the canonical url should outlive the mirror we fetched from"
assert_equal "1.4.0", installed.payload_version
assert_equal "MIT", installed.metadata.extra["licence"]
# Records: ours, describing this copy on this disk.
assert installed.verify_payload, "a freshly installed file must verify, whatever upstream claimed"
refute_predicate installed, :modified?
refute_predicate installed, :diverged?
assert_equal "upstream_name.rb", installed.filename, "filename records where it actually landed"
end
end
def test_a_stale_upstream_checksum_does_not_break_a_later_merge
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", PACKAGED)
resolve(path, project, cache)
local_path = File.join(project, "upstream_name.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
File.write(path, PACKAGED.sub(" fetch\n", " fetch\n prune\n"))
result = resolve(path, project, cache)
assert_equal :merged, result.state, result.message
merged = File.read(local_path)
assert_includes merged, "INTERVAL = 5"
assert_includes merged, "prune"
assert_equal "MIT", Picopackage::Package.from_file(local_path).metadata.extra["licence"]
end
end
def test_reinstalling_the_same_thing_is_a_no_op
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
before = File.read(File.join(project, "poller.rb"))
result = resolve(path, project, cache)
assert_equal :current, result.state
assert_equal before, File.read(File.join(project, "poller.rb")), "an up-to-date file should not be rewritten"
end
end
# The pleasant case: a file you pasted in by hand months ago gets its
# provenance attached without its content being touched.
def test_adopts_a_bare_local_file_with_identical_content
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
File.write(File.join(project, "poller.rb"), V1)
result = resolve(path, project, cache)
assert_equal :adopted, result.state
adopted = Picopackage::Package.from_file(File.join(project, "poller.rb"))
refute_predicate adopted, :bare?
assert_equal Picopackage::Payload.normalize(V1), adopted.payload
end
end
def test_fast_forwards_an_unmodified_package
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
File.write(path, V1.sub("INTERVAL = 30", "INTERVAL = 60"))
result = resolve(path, project, cache)
assert_equal :updated, result.state
assert_includes File.read(File.join(project, "poller.rb")), "INTERVAL = 60"
end
end
# The headline capability: local edits and upstream edits both survive.
def test_merges_local_edits_with_upstream_changes
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5 # tuned for us"))
File.write(path, V1.sub(" fetch\n", " fetch\n prune\n"))
result = resolve(path, project, cache)
assert_equal :merged, result.state, result.message
merged = File.read(local_path)
assert_includes merged, "INTERVAL = 5 # tuned for us", "local edit should survive"
assert_includes merged, "prune", "upstream change should be applied"
refute_includes merged, "<<<<<<<"
package = Picopackage::Package.from_file(local_path)
assert package.verify_payload, "a merged file should verify against its own recorded checksum"
refute_equal package.payload_checksum, package.base_checksum,
"base_checksum should still point at upstream, not at the merged result"
end
end
# A merged file must remain updatable: the base pointer has to advance to the
# upstream payload, not to the merge result, or the next merge replays old
# upstream changes as conflicts.
def test_a_merged_file_can_be_updated_again
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
File.write(path, V1.sub(" fetch\n", " fetch\n prune\n"))
assert_equal :merged, resolve(path, project, cache).state
File.write(path, V1.sub(" fetch\n", " fetch\n prune\n report\n"))
result = resolve(path, project, cache)
assert_equal :merged, result.state, result.message
merged = File.read(local_path)
assert_includes merged, "INTERVAL = 5"
assert_includes merged, "report"
refute_includes merged, "<<<<<<<"
end
end
# Editing locally while upstream stands still is the common case, and there is
# nothing to merge: upstream is still the ancestor we branched from.
def test_local_edits_with_no_upstream_change_are_left_alone
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
before = File.read(local_path)
result = resolve(path, project, cache)
assert_equal :current, result.state, result.message
assert_equal before, File.read(local_path), "an edited file with no upstream change must not be rewritten"
end
end
# Repopulating the merge base from an unchanged upstream is the whole reason
# that branch still touches the cache.
def test_an_unchanged_upstream_restores_a_lost_merge_base
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
# A fresh machine, or a cleared cache: the ancestor is gone.
empty = Picopackage::Cache.new(root: File.join(Dir.mktmpdir, "empty"))
assert_equal :current, resolve(path, project, empty).state
# Upstream now moves. The base recovered above makes this mergeable.
File.write(path, V1.sub(" fetch\n", " fetch\n prune\n"))
result = resolve(path, project, empty)
assert_equal :merged, result.state, result.message
assert_includes File.read(local_path), "INTERVAL = 5"
assert_includes File.read(local_path), "prune"
end
end
def test_conflicting_edits_leave_the_original_alone
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
before = File.read(local_path)
File.write(path, V1.sub("INTERVAL = 30", "INTERVAL = 90"))
result = resolve(path, project, cache)
assert_equal :conflict, result.state
assert_equal before, File.read(local_path), "the user's file must not be touched"
markers = local_path + Picopackage::Resolver::MERGE_SUFFIX
assert_path_exists markers
assert_includes File.read(markers), "<<<<<<<"
end
end
def test_force_discards_local_changes
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
File.write(path, V1.sub("INTERVAL = 30", "INTERVAL = 90"))
result = resolve(path, project, cache, force: true)
assert_equal :updated, result.state
assert_includes File.read(local_path), "INTERVAL = 90"
refute_includes File.read(local_path), "INTERVAL = 5"
end
end
def test_an_unrelated_bare_file_with_the_same_name_is_refused
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
File.write(File.join(project, "poller.rb"), "# something else entirely\n")
result = resolve(path, project, cache)
assert_equal :conflict, result.state
assert_equal "# something else entirely\n", File.read(File.join(project, "poller.rb"))
end
end
def test_a_modified_file_with_no_cached_ancestor_is_refused
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
File.write(local_path, File.read(local_path).sub("INTERVAL = 30", "INTERVAL = 5"))
File.write(path, V1.sub(" fetch\n", " fetch\n prune\n"))
empty_cache = Picopackage::Cache.new(root: File.join(Dir.mktmpdir, "empty"))
result = resolve(path, project, empty_cache)
assert_equal :conflict, result.state
assert_match(/cache/, result.message)
end
end
# Renaming a package locally is allowed; updating it must not install a second
# copy under upstream's preferred name.
def test_update_writes_back_to_the_local_filename
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
FileUtils.mv(File.join(project, "poller.rb"), File.join(project, "my_poller.rb"))
File.write(path, V1.sub("INTERVAL = 30", "INTERVAL = 60"))
result = resolve(path, project, cache, filename: "my_poller.rb")
assert_equal :updated, result.state
refute_path_exists File.join(project, "poller.rb"), "should not have created a second copy"
assert_includes File.read(File.join(project, "my_poller.rb")), "INTERVAL = 60"
assert_equal "my_poller.rb", Picopackage::Package.from_file(File.join(project, "my_poller.rb")).filename
end
end
def test_timestamps_never_override_the_checksum
with_dirs do |upstream, project, cache|
path = publish(upstream, "poller.rb", V1)
resolve(path, project, cache)
local_path = File.join(project, "poller.rb")
before = File.read(local_path)
# Same bytes, much newer mtime — the old timestamp-driven logic called
# this an update and rewrote the file.
FileUtils.touch(path, mtime: Time.now + 86_400)
result = resolve(path, project, cache)
assert_equal :current, result.state
assert_equal before, File.read(local_path)
end
end
end