Add explicit rollback bootstrap and maintenance model

This commit is contained in:
2026-07-20 00:30:30 -07:00
parent 328fa39c55
commit e0c9e0db94
6 changed files with 486 additions and 11 deletions

View File

@@ -122,7 +122,9 @@ durable certification documents and Git history are the archive. This keeps a
fast-moving upstream from making every future run retest every old release. fast-moving upstream from making every future run retest every old release.
See [docs/certification.md](docs/certification.md) for promotion, canary, and See [docs/certification.md](docs/certification.md) for promotion, canary, and
rollback evidence requirements. rollback evidence requirements, and [docs/maintenance.md](docs/maintenance.md)
for consumer ownership, release order, runner boundaries, and expected failure
modes.
## License ## License

View File

@@ -155,14 +155,31 @@ have real passing evidence can the manifest honestly contain certified
`current` and `previous` tuples. Until then, `promotion_readiness` remains `current` and `previous` tuples. Until then, `promotion_readiness` remains
blocked and the candidate PR must not be treated as a deploy authorization. blocked and the candidate PR must not be treated as a deploy authorization.
The schema-v1 promotion command deliberately cannot perform the first degraded Use the explicit degraded-bootstrap transition for the first passing rollback
bootstrap transition when `current` is a known-failing baseline. Do not bypass tuple. It requires passing evidence bound to the complete candidate fingerprint
that guard by hand-editing the manifest or relabeling alpha2 evidence. A and a literal acknowledgement; it preserves the failed baseline as uncertified
separate reviewed state-machine change must first add an explicit `emergency_provenance`, leaves `previous` null, and records the temporary
`bootstrap-current-only` state that preserves the failed baseline as `bootstrap-current-only` state:
uncertified emergency provenance and leaves `previous` null. Itemized rollback
certification is complete only after a later, materially distinct passing tuple ```sh
can move the first certified current into `previous`. ruby scripts/promote_runtime_tuple.rb bootstrap-current \
--consumer travelwolf \
--consumer-commit FULL_40_CHARACTER_ROLLBACK_COMMIT \
--status pass \
--certified-at 2026-07-20T12:00:00Z \
--evidence evidence/travelwolf-rollback.json \
--acknowledge-degraded-rollback \
accept-degraded-rollback-with-failed-emergency-provenance \
--dry-run
```
Review the dry run, repeat without `--dry-run`, and then treat that passing
tuple as the base for a normal promotion. Normal promotion moves it into
`previous`, removes the degraded rollback marker, and keeps the original failed
baseline only as historical emergency provenance. The repository becomes
`certified` only when every consumer has exact passing `current` and `previous`
tuples. Never use bootstrap to replace an existing previous tuple or to certify
a baseline already known to fail.
## Rollback ## Rollback

117
docs/maintenance.md Normal file
View File

