Files
copilotkit__copilotkit/showcase/bin/spec/test_promote_p1.rb
Jordan Ritter c03270135e fix(showcase): promote resolves staging tag to GHCR digest before pinning prod
The showcase deploy model is STAGING = mutable :latest tag, PROD = immutable
@sha256: digest (P6 enforces both shapes). SnapshotCommand#build_snapshot
stored the raw serviceInstance.source.image, so for staging svc["image"] was
the :latest TAG. execute_promotion was pinning THAT mutable tag to prod via
serviceInstanceUpdate, defeating the immutable-prod invariant before
pin_and_verify raised on the nil expected_digest.

Fix: add PromoteCommand#resolved_prod_image — returns the staging svc as
@sha256:-pinned (pass-through if already pinned; resolves the tag via the
shared GHCR client otherwise; returns nil if the tag cannot be resolved).
execute_promotion now refuses (P0) rather than pin a mutable tag, and
check_p1_ghcr_digests verifies the resolved digest (it previously SKIPPED
tag-form images entirely, so :latest was never P1-checked).

Also:
- P2 race-check guards latest["meta"] when Railway returns a JSON String
  (deserialized as Ruby String, not Hash) — .dig used to crash with
  NoMethodError. SUCCESS status remains the real gate.
- Remove dead --include-startcommand flag (never read; doubly inert because
  P6 REFUSEs on any startCommand divergence).
- Spec hygiene: P3 skip-test raises if probe runs under --no-require-staging-
  green; P6 warn-proceed stubs execute_promotion to isolate the gate and
  asserts rc==0; test_ghcr_token teardown unconditionally deletes
  GITHUB_TOKEN/GHCR_TOKEN/RAILWAY_TOKEN before restoring priors.

70 runs, 204 assertions, 0 failures (up from 66/188 baseline).
2026-05-29 11:45:13 -07:00

87 lines
4.1 KiB
Ruby

# frozen_string_literal: true
require_relative "spec_helper"
class PromoteP1Test < Minitest::Test
# Stub gql + ghcr clients used by PromoteCommand.
# Preflight checks accumulate findings before any short-circuit, so even
# on a P1 REFUSE the P2 deployments query is still issued. Provide a
# benign empty-deployments shape so the test focuses solely on P1.
class FakeGQLEmpty
def query(*); { "deployments" => { "edges" => [] } }; end
end
class FakeGHCR
def initialize(result); @result = result; end
def manifest_exists(_ref); @result; end
# Tag refs resolve to a synthetic digest; digest refs pass through.
# Returning a stable digest makes the resolved ref deterministic so
# the existing P1 tests (which staged digest-pinned images) continue
# to verify the same code path.
def resolve_digest(ref)
return ref.split("@", 2).last if ref.include?("@sha256:")
"sha256:fake_resolved_digest"
end
def parse_image_ref(ref); Railway::GHCR.allocate.parse_image_ref(ref); end
end
def test_refuses_when_digest_missing_in_ghcr
cmd = Railway::PromoteCommand.new(["--non-interactive", "--yes"])
# Inject deterministic precondition input — bypass live snapshot capture.
cmd.instance_variable_set(:@staging_snapshot, {
"services" => [{
"name" => "showcase-shell",
"service_id" => "svc-1",
"image" => "ghcr.io/copilotkit/showcase-shell@sha256:deadbeef",
"image_tag" => "ghcr.io/copilotkit/showcase-shell:latest",
"digest" => "sha256:deadbeef",
"env_keys" => [],
}],
})
cmd.instance_variable_set(:@prod_snapshot, { "services" => [] })
cmd.instance_variable_set(:@gql, FakeGQLEmpty.new)
cmd.instance_variable_set(:@ghcr, FakeGHCR.new(:missing))
cmd.define_singleton_method(:run_staging_probe) { |services:| { ok: true, summary: "" } }
out, _err = capture_io { @rc = cmd.run_with_preflight_only }
assert_equal 1, @rc, "promote must exit 1 on REFUSE"
assert_match(/REFUSE: P1.*ghcr\.io.*not found in GHCR/i, out)
end
def test_refuses_with_clear_message_on_auth_failed
cmd = Railway::PromoteCommand.new(["--non-interactive", "--yes"])
cmd.instance_variable_set(:@staging_snapshot, {
"services" => [{ "name" => "x", "service_id" => "s", "image" => "ghcr.io/copilotkit/x@sha256:abc", "env_keys" => [] }],
})
cmd.instance_variable_set(:@prod_snapshot, { "services" => [] })
# All preflight checks accumulate findings before any short-circuit;
# P2 will still query gql, so use the benign empty fake.
cmd.instance_variable_set(:@gql, FakeGQLEmpty.new)
cmd.instance_variable_set(:@ghcr, FakeGHCR.new(:auth_failed))
cmd.define_singleton_method(:run_staging_probe) { |services:| { ok: true, summary: "" } }
out, _err = capture_io { @rc = cmd.run_with_preflight_only }
assert_equal 1, @rc
assert_match(/REFUSE: P1.*GHCR.*auth/i, out)
assert_match(/GHCR_TOKEN.*or.*GITHUB_TOKEN/i, out)
end
def test_passes_p1_when_digest_exists
cmd = Railway::PromoteCommand.new(["--non-interactive", "--yes"])
cmd.instance_variable_set(:@staging_snapshot, {
"services" => [{ "name" => "x", "service_id" => "s", "image" => "ghcr.io/copilotkit/x@sha256:abc", "env_keys" => [] }],
})
cmd.instance_variable_set(:@prod_snapshot, { "services" => [] })
# P1 passes here so P2 will run — use the benign empty fake so
# the deployments query returns [].
cmd.instance_variable_set(:@gql, FakeGQLEmpty.new)
cmd.instance_variable_set(:@ghcr, FakeGHCR.new(:exists))
# P1 alone should not refuse; later checks (service-set parity) will,
# but P1's own gate is clean for this fixture.
cmd.define_singleton_method(:run_staging_probe) { |services:| { ok: true, summary: "" } }
out, _err = capture_io { cmd.run_with_preflight_only }
refute_match(/REFUSE: P1/, out)
end
end