Establish OpenCode compatibility certification corpus
Some checks failed
Candidate compatibility / prepare (push) Successful in 9s
Candidate compatibility / repository (push) Successful in 18s
Candidate compatibility / fixture-contract (push) Has been cancelled
Candidate compatibility / image ${{ matrix.id }} (push) Has been cancelled

This commit is contained in:
2026-07-18 13:35:32 -07:00
commit 51122eb000
39 changed files with 1218 additions and 0 deletions

93
.github/workflows/candidate.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: Candidate compatibility
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
opencode_ruby_ref:
description: Candidate opencode-ruby ref; blank uses the manifest
required: false
type: string
jobs:
repository:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
- run: ruby test/repository_test.rb
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.value }}
client_ref: ${{ steps.client.outputs.value }}
steps:
- uses: actions/checkout@v4
- id: matrix
run: echo "value=$(ruby scripts/matrix_json.rb)" >> "$GITHUB_OUTPUT"
- id: client
env:
INPUT_REF: ${{ inputs.opencode_ruby_ref }}
run: |
if [ -n "$INPUT_REF" ]; then
echo "value=$INPUT_REF" >> "$GITHUB_OUTPUT"
else
echo "value=$(jq -r .ref manifests/client-candidate.json)" >> "$GITHUB_OUTPUT"
fi
fixture-contract:
needs: prepare
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: ajaynomics/opencode-ruby
ref: ${{ needs.prepare.outputs.client_ref }}
path: client
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
working-directory: client
- name: Candidate unit tests
working-directory: client
run: bundle exec rake test
- name: Shared fixture contract
working-directory: client
env:
OPENCODE_RUBY_PATH: ${{ github.workspace }}/client
OPENCODE_RUBY_COMMIT: ${{ needs.prepare.outputs.client_ref }}
run: bundle exec ruby ../ruby/opencode_ruby_fixture_contract.rb
exact-image-contract:
needs: prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
name: image ${{ matrix.id }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: ajaynomics/opencode-ruby
ref: ${{ needs.prepare.outputs.client_ref }}
path: client
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
working-directory: client
- name: Exercise complete REST and SSE turn
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/client/Gemfile
OPENCODE_IMAGE: ${{ matrix.image }}
OPENCODE_RUBY_PATH: ${{ github.workspace }}/client
run: bundle exec scripts/run_image_contract.sh

81
.github/workflows/watch-upstream.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Watch upstream OpenCode
on:
schedule:
- cron: "23 */6 * * *"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: opencode-upstream-watcher
cancel-in-progress: false
jobs:
open-compatibility-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve latest release
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
release="$(gh api repos/anomalyco/opencode/releases/latest)"
tag="$(jq -r .tag_name <<<"$release")"
current="$(jq -r .release_tag manifests/upstream.json)"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "published_at=$(jq -r .published_at <<<"$release")" >> "$GITHUB_OUTPUT"
echo "release_url=$(jq -r .html_url <<<"$release")" >> "$GITHUB_OUTPUT"
if [ "$tag" = "$current" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Stop when a PR already exists
if: steps.release.outputs.changed == 'true'
id: existing
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
run: |
branch="compat/upstream-${TAG#v}"
count="$(gh pr list --state open --head "$branch" --json number --jq length)"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
if [ "$count" -gt 0 ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Resolve immutable image and update manifests
if: steps.release.outputs.changed == 'true' && steps.existing.outputs.skip == 'false'
env:
TAG: ${{ steps.release.outputs.tag }}
PUBLISHED_AT: ${{ steps.release.outputs.published_at }}
RELEASE_URL: ${{ steps.release.outputs.release_url }}
run: |
digest="$(docker buildx imagetools inspect "ghcr.io/anomalyco/opencode:${TAG#v}" --format '{{json .Manifest}}' | jq -r .digest)"
ruby scripts/record_upstream_candidate.rb "$TAG" "$PUBLISHED_AT" "$RELEASE_URL" "$digest"
ruby test/repository_test.rb
- name: Open compatibility PR
if: steps.release.outputs.changed == 'true' && steps.existing.outputs.skip == 'false'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
BRANCH: ${{ steps.existing.outputs.branch }}
run: |
git config user.name "opencode-compat-bot"
git config user.email "opencode-compat-bot@users.noreply.github.com"
git switch -c "$BRANCH"
git add manifests/image-matrix.json manifests/upstream.json
git commit -m "Test OpenCode ${TAG} compatibility"
git push --set-upstream origin "$BRANCH"
gh pr create \
--base main \
--head "$BRANCH" \
--title "Test OpenCode ${TAG} compatibility" \
--body "Upstream watcher detected ${TAG}, resolved its immutable OCI digest, and added it as a pending compatibility target. This PR only runs certification checks; promotion and consumer rollout remain manual."

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/.bundle/
/vendor/bundle/
/*.log
/.compat-evidence/
__pycache__/
*.pyc

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ajay Krishnan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

69
README.md Normal file
View File

@@ -0,0 +1,69 @@
# OpenCode compatibility corpus
This repository is the compatibility boundary between the fast-moving
[OpenCode](https://github.com/anomalyco/opencode) server and the clients and
host applications that depend on its HTTP, SSE, plugin, and runtime behavior.
It contains only synthetic fixtures and public image coordinates; no production
prompts, credentials, sessions, or user data belong here.
## Policy
- A gem release is a candidate until the fixture suite and every required image
profile pass.
- Image references are immutable OCI index digests. A tag may be recorded as
human-readable provenance, but it is never an execution coordinate.
- Custom consumer images are certified with an isolated canary on the host that
can pull them. Public CI certifies their upstream base plus the same profile.
- Promotion is explicit. CI may certify a tuple and the watcher may open a PR;
neither workflow deploys or changes a consumer.
- Each consumer retains both `current` and `previous` certified tuples so a
rollback restores the gem and server together.
## What is covered
The corpus exercises both historical and current terminal events, delta-before-
part ordering, authoritative final text, reasoning text, aggregate multi-step
usage, interactive question and permission waits, unknown-event tolerance, the
subscribe-before-prompt ordering contract, and reconnect without prompt replay.
Profiles keep the ownership boundaries explicit:
- `ruby-rest-sse`: the `opencode-ruby` HTTP/SSE adapter.
- `rails-persisted-turn`: Rails lifecycle, persistence, and recovery semantics.
- `voice-stream`: Greenroom's voice/LiveKit stream semantics.
- `strict-v2`: Leela's strict version and route-ownership gate.
- `plugin-ledger`: Ajent/plugin hook and ledger behavior.
- `provider-hooks`: inference provider configuration and hook behavior.
## Run locally
Prerequisites are Ruby 3.2+, Python 3, `jq`, and Docker.
```sh
ruby test/repository_test.rb
OPENCODE_RUBY_PATH=/data/projects/opencode-ruby-alpha6 \
ruby ruby/opencode_ruby_fixture_contract.rb
OPENCODE_RUBY_PATH=/data/projects/opencode-ruby-alpha6 \
OPENCODE_IMAGE='ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a' \
scripts/run_image_contract.sh
```
The live contract starts a deterministic local OpenAI-compatible model stub and
an isolated OpenCode container. It creates a session, subscribes, submits an
async prompt, observes terminal SSE, fetches the authoritative exchange, and
deletes the session. It never calls an external model provider.
## Adding an upstream release
The scheduled watcher compares the latest upstream GitHub release with the
manifest. When a new release appears, it resolves the tag to an OCI digest and
opens a compatibility PR. The PR adds a pending image-matrix entry, which runs
the candidate suite. A person still decides whether to promote it and update
consumers.
See [docs/certification.md](docs/certification.md) for promotion, canary, and
rollback evidence requirements.
## License
MIT. See [LICENSE](LICENSE).

49
docs/certification.md Normal file
View File

@@ -0,0 +1,49 @@
# Certification and promotion
## Candidate gate
A candidate tuple is the complete set of client version or commit, Rails gem
version or commit where applicable, exact OpenCode image, profile, and consumer
commit. Changing any member invalidates the certification.
The gate requires:
1. repository validation and the shared fixture corpus;
2. the public exact-image matrix against the deterministic model stub;
3. an isolated custom-image canary for Ajent Rails and Mushu;
4. the consumer's own focused and full required tests in its devcontainer;
5. a user-visible canary turn for stream consumers;
6. captured evidence including image ID/digest, server version, source commit,
gem commits, timestamps, and probe outcome.
Health-only probes do not certify a tuple.
## Promotion
Promotion is a reviewed manifest change that moves the old `current` tuple to
`previous` and the passing candidate to `current`. The consumer change is then
merged and deployed separately. Workflows in this repository have no deploy
credentials or deploy steps.
## Rollback
Rollback restores the whole `previous` tuple. Do not roll back only the gem or
only the runtime image: the wire contract is the unit of compatibility.
For a custom image, retain its immutable registry digest or Docker image ID and
the source commit used to build it. A source tag alone is insufficient.
## Custom-image canary
Run the shared live contract on a host that can pull the exact image:
```sh
OPENCODE_RUBY_PATH=/path/to/opencode-ruby \
OPENCODE_IMAGE='registry.example/image@sha256:...' \
scripts/run_image_contract.sh
```
If an older private registry cannot expose an OCI repository digest, set
`ALLOW_EXACT_IMAGE_ID=1` and pass the locally present `sha256:...` image ID.
Record the registry tag, image ID, source commit, and reason a repository
digest was unavailable in the evidence.

View File

@@ -0,0 +1,67 @@
{
"schema_version": 1,
"candidate": {
"repository": "ajaynomics/opencode-ruby",
"version": "0.0.1.alpha6",
"commit": "8adc95985a79b16e6e4bc0c0f827d638f244d509",
"transport_implementation_commit": "5113a953db8026697262bd4f97197a3d13077762"
},
"fixture_contract": {
"status": "pass",
"fixture_count": 7
},
"unit_contract": {
"status": "pass",
"runs": 24,
"assertions": 56
},
"image_contracts": [
{
"target": "travelwolf-1.16.1",
"image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a",
"server_version": "1.16.1",
"checked_at": "2026-07-18T20:34:05Z",
"status": "pass"
},
{
"target": "opencode-ajent-1.17.18",
"image": "ghcr.io/anomalyco/opencode@sha256:bf9d0e84b7cedef436a8f57db4d48767cd5d8fc6604f666335c1cc916b199a97",
"server_version": "1.17.18",
"checked_at": "2026-07-18T20:34:08Z",
"status": "pass"
},
{
"target": "ajent-base-1.18.3",
"image": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e",
"server_version": "1.18.3",
"checked_at": "2026-07-18T20:34:08Z",
"status": "pass"
},
{
"target": "ajent-blackline-live-artifact",
"image_id": "sha256:6fb2b3bf8e4cdf48e16bf6c3475e953df2f8304e77f5efe816ceb46f905eead8",
"registry_ref": "docker-registry:5000/opencode-blackline:83eed3c247a352842b08fc5cf339c4d0acdaacba",
"server_version": "1.18.3",
"checked_at": "2026-07-18T20:34:10Z",
"status": "pass"
},
{
"target": "mushu-permission-shortcuts-live-artifact",
"image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a",
"registry_ref": "docker-registry:5000/mushu-opencode:sha-99d6328b1",
"source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9",
"server_version": "0.0.0-permission-shortcuts-202606300721",
"checked_at": "2026-07-18T20:34:08Z",
"status": "pass"
}
],
"contract": [
"health",
"create-session",
"subscribe-ready-before-async-prompt",
"deterministic-model-request",
"sse-terminal",
"authoritative-final-exchange",
"delete-session"
]
}

View File

@@ -0,0 +1,4 @@
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_text","field":"text","delta":"current "}}
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_text","field":"text","delta":"ok"}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"part_text","messageID":"msg_assistant","sessionID":"ses_fixture","type":"text","text":"current ok"}}}
{"type":"session.status","properties":{"sessionID":"ses_fixture","status":{"type":"idle"}}}

View File

@@ -0,0 +1,3 @@
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_tail","field":"text","delta":"tail dro"}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"part_tail","messageID":"msg_assistant","sessionID":"ses_fixture","type":"text","text":"tail drop recovered"}}}
{"type":"session.status","properties":{"sessionID":"ses_fixture","status":{"type":"idle"}}}

View File

@@ -0,0 +1,7 @@
{"type":"question.asked","properties":{"id":"que_fixture","sessionID":"ses_fixture","questions":[{"header":"Choice","question":"Continue?","options":[{"label":"Yes","description":"Continue"}]}],"tool":{"messageID":"msg_assistant","callID":"call_question"}}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"part_tool","messageID":"msg_assistant","sessionID":"ses_fixture","type":"tool","callID":"call_question","tool":"question","state":{"status":"running","input":{}}}}}
{"type":"permission.asked","properties":{"id":"per_fixture","sessionID":"ses_fixture","permission":"bash","patterns":["echo *"]}}
{"type":"permission.replied","properties":{"sessionID":"ses_fixture","requestID":"per_fixture","reply":"once"}}
{"type":"question.replied","properties":{"sessionID":"ses_fixture","requestID":"que_fixture","answers":[["Yes"]]}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"part_tool","messageID":"msg_assistant","sessionID":"ses_fixture","type":"tool","callID":"call_question","tool":"question","state":{"status":"completed","input":{},"output":"answered"}}}}
{"type":"session.status","properties":{"sessionID":"ses_fixture","status":{"type":"idle"}}}

View File

@@ -0,0 +1,2 @@
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_text","field":"text","delta":"legacy ok"}}
{"type":"session.idle","properties":{"sessionID":"ses_fixture"}}

View File

@@ -0,0 +1,5 @@
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"step_1","messageID":"msg_assistant_1","sessionID":"ses_fixture","type":"step-finish","cost":0.12,"tokens":{"input":10,"output":3}}}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"step_2","messageID":"msg_assistant_2","sessionID":"ses_fixture","type":"step-finish","cost":0.34,"tokens":{"input":20,"output":7}}}}
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant_2","partID":"part_text","field":"text","delta":"two steps"}}
{"type":"message.updated","properties":{"sessionID":"ses_fixture","info":{"id":"msg_assistant_2","role":"assistant","cost":0.34,"tokens":{"input":20,"output":7}}}}
{"type":"session.status","properties":{"sessionID":"ses_fixture","status":{"type":"idle"}}}

View File

@@ -0,0 +1,5 @@
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_reason","field":"reasoning","delta":"considering"}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"part_reason","messageID":"msg_assistant","sessionID":"ses_fixture","type":"reasoning","text":"considering carefully"}}}
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_text","field":"text","delta":"final answer"}}
{"type":"message.part.updated","properties":{"sessionID":"ses_fixture","part":{"id":"part_text","messageID":"msg_assistant","sessionID":"ses_fixture","type":"text","text":"final answer"}}}
{"type":"session.status","properties":{"sessionID":"ses_fixture","status":{"type":"idle"}}}

View File

@@ -0,0 +1,3 @@
{"type":"server.future-capability","properties":{"sessionID":"ses_fixture","payload":{"new":true}}}
{"type":"message.part.delta","properties":{"sessionID":"ses_fixture","messageID":"msg_assistant","partID":"part_text","field":"text","delta":"still works"}}
{"type":"session.status","properties":{"sessionID":"ses_fixture","status":{"type":"idle"}}}

View File

@@ -0,0 +1,6 @@
{
"full_text": "current ok",
"reasoning_text": "",
"tool_count": 0,
"prompt_blocked": false
}

View File

@@ -0,0 +1,6 @@
{
"full_text": "tail drop recovered",
"reasoning_text": "",
"tool_count": 0,
"prompt_blocked": false
}

View File

@@ -0,0 +1,12 @@
{
"full_text": "",
"reasoning_text": "",
"tool_count": 1,
"prompt_blocked": false,
"tool": {
"tool": "question",
"status": "completed",
"callID": "call_question",
"opencode_request_id": "que_fixture"
}
}

View File

@@ -0,0 +1,6 @@
{
"full_text": "legacy ok",
"reasoning_text": "",
"tool_count": 0,
"prompt_blocked": false
}

View File

@@ -0,0 +1,9 @@
{
"full_text": "two steps",
"reasoning_text": "",
"tool_count": 0,
"prompt_blocked": false,
"total_cost": 0.46,
"total_input_tokens": 30,
"total_output_tokens": 10
}

View File

@@ -0,0 +1,6 @@
{
"full_text": "final answer",
"reasoning_text": "considering carefully",
"tool_count": 0,
"prompt_blocked": false
}

View File

@@ -0,0 +1,6 @@
{
"full_text": "still works",
"reasoning_text": "",
"tool_count": 0,
"prompt_blocked": false
}

12
fixtures/manifest.json Normal file
View File

@@ -0,0 +1,12 @@
{
"schema_version": 1,
"fixtures": [
{"id": "legacy-idle", "events": "events/legacy-idle.jsonl", "expected": "expected/legacy-idle.json"},
{"id": "current-status-idle", "events": "events/current-status-idle.jsonl", "expected": "expected/current-status-idle.json"},
{"id": "delta-before-part", "events": "events/delta-before-part.jsonl", "expected": "expected/delta-before-part.json"},
{"id": "reasoning-and-final", "events": "events/reasoning-and-final.jsonl", "expected": "expected/reasoning-and-final.json"},
{"id": "multi-step-usage", "events": "events/multi-step-usage.jsonl", "expected": "expected/multi-step-usage.json"},
{"id": "interactive-prompts", "events": "events/interactive-prompts.jsonl", "expected": "expected/interactive-prompts.json"},
{"id": "unknown-event", "events": "events/unknown-event.jsonl", "expected": "expected/unknown-event.json"}
]
}

View File

@@ -0,0 +1,6 @@
{
"repository": "ajaynomics/opencode-ruby",
"ref": "8adc95985a79b16e6e4bc0c0f827d638f244d509",
"version": "0.0.1.alpha6",
"status": "candidate"
}

View File

@@ -0,0 +1,58 @@
{
"schema_version": 1,
"public_ci": [
{
"id": "upstream-1.16.1",
"version": "1.16.1",
"image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a",
"tag_provenance": "ghcr.io/anomalyco/opencode:1.16.1",
"consumers": ["travelwolf"],
"profiles": ["ruby-rest-sse", "rails-persisted-turn"],
"certification_status": "certified"
},
{
"id": "upstream-1.17.18",
"version": "1.17.18",
"image": "ghcr.io/anomalyco/opencode@sha256:bf9d0e84b7cedef436a8f57db4d48767cd5d8fc6604f666335c1cc916b199a97",
"tag_provenance": "ghcr.io/anomalyco/opencode:1.17.18",
"consumers": ["opencode-ajent"],
"profiles": ["plugin-ledger", "ruby-rest-sse"],
"certification_status": "certified"
},
{
"id": "upstream-1.18.3",
"version": "1.18.3",
"image": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e",
"tag_provenance": "ghcr.io/anomalyco/opencode:1.18.3",
"consumers": ["ajent-rails"],
"profiles": ["ruby-rest-sse", "rails-persisted-turn", "plugin-ledger"],
"certification_status": "certified"
}
],
"host_canary": [
{
"id": "ajent-blackline-live",
"consumer": "ajent-rails",
"registry_ref": "docker-registry:5000/opencode-blackline:83eed3c247a352842b08fc5cf339c4d0acdaacba",
"docker_image_id": "sha256:6fb2b3bf8e4cdf48e16bf6c3475e953df2f8304e77f5efe816ceb46f905eead8",
"reported_version": "1.18.3",
"upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e",
"profiles": ["ruby-rest-sse", "rails-persisted-turn", "plugin-ledger"],
"certification_status": "candidate-certified",
"certified_candidate": "8adc95985a79b16e6e4bc0c0f827d638f244d509",
"certified_at": "2026-07-18T20:34:10Z"
},
{
"id": "mushu-permission-shortcuts-live",
"consumer": "mushu",
"registry_ref": "docker-registry:5000/mushu-opencode:sha-99d6328b1",
"docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a",
"reported_version": "0.0.0-permission-shortcuts-202606300721",
"source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9",
"profiles": ["ruby-rest-sse"],
"certification_status": "candidate-certified",
"certified_candidate": "8adc95985a79b16e6e4bc0c0f827d638f244d509",
"certified_at": "2026-07-18T20:34:08Z"
}
]
}

View File

@@ -0,0 +1,82 @@
{
"schema_version": 1,
"migration_state": "candidate",
"consumers": {
"ajent-rails": {
"profile": "rails-persisted-turn",
"current": {
"status": "observed-production",
"consumer_commit": "83eed3c247a352842b08fc5cf339c4d0acdaacba",
"opencode_ruby": {"version": "0.0.1.alpha2", "git_commit": "889d383"},
"opencode_rails": {"version": "0.0.1.alpha2", "git_commit": "9b0c4cd"},
"runtime": {
"registry_ref": "docker-registry:5000/opencode-blackline:83eed3c247a352842b08fc5cf339c4d0acdaacba",
"docker_image_id": "sha256:6fb2b3bf8e4cdf48e16bf6c3475e953df2f8304e77f5efe816ceb46f905eead8",
"reported_version": "1.18.3",
"upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e"
}
},
"candidate": {
"status": "compatibility-certified",
"opencode_ruby": {"version": "0.0.1.alpha6", "git_commit": "8adc95985a79b16e6e4bc0c0f827d638f244d509"},
"opencode_rails": {"version": "0.0.1.alpha6", "git_commit": "ca08bb36e82cbe059f2c0dedbed0ad2c602d1afd"},
"runtime": {
"docker_image_id": "sha256:6fb2b3bf8e4cdf48e16bf6c3475e953df2f8304e77f5efe816ceb46f905eead8",
"reported_version": "1.18.3",
"upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e"
},
"certified_at": "2026-07-18T20:34:10Z"
},
"previous": null
},
"travelwolf": {
"profile": "rails-persisted-turn",
"current": {
"status": "repository-configured",
"consumer_commit": "1ca756d64f058c9e0379f69fc4a3d53330cfcd15",
"opencode_ruby": {"version": "0.0.1.alpha2", "git_commit": "889d383"},
"opencode_rails": {"version": "0.0.1.alpha2", "git_commit": "9b0c4cd"},
"runtime": {
"image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a",
"reported_version": "1.16.1"
}
},
"candidate": {
"status": "compatibility-certified",
"opencode_ruby": {"version": "0.0.1.alpha6", "git_commit": "8adc95985a79b16e6e4bc0c0f827d638f244d509"},
"opencode_rails": {"version": "0.0.1.alpha6", "git_commit": "ca08bb36e82cbe059f2c0dedbed0ad2c602d1afd"},
"runtime": {
"image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a",
"reported_version": "1.16.1"
},
"certified_at": "2026-07-18T20:34:05Z"
},
"previous": null
},
"mushu": {
"profile": "ruby-rest-sse",
"current": {
"status": "observed-production",
"consumer_commit": "fe16e5bab028e3c92cd5508ef2cebd9bfa23386f",
"opencode_ruby": {"version": "0.0.1.alpha2", "git_commit": "889d383"},
"runtime": {
"registry_ref": "docker-registry:5000/mushu-opencode:sha-99d6328b1",
"docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a",
"source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9",
"reported_version": "0.0.0-permission-shortcuts-202606300721"
}
},
"candidate": {
"status": "compatibility-certified",
"opencode_ruby": {"version": "0.0.1.alpha6", "git_commit": "8adc95985a79b16e6e4bc0c0f827d638f244d509"},
"runtime": {
"docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a",
"source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9",
"reported_version": "0.0.0-permission-shortcuts-202606300721"
},
"certified_at": "2026-07-18T20:34:08Z"
},
"previous": null
}
}
}

9
manifests/upstream.json Normal file
View File

@@ -0,0 +1,9 @@
{
"repository": "anomalyco/opencode",
"release_tag": "v1.18.3",
"version": "1.18.3",
"published_at": "2026-07-16T15:34:33Z",
"release_url": "https://github.com/anomalyco/opencode/releases/tag/v1.18.3",
"image": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e",
"observed_at": "2026-07-18T00:00:00Z"
}

View File

@@ -0,0 +1,11 @@
{
"id": "plugin-ledger",
"owner": "opencode-ajent",
"required_behaviors": [
"exact-upstream-image",
"exact-plugin-version",
"runtime-launcher-separate-from-plugin",
"ledger-hooks",
"permission-hooks"
]
}

View File

@@ -0,0 +1,11 @@
{
"id": "provider-hooks",
"owner": "inference",
"required_behaviors": [
"provider-schema",
"model-routing",
"system-prompt-hook",
"session-migration-dry-run",
"session-migration-idempotency"
]
}

View File

@@ -0,0 +1,13 @@
{
"id": "rails-persisted-turn",
"owner": "opencode-rails",
"extends": ["ruby-rest-sse"],
"required_behaviors": [
"session-recovery",
"mid-stream-snapshot",
"terminal-state-cas",
"authoritative-final-exchange",
"aggregate-turn-usage",
"artifact-reconciliation"
]
}

View File

@@ -0,0 +1,19 @@
{
"id": "ruby-rest-sse",
"owner": "opencode-ruby",
"required_fixtures": [
"legacy-idle",
"current-status-idle",
"delta-before-part",
"reasoning-and-final",
"multi-step-usage",
"interactive-prompts",
"unknown-event"
],
"transport_scenarios": [
"subscribe-ready-before-prompt",
"reconnect-without-prompt-replay",
"prompt-transport-error-is-visible"
],
"live_contract": ["health", "create-session", "async-prompt", "sse-terminal", "final-exchange", "delete-session"]
}

11
profiles/strict-v2.json Normal file
View File

@@ -0,0 +1,11 @@
{
"id": "strict-v2",
"owner": "leela",
"required_behaviors": [
"exact-upstream-version-and-commit",
"bun-toolchain-provenance",
"v2-route-contract",
"every-mutating-route-requires-owner",
"admission-and-abort-remain-custom"
]
}

View File

@@ -0,0 +1,11 @@
{
"id": "voice-stream",
"owner": "greenroom",
"extends": ["ruby-rest-sse"],
"required_behaviors": [
"legacy-and-current-terminal-events",
"aggregate-assistant-usage",
"authoritative-final-text",
"reconnect-same-stream-without-reprompt"
]
}

66
ruby/live_probe.rb Normal file
View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
require "json"
require "time"
gem_path = File.expand_path(ENV.fetch("OPENCODE_RUBY_PATH"))
$LOAD_PATH.unshift(File.join(gem_path, "lib"))
require "opencode-ruby"
base_url = ENV.fetch("OPENCODE_BASE_URL")
model = ENV.fetch("OPENCODE_COMPAT_MODEL", "compat/compat-model")
expected_text = ENV.fetch("OPENCODE_COMPAT_EXPECTED_TEXT", "compat-ok")
client = Opencode::Client.new(base_url: base_url, timeout: 30)
session_id = nil
begin
health = client.health
raise "OpenCode health did not report healthy: #{health.inspect}" unless health[:healthy] || health[:ok]
session = client.create_session(title: "opencode-compat isolated probe")
session_id = session.fetch(:id)
observed_parts = []
result = client.stream(
session_id,
"Reply with exactly: #{expected_text}",
model: model,
stream_timeout: 30,
first_event_timeout: 15,
idle_stream_timeout: 15
) do |part|
observed_parts << JSON.parse(JSON.generate(part))
end
unless result.full_text.include?(expected_text)
raise "Expected final text to include #{expected_text.inspect}, got #{result.full_text.inspect}"
end
messages = client.get_messages(session_id)
assistant_messages = Array(messages).select { |message| message.dig(:info, :role) == "assistant" }
raise "No authoritative assistant exchange returned" if assistant_messages.empty?
puts JSON.generate(
status: "pass",
checked_at: Time.now.utc.iso8601,
opencode_base_url: base_url,
opencode_health: health,
opencode_ruby_version: Opencode::VERSION,
opencode_ruby_commit: ENV["OPENCODE_RUBY_COMMIT"],
session_id: session_id,
expected_text: expected_text,
full_text: result.full_text,
observed_part_count: observed_parts.length,
authoritative_assistant_message_count: assistant_messages.length
)
rescue StandardError => error
diagnostic = {error: "#{error.class}: #{error.message}"}
if session_id
diagnostic[:session_status] = client.session_status
diagnostic[:messages] = client.get_messages(session_id)
end
warn JSON.generate(diagnostic)
raise
ensure
client.delete_session(session_id) if session_id
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
require "json"
root = File.expand_path("..", __dir__)
gem_path = File.expand_path(ENV.fetch("OPENCODE_RUBY_PATH"))
$LOAD_PATH.unshift(File.join(gem_path, "lib"))
require "opencode-ruby"
manifest = JSON.parse(File.read(File.join(root, "fixtures/manifest.json")))
failures = []
manifest.fetch("fixtures").each do |entry|
reply = Opencode::Reply.new
event_path = File.join(root, "fixtures", entry.fetch("events"))
expected_path = File.join(root, "fixtures", entry.fetch("expected"))
File.foreach(event_path) do |line|
next if line.strip.empty?
reply.apply(JSON.parse(line, symbolize_names: true))
end
result = reply.result
expected = JSON.parse(File.read(expected_path))
actual = {
"full_text" => result.full_text,
"reasoning_text" => result.reasoning_text,
"tool_count" => result.tool_parts.length,
"prompt_blocked" => reply.prompt_blocked?,
"total_cost" => reply.total_cost,
"total_input_tokens" => reply.total_input_tokens,
"total_output_tokens" => reply.total_output_tokens
}
if (tool_expectation = expected["tool"])
tool = result.tool_parts.first || reply.parts.find { |part| part["type"] == "tool" }
actual["tool"] = {
"tool" => tool&.fetch("tool", nil),
"status" => tool&.fetch("status", nil),
"callID" => tool&.fetch("callID", nil),
"opencode_request_id" => tool&.dig("input", "opencode_request_id")
}
expected["tool"] = tool_expectation
end
expected.each do |key, value|
next if actual[key] == value
failures << "#{entry.fetch("id")}: #{key} expected #{value.inspect}, got #{actual[key].inspect}"
end
unless failures.any? { |failure| failure.start_with?("#{entry.fetch("id")}:") }
puts "PASS #{entry.fetch("id")}"
end
end
unless failures.empty?
warn failures.join("\n")
exit 1
end
puts JSON.generate(
status: "pass",
adapter: "opencode-ruby",
adapter_version: Opencode::VERSION,
adapter_commit: ENV["OPENCODE_RUBY_COMMIT"],
fixture_count: manifest.fetch("fixtures").length
)

111
scripts/fake_llm.py Executable file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Deterministic OpenAI-compatible streaming server for image contracts."""
from __future__ import annotations
import argparse
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
class State:
def __init__(self) -> None:
self.lock = threading.Lock()
self.requests: list[dict[str, object]] = []
def record(self, path: str, body: object) -> None:
with self.lock:
self.requests.append({"path": path, "body": body, "at": time.time()})
def snapshot(self) -> dict[str, object]:
with self.lock:
return {"request_count": len(self.requests), "requests": list(self.requests)}
STATE = State()
def chat_stream(text: str) -> bytes:
chunks = [
{
"id": "chatcmpl-opencode-compat",
"object": "chat.completion.chunk",
"choices": [{"delta": {"role": "assistant"}}],
},
{
"id": "chatcmpl-opencode-compat",
"object": "chat.completion.chunk",
"choices": [{"delta": {"content": text}}],
},
{
"id": "chatcmpl-opencode-compat",
"object": "chat.completion.chunk",
"choices": [{"delta": {}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7},
},
]
payload = "".join(f"data: {json.dumps(chunk, separators=(',', ':'))}\n\n" for chunk in chunks)
payload += "data: [DONE]\n\n"
return payload.encode()
class Handler(BaseHTTPRequestHandler):
server_version = "OpenCodeCompatLLM/1"
def log_message(self, _format: str, *_args: object) -> None:
return
def send_bytes(self, status: int, body: bytes, content_type: str) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None: # noqa: N802
if self.path == "/health":
self.send_bytes(200, b'{"ok":true}', "application/json")
return
if self.path == "/stats":
self.send_bytes(200, json.dumps(STATE.snapshot()).encode(), "application/json")
return
if self.path == "/v1/models":
body = {"object": "list", "data": [{"id": "compat-model", "object": "model"}]}
self.send_bytes(200, json.dumps(body).encode(), "application/json")
return
self.send_bytes(404, b'{"error":"not found"}', "application/json")
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length) if length else b"{}"
try:
body = json.loads(raw)
except json.JSONDecodeError:
body = {"invalid_json": raw.decode(errors="replace")}
STATE.record(self.path, body)
if self.path == "/v1/chat/completions":
self.send_bytes(200, chat_stream("compat-ok"), "text/event-stream")
return
self.send_bytes(404, b'{"error":"unsupported endpoint"}', "application/json")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=0)
parser.add_argument("--port-file", required=True)
args = parser.parse_args()
server = ThreadingHTTPServer((args.host, args.port), Handler)
Path(args.port_file).write_text(str(server.server_port), encoding="utf-8")
server.serve_forever()
if __name__ == "__main__":
main()

15
scripts/matrix_json.rb Executable file
View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
require "json"
root = File.expand_path("..", __dir__)
manifest = JSON.parse(File.read(File.join(root, "manifests/image-matrix.json")))
matrix = manifest.fetch("public_ci").map do |target|
{
"id" => target.fetch("id"),
"version" => target.fetch("version"),
"image" => target.fetch("image")
}
end
puts JSON.generate("include" => matrix)

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
require "json"
require "time"
root = File.expand_path("..", __dir__)
tag, published_at, release_url, digest = ARGV
abort "usage: record_upstream_candidate.rb TAG PUBLISHED_AT RELEASE_URL DIGEST" unless digest
abort "invalid upstream release tag: #{tag.inspect}" unless tag.match?(/\Av\d+\.\d+\.\d+\z/)
abort "invalid OCI digest: #{digest.inspect}" unless digest.match?(/\Asha256:[0-9a-f]{64}\z/)
version = tag.delete_prefix("v")
image = "ghcr.io/anomalyco/opencode@#{digest}"
upstream_path = File.join(root, "manifests/upstream.json")
matrix_path = File.join(root, "manifests/image-matrix.json")
upstream = JSON.parse(File.read(upstream_path))
matrix = JSON.parse(File.read(matrix_path))
exit 0 if upstream.fetch("release_tag") == tag
upstream.merge!(
"release_tag" => tag,
"version" => version,
"published_at" => published_at,
"release_url" => release_url,
"image" => image,
"observed_at" => Time.now.utc.iso8601
)
unless matrix.fetch("public_ci").any? { |target| target.fetch("image") == image }
matrix.fetch("public_ci") << {
"id" => "upstream-#{version}",
"version" => version,
"image" => image,
"tag_provenance" => "ghcr.io/anomalyco/opencode:#{version}",
"consumers" => ["upstream-candidate"],
"profiles" => ["ruby-rest-sse", "rails-persisted-turn", "voice-stream", "strict-v2", "plugin-ledger", "provider-hooks"],
"certification_status" => "pending"
}
end
File.write(upstream_path, JSON.pretty_generate(upstream) + "\n")
File.write(matrix_path, JSON.pretty_generate(matrix) + "\n")

142
scripts/run_image_contract.sh Executable file
View File

@@ -0,0 +1,142 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
image="${OPENCODE_IMAGE:?set OPENCODE_IMAGE to an immutable image digest}"
gem_path="${OPENCODE_RUBY_PATH:?set OPENCODE_RUBY_PATH to the candidate checkout}"
if [[ "$image" != *@sha256:* ]]; then
if [[ "${ALLOW_EXACT_IMAGE_ID:-0}" != "1" || "$image" != sha256:* ]]; then
echo "OPENCODE_IMAGE must be an OCI digest (or an exact local image ID with ALLOW_EXACT_IMAGE_ID=1)" >&2
exit 2
fi
fi
container_name="opencode-compat-${RANDOM}-$$"
llm_container_name="opencode-compat-llm-${RANDOM}-$$"
network_name="opencode-compat-net-${RANDOM}-$$"
python_image="python@sha256:399babc8b49529dabfd9c922f2b5eea81d611e4512e3ed250d75bd2e7683f4b0"
cleanup() {
status=$?
if [[ "$status" != "0" ]]; then
echo "OpenCode compatibility probe failed; container log follows" >&2
docker logs "$container_name" >&2 2>/dev/null || true
echo "Deterministic model request summary follows" >&2
docker exec "$llm_container_name" wget -qO- http://127.0.0.1:8080/stats >&2 2>/dev/null || true
echo >&2
fi
docker rm -f "$container_name" >/dev/null 2>&1 || true
docker rm -f "$llm_container_name" >/dev/null 2>&1 || true
docker network rm "$network_name" >/dev/null 2>&1 || true
return "$status"
}
trap cleanup EXIT
docker network create "$network_name" >/dev/null
docker run --detach \
--name "$llm_container_name" \
--network "$network_name" \
--network-alias compat-llm \
--volume "$repo_root/scripts:/compat:ro" \
"$python_image" \
python /compat/fake_llm.py --port 8080 --port-file /tmp/compat-port \
>/dev/null
for _ in $(seq 1 100); do
if docker exec "$llm_container_name" wget -qO- http://127.0.0.1:8080/health >/dev/null 2>&1; then
llm_ready=1
break
fi
sleep 0.05
done
[[ "${llm_ready:-0}" == "1" ]] || { echo "fake LLM did not start" >&2; exit 1; }
config_json="$(jq -cn --arg url "http://compat-llm:8080/v1" '{
"$schema": "https://opencode.ai/config.json",
formatter: false,
lsp: false,
provider: {
compat: {
name: "Compatibility fixture",
id: "compat",
env: [],
npm: "@ai-sdk/openai-compatible",
models: {
"compat-model": {
id: "compat-model",
name: "Compatibility model",
attachment: false,
reasoning: false,
temperature: false,
tool_call: true,
release_date: "2026-01-01",
limit: {context: 100000, output: 10000},
cost: {input: 0, output: 0},
options: {}
}
},
options: {apiKey: "compat-key", baseURL: $url}
}
}
}')"
docker run --detach \
--name "$container_name" \
--network "$network_name" \
--publish 127.0.0.1::4096 \
--env "OPENCODE_CONFIG_CONTENT=$config_json" \
--env OPENCODE_DISABLE_AUTOUPDATE=1 \
--env OPENCODE_DISABLE_AUTOCOMPACT=1 \
--env OPENCODE_DISABLE_MODELS_FETCH=1 \
--env OPENCODE_DISABLE_PROJECT_CONFIG=1 \
--env OPENCODE_PURE=1 \
"$image" serve --hostname 0.0.0.0 --port 4096 \
>/dev/null
host_port="$(docker port "$container_name" 4096/tcp | sed -E 's/.*:([0-9]+)$/\1/' | head -1)"
base_url="http://127.0.0.1:${host_port}"
ready=0
for _ in $(seq 1 120); do
if curl --fail --silent --connect-timeout 1 --max-time 2 "$base_url/global/health" >/dev/null; then
ready=1
break
fi
if ! docker inspect "$container_name" --format '{{.State.Running}}' 2>/dev/null | grep -qx true; then
docker logs "$container_name" >&2 || true
exit 1
fi
sleep 0.25
done
if [[ "$ready" != "1" ]]; then
docker logs "$container_name" >&2 || true
echo "OpenCode did not become ready" >&2
exit 1
fi
server_version="$(docker exec "$container_name" opencode --version)"
image_id="$(docker inspect "$container_name" --format '{{.Image}}')"
gem_commit="$(git -C "$gem_path" rev-parse HEAD 2>/dev/null || true)"
OPENCODE_BASE_URL="$base_url" \
OPENCODE_RUBY_PATH="$gem_path" \
OPENCODE_RUBY_COMMIT="$gem_commit" \
ruby "$repo_root/ruby/live_probe.rb"
llm_stats="$(docker exec "$llm_container_name" wget -qO- http://127.0.0.1:8080/stats)"
request_count="$(jq -r '.request_count' <<<"$llm_stats")"
if [[ "$request_count" -lt 1 ]]; then
echo "OpenCode never called the deterministic model" >&2
exit 1
fi
jq -cn \
--arg status pass \
--arg image "$image" \
--arg image_id "$image_id" \
--arg server_version "$server_version" \
--arg gem_commit "$gem_commit" \
--argjson llm_request_count "$request_count" \
'{status:$status,image:$image,image_id:$image_id,server_version:$server_version,opencode_ruby_commit:$gem_commit,llm_request_count:$llm_request_count}'

63
test/repository_test.rb Normal file
View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
require "json"
require "minitest/autorun"
class RepositoryTest < Minitest::Test
ROOT = File.expand_path("..", __dir__)
def json(path)
JSON.parse(File.read(File.join(ROOT, path)))
end
def test_every_json_document_parses
paths = Dir.glob(File.join(ROOT, "{fixtures,manifests,profiles}/**/*.json"))
refute_empty paths
paths.each { |path| JSON.parse(File.read(path)) }
end
def test_fixture_manifest_is_complete_and_unique
entries = json("fixtures/manifest.json").fetch("fixtures")
ids = entries.map { |entry| entry.fetch("id") }
assert_equal ids.uniq, ids
entries.each do |entry|
assert_path_exists File.join(ROOT, "fixtures", entry.fetch("events"))
assert_path_exists File.join(ROOT, "fixtures", entry.fetch("expected"))
end
end
def test_profile_fixture_references_exist
fixture_ids = json("fixtures/manifest.json").fetch("fixtures").map { |entry| entry.fetch("id") }
Dir.glob(File.join(ROOT, "profiles/*.json")).each do |path|
profile = JSON.parse(File.read(path))
Array(profile["required_fixtures"]).each { |id| assert_includes fixture_ids, id }
end
end
def test_public_matrix_uses_only_immutable_oci_digests
targets = json("manifests/image-matrix.json").fetch("public_ci")
refute_empty targets
targets.each do |target|
assert_match %r{\Aghcr\.io/anomalyco/opencode@sha256:[0-9a-f]{64}\z}, target.fetch("image")
refute_includes target.fetch("image"), ":latest"
refute_empty target.fetch("profiles")
end
end
def test_certified_migration_keeps_previous_tuple
tuples = json("manifests/runtime-tuples.json")
return unless tuples.fetch("migration_state") == "certified"
tuples.fetch("consumers").each_value do |consumer|
refute_nil consumer["current"]
refute_nil consumer["previous"]
assert_equal "certified", consumer.fetch("current").fetch("status")
end
end
def test_watcher_has_no_deployment_commands
workflow = File.read(File.join(ROOT, ".github/workflows/watch-upstream.yml"))
refute_match(/\b(kamal|kubectl|helm|nomad|docker\s+service)\b/i, workflow)
end
end