@@ -0,0 +1,117 @@
# Maintaining OpenCode consumers
OpenCode compatibility is an executable tuple, not a gem version. The tuple is
the exact client commit, Rails adapter commit where used, OpenCode image digest,
consumer commit, compatibility profile, and passing evidence. A change to any
coordinate creates a new candidate.
## Dependency ownership
The adjacent projects intentionally do not all use the same gems:
| Consumer | OpenCode boundary | Owned dependency |
| --- | --- | --- |
| Ajent Rails | Ruby REST/SSE plus persisted Rails turns | `opencode-ruby` and `opencode-rails` |
| Travelwolf | Ruby REST/SSE plus persisted Rails turns and Sprite lifecycle | `opencode-ruby` and `opencode-rails` |
| Mushu | Ruby REST/SSE with application-owned conversation, claim, recovery, Telegram scope, and idempotency | `opencode-ruby` only |
| Greenroom | Direct voice-stream worker | No Ruby adapter gem |
| Leela | Custom strict-v2 server, security, and toolchain lane | No Ruby adapter gem |
| opencode-ajent | Native CLI and plugin hook/event lane | No Ruby adapter gem |
| inference | Provider configuration, hooks, routing, and migration lane | No Ruby adapter gem |
| Context Kit | MCP and OpenCode configuration producer | No Ruby adapter gem |
Do not add `opencode-rails` to an application that owns different persistence
semantics, and do not route plugin, provider, voice, or strict-v2 behavior
through the Ruby REST/SSE adapter merely to make versions look uniform. Share
fixtures, provenance, and promotion policy across those lanes instead.
Generated release snapshots, detached operational copies, editor locks, caches,
and linked task worktrees are evidence or tooling. They are not additional
consumers and must not be bulk-upgraded.
## Supported window
Support the exact current and previous certified runtime tuples. Do not claim
compatibility with arbitrary future OpenCode versions. Additive fields and
unknown events should remain tolerant, while terminal text, request count,
ownership, persistence, and cleanup invariants remain strict.
The Ruby/SSE profile covers the endpoints and events the client actually uses,
including session creation and deletion, asynchronous prompts, event
subscription, status, terminal idle/status events, part deltas and updates,
authoritative assistant messages, questions, and permissions. A passing shared
profile does not certify Rails persistence, voice streaming, plugin hooks,
provider migrations, or generated MCP configuration.
## Release and promotion order
1. The watcher records a new upstream release tag and resolved OCI digest in a
PR. It never merges or deploys.
2. Update fixtures for any observed protocol change before changing the client.
3. Build `opencode-ruby` and `opencode-rails` as one release train. Rails must
resolve the exact Ruby version and commit being tested.
4. Run the shared fixture corpus, Ruby 3.2 through 4.0 lockstep matrix, and every
exact public image still used by a current, previous, or candidate tuple.
5. Run only the application-owned profiles for each consumer, in its own PR and
isolated environment. Health checks alone do not certify a tuple.
6. Promote and deploy one consumer at a time. Record the exact production
commit, image/base digest, loaded client commits, and live result.
7. Commit reviewed evidence and move the old passing current tuple to previous.
8. Publish annotated gem tags only after the exact commit candidate is green and
the trusted publisher is configured. Publication never implies deployment.
Commit pins are valid for an unpublished candidate, but the durable published
state must record the annotated tag object and peeled commit. Tags or `latest`
may be kept as human-readable provenance only; execution coordinates use full
Git commits and `image@sha256:...` references.
## Custom images
Record each provenance layer separately:
- consumer commit;
- exact output registry digest or Docker image ID;
- exact base image digest;
- custom OpenCode source commit, when the base is a fork;
- build-source commit when it differs from the deployment commit.
The tuple fingerprint binds these values. On the next rebuild of an older
unlabelled private image, add OCI labels for the custom OpenCode source, reported
version, consumer build revision, and base digest, then make preflight compare
the labels. Do not relabel an already certified image: that changes its digest.
## Runner and forge contract
Runner upgrades are a separate compatibility surface from OpenCode upgrades.
Workflows must install their required Ruby/toolchain explicitly, declare Bash
for scripts that use Bash syntax, and avoid relying on ambient runner packages.
GitHub and Gitea do not implement every Actions feature identically. Keep the
same tests on both forges, but use forge-specific execution where necessary:
GitHub retains review artifacts and parallel dynamic image jobs; Gitea runs the
same manifest image set sequentially and makes no artifact-retention claim.
Neither path may mutate a runner or deploy a consumer.
A runner-only workflow repair needs exact-head CI, not an application canary.
A client, runtime image, event, persistence, or toolchain change needs the
profile and consumer canaries described above.
## Expected breakages
- Prompt submission before event subscription can miss a terminal event and
hang a turn.
- Changes to terminal or message-part events can duplicate or lose final text.
- Usage events can undercount multi-step requests if totals are overwritten.
- Reconnect logic can replay a prompt and create duplicate model requests.
- Asset builds can fail when runtime configuration is evaluated without the
image variables available only at deploy time.
- A workflow can pass locally but fail under `sh` when it uses Bash arrays or
`mapfile`.
- Plugin hook names, provider schema, config/MCP schema, or CLI flags can break
direct consumers even when the Ruby profile stays green.
- A custom fork can silently lose its required ordering or permission patch if
only an opaque output digest is retained.
When one of these changes, update the owning profile and consumer evidence. Do
not weaken a strict invariant to make a new upstream release pass.

View File

