Establish OpenCode compatibility certification corpus
Some checks failed
Some checks failed
This commit is contained in:
93
.github/workflows/candidate.yml
vendored
Normal file
93
.github/workflows/candidate.yml
vendored
Normal 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
81
.github/workflows/watch-upstream.yml
vendored
Normal 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
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/.bundle/
|
||||||
|
/vendor/bundle/
|
||||||
|
/*.log
|
||||||
|
/.compat-evidence/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
69
README.md
Normal 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
49
docs/certification.md
Normal 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.
|
||||||
67
evidence/2026-07-18-opencode-ruby-alpha6.json
Normal file
67
evidence/2026-07-18-opencode-ruby-alpha6.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
4
fixtures/events/current-status-idle.jsonl
Normal file
4
fixtures/events/current-status-idle.jsonl
Normal 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"}}}
|
||||||
3
fixtures/events/delta-before-part.jsonl
Normal file
3
fixtures/events/delta-before-part.jsonl
Normal 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"}}}
|
||||||
7
fixtures/events/interactive-prompts.jsonl
Normal file
7
fixtures/events/interactive-prompts.jsonl
Normal 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"}}}
|
||||||
2
fixtures/events/legacy-idle.jsonl
Normal file
2
fixtures/events/legacy-idle.jsonl
Normal 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"}}
|
||||||
5
fixtures/events/multi-step-usage.jsonl
Normal file
5
fixtures/events/multi-step-usage.jsonl
Normal 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"}}}
|
||||||
5
fixtures/events/reasoning-and-final.jsonl
Normal file
5
fixtures/events/reasoning-and-final.jsonl
Normal 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"}}}
|
||||||
3
fixtures/events/unknown-event.jsonl
Normal file
3
fixtures/events/unknown-event.jsonl
Normal 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"}}}
|
||||||
6
fixtures/expected/current-status-idle.json
Normal file
6
fixtures/expected/current-status-idle.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"full_text": "current ok",
|
||||||
|
"reasoning_text": "",
|
||||||
|
"tool_count": 0,
|
||||||
|
"prompt_blocked": false
|
||||||
|
}
|
||||||
6
fixtures/expected/delta-before-part.json
Normal file
6
fixtures/expected/delta-before-part.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"full_text": "tail drop recovered",
|
||||||
|
"reasoning_text": "",
|
||||||
|
"tool_count": 0,
|
||||||
|
"prompt_blocked": false
|
||||||
|
}
|
||||||
12
fixtures/expected/interactive-prompts.json
Normal file
12
fixtures/expected/interactive-prompts.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
fixtures/expected/legacy-idle.json
Normal file
6
fixtures/expected/legacy-idle.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"full_text": "legacy ok",
|
||||||
|
"reasoning_text": "",
|
||||||
|
"tool_count": 0,
|
||||||
|
"prompt_blocked": false
|
||||||
|
}
|
||||||
9
fixtures/expected/multi-step-usage.json
Normal file
9
fixtures/expected/multi-step-usage.json
Normal 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
|
||||||
|
}
|
||||||
6
fixtures/expected/reasoning-and-final.json
Normal file
6
fixtures/expected/reasoning-and-final.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"full_text": "final answer",
|
||||||
|
"reasoning_text": "considering carefully",
|
||||||
|
"tool_count": 0,
|
||||||
|
"prompt_blocked": false
|
||||||
|
}
|
||||||
6
fixtures/expected/unknown-event.json
Normal file
6
fixtures/expected/unknown-event.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"full_text": "still works",
|
||||||
|
"reasoning_text": "",
|
||||||
|
"tool_count": 0,
|
||||||
|
"prompt_blocked": false
|
||||||
|
}
|
||||||
12
fixtures/manifest.json
Normal file
12
fixtures/manifest.json
Normal 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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
6
manifests/client-candidate.json
Normal file
6
manifests/client-candidate.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"repository": "ajaynomics/opencode-ruby",
|
||||||
|
"ref": "8adc95985a79b16e6e4bc0c0f827d638f244d509",
|
||||||
|
"version": "0.0.1.alpha6",
|
||||||
|
"status": "candidate"
|
||||||
|
}
|
||||||
58
manifests/image-matrix.json
Normal file
58
manifests/image-matrix.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
manifests/runtime-tuples.json
Normal file
82
manifests/runtime-tuples.json
Normal 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
9
manifests/upstream.json
Normal 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"
|
||||||
|
}
|
||||||
11
profiles/plugin-ledger.json
Normal file
11
profiles/plugin-ledger.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
profiles/provider-hooks.json
Normal file
11
profiles/provider-hooks.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
13
profiles/rails-persisted-turn.json
Normal file
13
profiles/rails-persisted-turn.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
19
profiles/ruby-rest-sse.json
Normal file
19
profiles/ruby-rest-sse.json
Normal 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
11
profiles/strict-v2.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
profiles/voice-stream.json
Normal file
11
profiles/voice-stream.json
Normal 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
66
ruby/live_probe.rb
Normal 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
|
||||||
69
ruby/opencode_ruby_fixture_contract.rb
Normal file
69
ruby/opencode_ruby_fixture_contract.rb
Normal 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
111
scripts/fake_llm.py
Executable 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
15
scripts/matrix_json.rb
Executable 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)
|
||||||
43
scripts/record_upstream_candidate.rb
Executable file
43
scripts/record_upstream_candidate.rb
Executable 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
142
scripts/run_image_contract.sh
Executable 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
63
test/repository_test.rb
Normal 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
|
||||||
Reference in New Issue
Block a user