@@ -15,6 +15,8 @@ module OpenCodeCompat
IMMUTABLE_IMAGE = /\A[^@\s]+@sha256:[0-9a-f]{64}\z/ IMMUTABLE_IMAGE = /\A[^@\s]+@sha256:[0-9a-f]{64}\z/
UTC_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\z/ UTC_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\z/
CERTIFICATION_STATUS = "pass" CERTIFICATION_STATUS = "pass"
DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT =
"accept-degraded-rollback-with-failed-emergency-provenance"
NON_CERTIFIABLE_TUPLE_STATUSES = %w[observed-production-contract-failed].freeze NON_CERTIFIABLE_TUPLE_STATUSES = %w[observed-production-contract-failed].freeze
TUPLE_METADATA_KEYS = %w[ TUPLE_METADATA_KEYS = %w[
certification certification
@@ -82,6 +84,37 @@ module OpenCodeCompat
end end
end end
def bootstrap_current(consumer:, consumer_commit:, certification:, acknowledgement:, dry_run: false)
validate_full_commit!(consumer_commit, "consumer commit")
unless acknowledgement == DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT
raise PromotionError,
"bootstrap requires explicit acknowledgement #{DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT.inspect}"
end
if dry_run
manifest = read_manifest
return bootstrap_current_manifest(
manifest,
consumer: consumer,
consumer_commit: consumer_commit,
certification: certification,
acknowledgement: acknowledgement
)
end
with_current_manifest_lock do |manifest|
bootstrapped = bootstrap_current_manifest(
manifest,
consumer: consumer,
consumer_commit: consumer_commit,
certification: certification,
acknowledgement: acknowledgement
)
atomic_write(bootstrapped)
bootstrapped
end
end
private private
def read_manifest def read_manifest
@@ -124,7 +157,55 @@ module OpenCodeCompat
consumer_entry["previous"] = certified_previous consumer_entry["previous"] = certified_previous
consumer_entry["current"] = certified_candidate consumer_entry["current"] = certified_candidate
consumer_entry["candidate"] = nil consumer_entry["candidate"] = nil
manifest["migration_state"] = all_consumers_certified?(manifest) ? "certified" : "candidate" consumer_entry.delete("rollback_state")
refresh_promotion_state!(manifest)
manifest
end
def bootstrap_current_manifest(manifest, consumer:, consumer_commit:, certification:, acknowledgement:)
consumer_entry = fetch_consumer!(manifest, consumer)
profile = fetch_profile!(consumer_entry, consumer)
candidate = prepare_candidate!(consumer_entry, consumer_commit)
current = consumer_entry.fetch("current") do
raise PromotionError, "#{consumer} has no current tuple to retain as emergency provenance"
end
validate_tuple!(current, "#{consumer} current")
unless NON_CERTIFIABLE_TUPLE_STATUSES.include?(current["status"])
raise PromotionError, "degraded bootstrap is only for a current tuple known to fail the contract"
end
unless consumer_entry["previous"].nil?
raise PromotionError, "degraded bootstrap cannot replace an existing previous tuple"
end
if consumer_entry.key?("emergency_provenance") || consumer_entry.key?("rollback_state")
raise PromotionError, "degraded bootstrap has already been recorded for this consumer"
end
candidate_fingerprint = tuple_fingerprint(candidate, consumer: consumer, profile: profile)
current_fingerprint = tuple_fingerprint(current, consumer: consumer, profile: profile)
if candidate_fingerprint == current_fingerprint
raise PromotionError, "#{consumer} candidate is identical to its failed current tuple"
end
certified_candidate = certify_tuple!(
candidate,
consumer: consumer,
profile: profile,
supplied: certification,
expected_fingerprint: candidate_fingerprint,
label: "bootstrap candidate"
)
consumer_entry["emergency_provenance"] = deep_copy(current)
consumer_entry["current"] = certified_candidate
consumer_entry["candidate"] = nil
consumer_entry["previous"] = nil
consumer_entry["rollback_state"] = {
"status" => "degraded-no-certified-previous",
"acknowledgement" => acknowledgement,
"recorded_at" => certification.fetch("certified_at"),
"emergency_provenance_status" => current.fetch("status")
}
refresh_promotion_state!(manifest)
manifest manifest
end end
@@ -455,6 +536,61 @@ module OpenCodeCompat
end end
end end
def all_consumers_bootstrapped?(manifest)
manifest.fetch("consumers").all? do |consumer, entry|
next false unless entry["candidate"].nil? && entry["previous"].nil?
next false unless entry.dig("rollback_state", "status") == "degraded-no-certified-previous"
next false unless NON_CERTIFIABLE_TUPLE_STATUSES.include?(entry.dig("emergency_provenance", "status"))
profile = fetch_profile!(entry, consumer)
tuple = entry["current"]
next false unless tuple.is_a?(Hash) && tuple["status"] == "certified"
validate_tuple!(tuple, "#{consumer} current")
validate_recorded_certification!(
tuple,
consumer: consumer,
profile: profile,
expected_fingerprint: tuple_fingerprint(tuple, consumer: consumer, profile: profile),
label: "#{consumer} current"
)
true
rescue PromotionError
false
end
end
def migration_state_for(manifest)
return "certified" if all_consumers_certified?(manifest)
return "bootstrap-current-only" if all_consumers_bootstrapped?(manifest)
"candidate"
end
def refresh_promotion_state!(manifest)
manifest["migration_state"] = migration_state_for(manifest)
manifest["promotion_readiness"] = case manifest.fetch("migration_state")
when "certified"
{
"status" => "certified",
"reason" => "Every consumer has exact current and previous passing tuples.",
"required_action" => "Promote only a newly certified, materially changed tuple."
}
when "bootstrap-current-only"
{
"status" => "bootstrap-current-only",
"reason" => "Every current tuple is certified, but no independently passing previous tuple exists yet.",
"required_action" => "Certify the next meaningful release so normal promotion retains the current tuple as previous."
}
else
{
"status" => "candidate",
"reason" => "At least one consumer transition remains incomplete.",
"required_action" => "Finish exact tuple certification without treating failed emergency provenance as rollback evidence."
}
end
end
def with_current_manifest_lock def with_current_manifest_lock
loop do loop do
retry_with_new_inode = false retry_with_new_inode = false

View File

@@ -20,6 +20,10 @@ parser = OptionParser.new do |opts|
--status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\ --status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\
[--previous-status pass --previous-certified-at TIMESTAMP \\ [--previous-status pass --previous-certified-at TIMESTAMP \\
--previous-evidence evidence/FILE.json] [--dry-run] --previous-evidence evidence/FILE.json] [--dry-run]
ruby scripts/promote_runtime_tuple.rb bootstrap-current --consumer NAME --consumer-commit SHA \\
--status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\
--acknowledge-degraded-rollback \\
accept-degraded-rollback-with-failed-emergency-provenance [--dry-run]
USAGE USAGE
opts.on("--consumer NAME") { |value| options["consumer"] = value } opts.on("--consumer NAME") { |value| options["consumer"] = value }
@@ -30,6 +34,9 @@ parser = OptionParser.new do |opts|
opts.on("--previous-status STATUS") { |value| options["previous_status"] = value } opts.on("--previous-status STATUS") { |value| options["previous_status"] = value }
opts.on("--previous-certified-at TIMESTAMP") { |value| options["previous_certified_at"] = value } opts.on("--previous-certified-at TIMESTAMP") { |value| options["previous_certified_at"] = value }
opts.on("--previous-evidence PATH") { |value| options["previous_evidence"] << value } opts.on("--previous-evidence PATH") { |value| options["previous_evidence"] << value }
opts.on("--acknowledge-degraded-rollback VALUE") do |value|
options["degraded_rollback_acknowledgement"] = value
end
opts.on("--dry-run") { options["dry_run"] = true } opts.on("--dry-run") { options["dry_run"] = true }
end end
@@ -94,8 +101,42 @@ begin
"migration_state" => promoted.fetch("migration_state") "migration_state" => promoted.fetch("migration_state")
) )
end end
when "bootstrap-current"
required!(
options,
"consumer",
"consumer_commit",
"status",
"certified_at",
"evidence",
"degraded_rollback_acknowledgement"
)
bootstrapped = promoter.bootstrap_current(
consumer: options.fetch("consumer"),
consumer_commit: options.fetch("consumer_commit"),
certification: {
"status" => options.fetch("status"),
"certified_at" => options.fetch("certified_at"),
"evidence" => options.fetch("evidence")
},
acknowledgement: options.fetch("degraded_rollback_acknowledgement"),
dry_run: options.fetch("dry_run", false)
)
if options.fetch("dry_run", false)
puts JSON.pretty_generate(bootstrapped)
else
consumer = bootstrapped.fetch("consumers").fetch(options.fetch("consumer"))
puts JSON.pretty_generate(
"consumer" => options.fetch("consumer"),
"current_consumer_commit" => consumer.dig("current", "consumer_commit"),
"current_tuple_sha256" => consumer.dig("current", "certification", "tuple_sha256"),
"previous" => consumer.fetch("previous"),
"rollback_state" => consumer.fetch("rollback_state"),
"migration_state" => bootstrapped.fetch("migration_state")
)
end
else else
raise OptionParser::InvalidArgument, "command must be fingerprint or promote" raise OptionParser::InvalidArgument, "command must be fingerprint, promote, or bootstrap-current"
end end
rescue OpenCodeCompat::PromotionError, OptionParser::ParseError => e rescue OpenCodeCompat::PromotionError, OptionParser::ParseError => e
warn "error: #{e.message}" warn "error: #{e.message}"

View File

@@ -10,14 +10,19 @@ class RuntimeTuplePromoterTest < Minitest::Test
CONSUMER = "example" CONSUMER = "example"
CURRENT_COMMIT = "1" * 40 CURRENT_COMMIT = "1" * 40
CANDIDATE_COMMIT = "2" * 40 CANDIDATE_COMMIT = "2" * 40
NEXT_COMMIT = "7" * 40
RUBY_CURRENT = "3" * 40 RUBY_CURRENT = "3" * 40
RUBY_CANDIDATE = "4" * 40 RUBY_CANDIDATE = "4" * 40
RUBY_NEXT = "8" * 40
RAILS_CURRENT = "5" * 40 RAILS_CURRENT = "5" * 40
RAILS_CANDIDATE = "6" * 40 RAILS_CANDIDATE = "6" * 40
RAILS_NEXT = "9" * 40
CURRENT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'a' * 64}" CURRENT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'a' * 64}"
CANDIDATE_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'b' * 64}" CANDIDATE_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'b' * 64}"
NEXT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'c' * 64}"
CURRENT_TIME = "2026-07-17T12:00:00Z" CURRENT_TIME = "2026-07-17T12:00:00Z"
CANDIDATE_TIME = "2026-07-18T12:00:00Z" CANDIDATE_TIME = "2026-07-18T12:00:00Z"
NEXT_TIME = "2026-07-19T12:00:00Z"
def setup def setup
@root = Dir.mktmpdir("opencode-compat-promotion") @root = Dir.mktmpdir("opencode-compat-promotion")
@@ -175,6 +180,163 @@ class RuntimeTuplePromoterTest < Minitest::Test
assert_equal before, File.binread(manifest_path) assert_equal before, File.binread(manifest_path)
end end
def test_explicit_degraded_bootstrap_preserves_failed_emergency_provenance
manifest = valid_manifest
manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed"
write_manifest(manifest)
candidate_fingerprint = @promoter.fingerprints(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT
).fetch("candidate_tuple_sha256")
evidence = write_evidence(
"bootstrap-candidate.json",
commit: CANDIDATE_COMMIT,
timestamp: CANDIDATE_TIME,
fingerprint: candidate_fingerprint
)
before = File.binread(manifest_path)
preview = @promoter.bootstrap_current(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, evidence),
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT,
dry_run: true
)
assert_equal before, File.binread(manifest_path)
assert_equal "bootstrap-current-only", preview.fetch("migration_state")
bootstrapped = @promoter.bootstrap_current(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, evidence),
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT
)
consumer = bootstrapped.dig("consumers", CONSUMER)
assert_equal "certified", consumer.dig("current", "status")
assert_equal CANDIDATE_COMMIT, consumer.dig("current", "consumer_commit")
assert_nil consumer["candidate"]
assert_nil consumer["previous"]
assert_equal "observed-production-contract-failed", consumer.dig("emergency_provenance", "status")
assert_nil consumer.dig("emergency_provenance", "certification")
assert_equal "degraded-no-certified-previous", consumer.dig("rollback_state", "status")
assert_equal "bootstrap-current-only", bootstrapped.dig("promotion_readiness", "status")
assert_equal bootstrapped, JSON.parse(File.read(manifest_path))
end
def test_bootstrap_then_normal_promotion_retains_the_first_passing_tuple_as_previous
manifest = valid_manifest
manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed"
write_manifest(manifest)
bootstrap_fingerprint = @promoter.fingerprints(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT
).fetch("candidate_tuple_sha256")
bootstrap_evidence = write_evidence(
"bootstrap-rollback.json",
commit: CANDIDATE_COMMIT,
timestamp: CANDIDATE_TIME,
fingerprint: bootstrap_fingerprint
)
@promoter.bootstrap_current(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, bootstrap_evidence),
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT
)
manifest = JSON.parse(File.read(manifest_path))
manifest.dig("consumers", CONSUMER)["candidate"] = {
"status" => "compatibility-certified",
"certified_at" => NEXT_TIME,
"consumer_commit" => NEXT_COMMIT,
"opencode_ruby" => {"version" => "0.0.1.alpha8", "git_commit" => RUBY_NEXT},
"opencode_rails" => {"version" => "0.0.1.alpha8", "git_commit" => RAILS_NEXT},
"runtime" => {"image" => NEXT_IMAGE, "reported_version" => "1.19.0"}
}
write_manifest(manifest)
next_fingerprint = @promoter.fingerprints(
consumer: CONSUMER,
consumer_commit: NEXT_COMMIT
).fetch("candidate_tuple_sha256")
next_evidence = write_evidence(
"next-current.json",
commit: NEXT_COMMIT,
timestamp: NEXT_TIME,
fingerprint: next_fingerprint
)
promoted = @promoter.promote(
consumer: CONSUMER,
consumer_commit: NEXT_COMMIT,
certification: certification(NEXT_TIME, next_evidence)
)
consumer = promoted.dig("consumers", CONSUMER)
assert_equal NEXT_COMMIT, consumer.dig("current", "consumer_commit")
assert_equal CANDIDATE_COMMIT, consumer.dig("previous", "consumer_commit")
assert_equal "certified", consumer.dig("previous", "status")
assert_equal "observed-production-contract-failed", consumer.dig("emergency_provenance", "status")
refute consumer.key?("rollback_state")
assert_equal "certified", promoted.fetch("migration_state")
assert_equal "certified", promoted.dig("promotion_readiness", "status")
end
def test_degraded_bootstrap_requires_exact_acknowledgement
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.bootstrap_current(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: {},
acknowledgement: "yes",
dry_run: true
)
end
assert_match(/explicit acknowledgement/, error.message)
end
def test_degraded_bootstrap_rejects_a_passing_baseline_or_existing_previous_tuple
candidate, = write_matching_evidence
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.bootstrap_current(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate),
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT,
dry_run: true
)
end
assert_match(/only for a current tuple known to fail/, error.message)
manifest = valid_manifest
manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed"
manifest.dig("consumers", CONSUMER)["previous"] = manifest.dig("consumers", CONSUMER, "current").dup
write_manifest(manifest)
candidate_fingerprint = @promoter.fingerprints(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT
).fetch("candidate_tuple_sha256")
evidence = write_evidence(
"bootstrap-existing-previous.json",
commit: CANDIDATE_COMMIT,
timestamp: CANDIDATE_TIME,
fingerprint: candidate_fingerprint
)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.bootstrap_current(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, evidence),
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT,
dry_run: true
)
end
assert_match(/cannot replace an existing previous/, error.message)
end
def test_rejects_evidence_that_does_not_match_the_complete_tuple def test_rejects_evidence_that_does_not_match_the_complete_tuple
candidate, previous = write_matching_evidence candidate, previous = write_matching_evidence
manifest = JSON.parse(File.read(manifest_path)) manifest = JSON.parse(File.read(manifest_path))