@openwop/openwop-conformance 2.46.28 → 2.46.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/eval-suites/conformance-eval-gate-failing.json +34 -0
- package/fixtures.md +9 -0
- package/package.json +2 -2
- package/requirements.json +20 -4
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +10 -10
- package/src/lib/deployment-eval-gate-witness.ts +53 -0
- package/src/scenarios/v2-deployment-eval-gate.test.ts +116 -0
- package/src/scenarios/v2-eval-trajectory-trials.test.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [2.46.30] — 2026-10-26 — RFC 0252 Draft (no assertion changes)
|
|
2
|
+
|
|
3
|
+
- **No scenario or assertion changes.** RFC 0252 (`Draft`) adds its gap register and RFC 0244's G5 row moves to it, so the packed `spec/v1/gaps.json` changes.
|
|
4
|
+
- **Host impact:** none.
|
|
5
|
+
|
|
6
|
+
## [2.46.29] — 2026-10-26 — RFC 0082 eval-gated promotion at major 2
|
|
7
|
+
|
|
8
|
+
- **New scenario `v2-deployment-eval-gate` (RFC 0082 §E).** It reads the scripted agent's deployment records and picks a `draft`, `test` or `staged` one. It then runs the new fixture `conformance-eval-gate-failing` (one task that always fails) and promotes that version, citing the failed run as `evalRunId`. `0082.eval-gate-unmet-refused` expects `422 eval_gate_unmet`, and `0082.eval-gate-unmet-unchanged` expects the record's `state`, `channels` and `canaryPercent` to be unchanged. A summary that does not say `passed: false`, or no promotable record, records `blocked`. Judges in `lib/deployment-eval-gate-witness.ts` with self-tests.
|
|
9
|
+
- **Host impact:** opt-in. The legs need `agents.deployment`, `agents.evalSuite` and the fixture in discovery `fixtures`. Listing it states that the host's promote of the scripted agent carries `requiredEval` and that it holds a promotable record (contract in `fixtures.md`). No host lists it yet, so the legs record `inapplicable`.
|
|
10
|
+
|
|
1
11
|
## [2.46.28] — 2026-10-26 — RFC 0244 G2 closed; RFC 0251 and the §F legs
|
|
2
12
|
|
|
3
13
|
- **New scenario `v2-eval-regression-baseline` (RFC 0251)** over two fixtures sharing one `suiteId` (`conformance-eval-regression-base` / `-candidate`), whose paired scores are equal while the aggregates differ by 1/3: `0251.shape` (server-free), `baseline-echoed`, `baseline-refused`, `baseline-uniform` (needs `OPENWOP_TEST_TENANT_B_API_KEY`, else `blocked`), `fields-refused`, and RFC 0244 §F `0244.regression-paired` and `regression-no-false-verdict`. Judges in `lib/eval-regression-witness.ts`.
|
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ Each scenario file records its disposition and assertion count in `<report-dir>/
|
|
|
137
137
|
|
|
138
138
|
## What's Covered
|
|
139
139
|
|
|
140
|
-
The current suite has
|
|
140
|
+
The current suite has 663 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 223 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
|
|
141
141
|
|
|
142
142
|
The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
|
|
143
143
|
|
|
@@ -166,7 +166,7 @@ Three kinds of scenario share the tree:
|
|
|
166
166
|
- **Host** checks drive a live host over HTTP.
|
|
167
167
|
- **Gated** checks run only when the host advertises the family, profile or fixture they need, and otherwise record `inapplicable` or `blocked` with a reason. `--require-behavior` turns an advertised-but-unwitnessable behaviour into a failure.
|
|
168
168
|
|
|
169
|
-
Current source tree:
|
|
169
|
+
Current source tree: 663 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
|
|
170
170
|
|
|
171
171
|
## How the 2.x suite is built
|
|
172
172
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"suiteId": "core.openwop.evals.conformance-eval-gate-failing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"targetAgentId": "core.conformance.agents.scripted",
|
|
5
|
+
"modes": [
|
|
6
|
+
"golden"
|
|
7
|
+
],
|
|
8
|
+
"thresholds": {
|
|
9
|
+
"passScore": 1
|
|
10
|
+
},
|
|
11
|
+
"tasks": [
|
|
12
|
+
{
|
|
13
|
+
"taskId": "always-fails",
|
|
14
|
+
"input": {
|
|
15
|
+
"script": [
|
|
16
|
+
{
|
|
17
|
+
"tool": "conformance.eval:lookup",
|
|
18
|
+
"arguments": {
|
|
19
|
+
"orderId": "o-1"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"output": "not the expected answer"
|
|
24
|
+
},
|
|
25
|
+
"expected": {
|
|
26
|
+
"kind": "golden",
|
|
27
|
+
"match": {
|
|
28
|
+
"strategy": "contains",
|
|
29
|
+
"value": "done"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
package/fixtures.md
CHANGED
|
@@ -79,6 +79,7 @@ All fixtures MUST advertise:
|
|
|
79
79
|
| Eval — Counter (miss) | `conformance-eval-counter-miss` | RFC 0244 §D, G2 — `onMiss: "fail"`; three `inc S` calls against two exact entries, then an unmatched `inc F` (`v2-eval-trajectory-trials`) | eval run `completed`; `{served: 1}`, `{served: 2}`, then two tool errors | ≤ 30s |
|
|
80
80
|
| Eval — Efficiency Ungated | `conformance-eval-efficiency-ungated` | RFC 0244 §C — two golden tasks with one output, `lean` (1 call) and `busy` (8 calls), at `passScore: 1` (`v2-eval-trajectory-trials`) | eval run `completed`; both tasks and the suite pass | ≤ 30s |
|
|
81
81
|
| Eval — Memory Isolation | `conformance-eval-memory-isolation` | RFC 0244 §B, G3 — `trials.count: 2`; one task seeds `seeded`, then reads it, reads `trial-note`, writes it and reads it back through `conformance.eval:memory` (`v2-eval-trajectory-trials`) | eval run `completed`; each trial reads the seed and finds no `trial-note` before its own write | ≤ 30s |
|
|
82
|
+
| Eval — Gate Failing | `conformance-eval-gate-failing` | RFC 0082 §E — one task whose output never contains the golden `done`, so the summary is `passed: false`; its run is then cited by a `promote` of the scripted agent (`v2-deployment-eval-gate`) | eval run `completed`, `passed: false`; the promote is refused `422 eval_gate_unmet` | ≤ 30s |
|
|
82
83
|
| Eval — Regression (base) | `conformance-eval-regression-base` | RFC 0251 — the baseline run: suite 1.0.0, `base-only` fails, `shared-a` and `shared-b` pass (`v2-eval-regression-baseline`) | eval run `completed`; `aggregateScore` 2/3 | ≤ 30s |
|
|
83
84
|
| Eval — Regression (candidate) | `conformance-eval-regression-candidate` | RFC 0251, RFC 0244 §F — the same `suiteId` at 1.1.0: the two shared tasks unchanged, plus `candidate-only` (passes), run against the base run (`v2-eval-regression-baseline`) | eval run `completed`; `aggregateScore` 1; `scoreDelta` 1/3; `pairedTaskCount` 2; verdict neither `improved` nor `regressed` | ≤ 30s |
|
|
84
85
|
| Dispatch Fan-Out Waves | `conformance-dispatch-fanout-waves` | RFC 0243 §A.3 — a parallel `core.dispatch` (`maxConcurrency: 2`, `wait-all`/`collect`) whose supervisor's `mockDispatchPlan` names three `conformance-delay` children. At most `min(2, maxFanOut)` run at once and none is dropped. Gated on `agents.fanOut` admitting `wait-all` with `collect`. | `completed` | ≤ 30s |
|
|
@@ -1067,6 +1068,7 @@ Every task targets **the scripted agent `core.conformance.agents.scripted`**. It
|
|
|
1067
1068
|
| `conformance-eval-counter-miss` | `agents.evalSuite.fixtureMatching` | the three `inc S` calls return `{served: 1}`, `{served: 2}`, then a tool error; `inc F` returns a tool error. Neither miss moves the live counter |
|
|
1068
1069
|
| `conformance-eval-efficiency-ungated` | `agents.evalSuite` | RFC 0244 §C never-gated: with all nine scripted calls in the log and `lean` passing, `busy` passes with `lean`'s score and the suite passes. A host MUST NOT fail either on `efficiency`, reported or not |
|
|
1069
1070
|
| `conformance-eval-memory-isolation` | `agents.evalSuite.trials` | RFC 0244 §B isolation: both reads of `seeded` return `seed`; of the four `trial-note` reads, two return `written` (each trial's read-back) and two return `null` (each trial's first read). A later trial reading `written` first saw an earlier trial's write. Fewer than two read-backs record `blocked` |
|
|
1071
|
+
| `conformance-eval-gate-failing` | `agents.evalSuite` + `agents.deployment` | RFC 0082 §E: a `promote` of a `draft`, `test` or `staged` record of the scripted agent citing this run as `evalRunId` is refused `422 eval_gate_unmet`, and the record keeps its `state`, `channels` and `canaryPercent`. A summary that does not say `passed: false` records `blocked` |
|
|
1070
1072
|
| `conformance-eval-regression-base` | `agents.evalSuite.baselineSelection` + `regression` in `modes` | run first with `evalModes: [golden]`; the baseline of the candidate run |
|
|
1071
1073
|
| `conformance-eval-regression-candidate` | `agents.evalSuite.baselineSelection` + `regression` in `modes` (+ `regressionVerdict` for the §F legs) | run with `evalModes: [golden, regression]` and the base run as `baselineRunId`: both carriers name it, `scoreDelta` 1/3, `pairedTaskCount` 2, verdict neither `improved` nor `regressed`. Both fixtures share one `suiteId`, so a host's suite store resolves them by fixture id |
|
|
1072
1074
|
|
|
@@ -1078,6 +1080,13 @@ Every task targets **the scripted agent `core.conformance.agents.scripted`**. It
|
|
|
1078
1080
|
|
|
1079
1081
|
The counter has no external effect, so serving it is safe on a production tenant. Two concurrent cuts on one tenant make the persistence check fail; the legs then record `blocked`, not a failure.
|
|
1080
1082
|
|
|
1083
|
+
**`conformance-eval-gate-failing` declares an eval-gated promotion.** Whether a promote carries `requiredEval` is host policy, so listing this fixture in discovery `fixtures` is the host's statement that:
|
|
1084
|
+
|
|
1085
|
+
- its approval gate for `promote` of `core.conformance.agents.scripted` carries `requiredEval`, so the promote's `evalRunId` is verified as the `agents.deployment` facet requires;
|
|
1086
|
+
- it holds at least one deployment record of that agent in `draft`, `test` or `staged`, readable at `GET /agents/core.conformance.agents.scripted/deployments` by the conformance credential, which also holds the `deploy:*` scope.
|
|
1087
|
+
|
|
1088
|
+
A host whose gate the suite cannot reach (no promotable record) records `blocked`. A host that promotes anyway fails, and has moved that record: reset it before the next cut.
|
|
1089
|
+
|
|
1081
1090
|
**The memory tool `conformance.eval:memory` is conformance-RESERVED.** A host that lists `conformance-eval-memory-isolation` in discovery `fixtures` MUST, at its eval tool boundary:
|
|
1082
1091
|
|
|
1083
1092
|
- answer every call to it from the agent's memory for the current trial: the store the task's `fixtures.memorySeed` seeds (RFC 0004 `MemoryAdapter` entries) and the agent's own memory writes land in. It is never served from `toolResponses` and never counted as a fixture miss.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.46.
|
|
3
|
+
"version": "2.46.30",
|
|
4
4
|
"description": "Black-box conformance suite for OpenWOP hosts (v2 current; v1 scenarios retained for the frozen v1 tree).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.46.
|
|
59
|
+
"@openwop/spec-artifacts": "2.46.30"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
|
-
"files":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 751,
|
|
6
|
+
"tests": 2857,
|
|
7
|
+
"withStableId": 2857,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2730
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -30182,6 +30182,22 @@
|
|
|
30182
30182
|
}
|
|
30183
30183
|
]
|
|
30184
30184
|
},
|
|
30185
|
+
{
|
|
30186
|
+
"id": "openwop.it.v2-deployment-eval-gate.answers-422-eval-gate-unmet",
|
|
30187
|
+
"file": "v2-deployment-eval-gate.test.ts",
|
|
30188
|
+
"line": 100,
|
|
30189
|
+
"title": "answers 422 eval_gate_unmet",
|
|
30190
|
+
"explicitId": "openwop.requirement.0082.eval-gate-unmet-refused",
|
|
30191
|
+
"citations": []
|
|
30192
|
+
},
|
|
30193
|
+
{
|
|
30194
|
+
"id": "openwop.it.v2-deployment-eval-gate.leaves-the-deployment-record-as-it-was",
|
|
30195
|
+
"file": "v2-deployment-eval-gate.test.ts",
|
|
30196
|
+
"line": 108,
|
|
30197
|
+
"title": "leaves the deployment record as it was",
|
|
30198
|
+
"explicitId": "openwop.requirement.0082.eval-gate-unmet-unchanged",
|
|
30199
|
+
"citations": []
|
|
30200
|
+
},
|
|
30185
30201
|
{
|
|
30186
30202
|
"id": "openwop.it.v2-discovery-etag.the-discovery-document-carries-an-etag",
|
|
30187
30203
|
"file": "v2-discovery-etag.test.ts",
|
package/scenario-majors.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
|
|
3
3
|
"counts": {
|
|
4
|
-
"files":
|
|
4
|
+
"files": 663,
|
|
5
5
|
"v1": 455,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 223
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1425,6 +1425,9 @@
|
|
|
1425
1425
|
"v2-dead-letter-advertisement.test.ts": [
|
|
1426
1426
|
2
|
|
1427
1427
|
],
|
|
1428
|
+
"v2-deployment-eval-gate.test.ts": [
|
|
1429
|
+
2
|
|
1430
|
+
],
|
|
1428
1431
|
"v2-discovery-etag.test.ts": [
|
|
1429
1432
|
2
|
|
1430
1433
|
],
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.46.
|
|
5
|
-
"corpusTag": "v2.46.
|
|
4
|
+
"version": "2.46.30",
|
|
5
|
+
"corpusTag": "v2.46.30",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "08c68b844a0298d1f7232e5956713ed5235414eca608782f8c9cc607cee06f32",
|
|
13
|
+
"api/v2/asyncapi.yaml": "edfda007ec4a3ef1ceafa8125c87d02dbf77367e4816f0b41455c9827ae3f8fe",
|
|
14
|
+
"api/v2/openapi.yaml": "2e8450ca565d0fe75c27dae848122891ba80e5fa40f0876796c51979572efd44",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -212,14 +212,14 @@
|
|
|
212
212
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
213
213
|
"spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
|
|
214
214
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
215
|
-
"spec/v1/core-standard-manifest.json": "
|
|
215
|
+
"spec/v1/core-standard-manifest.json": "e341bdbad39a25a8c04f535b1dc44d581b5473069c5ebbe786f5f8031f61a679",
|
|
216
216
|
"spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
|
|
217
217
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
218
218
|
"spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
|
|
219
219
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
220
220
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
221
221
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
222
|
-
"spec/v1/gaps.json": "
|
|
222
|
+
"spec/v1/gaps.json": "16ab681ea051c41104d53161bcf179704f80659861f7dd3ba8bdbfe3015f71b8",
|
|
223
223
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
224
224
|
"spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
|
|
225
225
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -318,9 +318,9 @@
|
|
|
318
318
|
"spec/v2/path-manifest.json": "20f598f56df460ad3ed50aba3d6bc2933ea16a3c96ce125da1c5dd6989d4155c",
|
|
319
319
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
320
320
|
"spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
|
|
321
|
-
"spec/v2/release.json": "
|
|
321
|
+
"spec/v2/release.json": "5bb12627a01a0f732dc8ab1f6c7f8d23ed4f583782b83591a1d72803be9592bc",
|
|
322
322
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
323
|
-
"spec/v2/surface-baseline.json": "
|
|
323
|
+
"spec/v2/surface-baseline.json": "da513617337bdcf1073907d88662f3c853110bb0107e789ea3cfd245e86a0933"
|
|
324
324
|
},
|
|
325
|
-
"corpusCommit": "
|
|
325
|
+
"corpusCommit": "2c254f70680834a1ed087ba9369dcdbfd1c0a40d"
|
|
326
326
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0082 §E — eval-gated promotion at major 2. Pure judges over one
|
|
3
|
+
* `transitionAgentDeployment` promote that cites a failed eval run, and the
|
|
4
|
+
* deployment records read before and after it, for
|
|
5
|
+
* `v2-deployment-eval-gate.test.ts`.
|
|
6
|
+
*
|
|
7
|
+
* The rule is the `agents.deployment` facet description: when the gate
|
|
8
|
+
* carries `requiredEval`, the host MUST verify the eval run is terminal and
|
|
9
|
+
* passed before `deployment.promoted`, else deny with `eval_gate_unmet`
|
|
10
|
+
* (`422`, `spec/v2/errors.json`). A host opts in by listing
|
|
11
|
+
* `conformance-eval-gate-failing`, which declares that its promote of the
|
|
12
|
+
* scripted agent is so gated (`conformance/fixtures.md`).
|
|
13
|
+
*
|
|
14
|
+
* @see RFCS/0082-agent-deployment-lifecycle.md §E
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readErrorCode } from './error-envelope.js';
|
|
18
|
+
|
|
19
|
+
export interface GateFinding { readonly ok: boolean; readonly message: string }
|
|
20
|
+
export interface Answer { readonly status: number; readonly json: unknown }
|
|
21
|
+
type Rec = Record<string, unknown>;
|
|
22
|
+
const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
23
|
+
|
|
24
|
+
/** The states a `promote` leaves: the record moves toward `active`. */
|
|
25
|
+
export const PROMOTABLE = ['draft', 'test', 'staged'] as const;
|
|
26
|
+
|
|
27
|
+
/** The first record a promote can advance, or undefined. */
|
|
28
|
+
export function pickPromotable(records: unknown): Rec | undefined {
|
|
29
|
+
if (!Array.isArray(records)) return undefined;
|
|
30
|
+
return records.find((r): r is Rec => isRec(r) && typeof r['version'] === 'string' && (PROMOTABLE as readonly string[]).includes(String(r['state'])));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The promote is refused `422 eval_gate_unmet`. */
|
|
34
|
+
export function judgeGateRefused(a: Answer): GateFinding[] {
|
|
35
|
+
const code = readErrorCode(a.json);
|
|
36
|
+
return [
|
|
37
|
+
{ ok: a.status === 422, message: `a promote citing a failed eval run MUST be refused 422 (got ${a.status} ${code ?? ''})`.trim() },
|
|
38
|
+
{ ok: code === 'eval_gate_unmet', message: `the refusal MUST carry the registered code eval_gate_unmet (got ${JSON.stringify(code)})` },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The refused promote left the version's record as it was. */
|
|
43
|
+
export function judgeRecordUnchanged(before: Rec, after: unknown): GateFinding[] {
|
|
44
|
+
const version = before['version'];
|
|
45
|
+
const now = Array.isArray(after) ? after.find((r): r is Rec => isRec(r) && r['version'] === version) : undefined;
|
|
46
|
+
if (now === undefined) return [{ ok: false, message: `the deployment record for version ${String(version)} MUST still be listed after the refused promote` }];
|
|
47
|
+
const same = (k: string): boolean => JSON.stringify(before[k]) === JSON.stringify(now[k]);
|
|
48
|
+
return [
|
|
49
|
+
{ ok: same('state'), message: `a refused promote MUST NOT change state (was ${JSON.stringify(before['state'])}, now ${JSON.stringify(now['state'])})` },
|
|
50
|
+
{ ok: same('channels'), message: `a refused promote MUST NOT bind channels (was ${JSON.stringify(before['channels'])}, now ${JSON.stringify(now['channels'])})` },
|
|
51
|
+
{ ok: same('canaryPercent'), message: `a refused promote MUST NOT change canaryPercent (was ${JSON.stringify(before['canaryPercent'])}, now ${JSON.stringify(now['canaryPercent'])})` },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — a promote that cites a failed eval run is refused (RFC 0082 §E; the
|
|
3
|
+
* rule is the `agents.deployment` facet description in
|
|
4
|
+
* `spec/v2/facets/agents.schema.json`: when the gate carries `requiredEval`,
|
|
5
|
+
* the host MUST verify the eval run is terminal and passed before
|
|
6
|
+
* `deployment.promoted`, else deny with `eval_gate_unmet`).
|
|
7
|
+
*
|
|
8
|
+
* The v1 `agent-deployment-lifecycle` scenario witnessed this through the v1
|
|
9
|
+
* sample seam only. Here the suite causes it on the wire, with no seam:
|
|
10
|
+
* 1. read `GET /agents/{agentId}/deployments` for the scripted agent and
|
|
11
|
+
* pick a `draft`, `test` or `staged` record;
|
|
12
|
+
* 2. run `conformance-eval-gate-failing` (one task that always fails, so the
|
|
13
|
+
* summary is `passed: false`) to completion;
|
|
14
|
+
* 3. `POST /agents/{agentId}/deployments` `{version, transition: "promote",
|
|
15
|
+
* evalRunId}`.
|
|
16
|
+
*
|
|
17
|
+
* Whether a promote is gated on `requiredEval` is host policy, so a host opts
|
|
18
|
+
* in by listing `conformance-eval-gate-failing` in discovery `fixtures`: the
|
|
19
|
+
* fixture contract (`conformance/fixtures.md`) says its promote of the
|
|
20
|
+
* scripted agent carries `requiredEval`, and that it holds a promotable record.
|
|
21
|
+
* Judges in `lib/deployment-eval-gate-witness.ts`.
|
|
22
|
+
* - `0082.eval-gate-unmet-refused` — `422 eval_gate_unmet`.
|
|
23
|
+
* - `0082.eval-gate-unmet-unchanged` — the record keeps its state, channels
|
|
24
|
+
* and canary share.
|
|
25
|
+
*
|
|
26
|
+
* @see RFCS/0082-agent-deployment-lifecycle.md §E
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
31
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
32
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
33
|
+
import { req } from '../lib/requirement-ids.js';
|
|
34
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
35
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
36
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
37
|
+
import { pollTerminal, isRecord } from '../lib/fixture-run-observer.js';
|
|
38
|
+
import { judgeGateRefused, judgeRecordUnchanged, pickPromotable, type GateFinding } from '../lib/deployment-eval-gate-witness.js';
|
|
39
|
+
|
|
40
|
+
const PROFILE = majorProfile(2);
|
|
41
|
+
const DOC = 'agents.schema.json §deployment, transitionAgentDeployment (RFC 0082 §E)';
|
|
42
|
+
const AGENT = 'core.conformance.agents.scripted';
|
|
43
|
+
const FIXTURE = 'conformance-eval-gate-failing';
|
|
44
|
+
const ID_REFUSED = 'openwop.requirement.0082.eval-gate-unmet-refused';
|
|
45
|
+
const ID_UNCHANGED = 'openwop.requirement.0082.eval-gate-unmet-unchanged';
|
|
46
|
+
const DEPLOYMENTS = `/agents/${encodeURIComponent(AGENT)}/deployments`;
|
|
47
|
+
|
|
48
|
+
type Skip = { skip: [Parameters<typeof softSkip>[0], string] };
|
|
49
|
+
type Outcome = { before: Record<string, unknown>; refusal: OpenWOPResponse; after: unknown };
|
|
50
|
+
|
|
51
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
52
|
+
try { return await fn(); } catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
const expectAll = (id: string, findings: readonly GateFinding[]): void => {
|
|
55
|
+
for (const x of findings) expect(x.ok, req(id, DOC, x.message)).toBe(true);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
async function gate(): Promise<Skip | null> {
|
|
59
|
+
let doc: Record<string, unknown> | null;
|
|
60
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
61
|
+
if (!doc) return { skip: ['blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0'] };
|
|
62
|
+
const agents = doc['agents'];
|
|
63
|
+
if (!isRecord(agents) || !isRecord(agents['deployment'])) return { skip: ['inapplicable', 'the host does not advertise agents.deployment'] };
|
|
64
|
+
if (!isRecord(agents['evalSuite'])) return { skip: ['inapplicable', 'the host does not advertise agents.evalSuite'] };
|
|
65
|
+
if (!isFixtureAdvertised(FIXTURE)) return { skip: ['inapplicable', `the host does not advertise fixture ${FIXTURE}`] };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let once: Promise<Skip | Outcome> | undefined;
|
|
70
|
+
/** One refused promote, read by both legs. */
|
|
71
|
+
function promoteAgainstFailedEval(): Promise<Skip | Outcome> {
|
|
72
|
+
once ??= (async (): Promise<Skip | Outcome> => {
|
|
73
|
+
const list = await http(() => driver.get(DEPLOYMENTS));
|
|
74
|
+
if (list === null || list.status !== 200) return { skip: ['blocked', `GET ${DEPLOYMENTS} answered ${list?.status ?? 'nothing'}`] };
|
|
75
|
+
const before = pickPromotable(list.json);
|
|
76
|
+
if (before === undefined) return { skip: ['blocked', `the host lists ${FIXTURE} but holds no draft, test or staged record of ${AGENT} to promote`] };
|
|
77
|
+
|
|
78
|
+
const res = await http(() => driver.post(PROFILE.runsPath, { mode: 'eval', evalSuiteRef: `https://conformance.openwop.dev/eval-suites/${FIXTURE}`, agentId: AGENT }));
|
|
79
|
+
if (res === null) return { skip: ['blocked', 'POST /runs unreachable (fetch failed)'] };
|
|
80
|
+
if (res.status === 429) return { skip: ['blocked', 'POST /runs answered 429 — the run budget, not the wire'] };
|
|
81
|
+
const evalRunId = isRecord(res.json) && typeof res.json['runId'] === 'string' ? res.json['runId'] : undefined;
|
|
82
|
+
if ((res.status !== 201 && res.status !== 202) || evalRunId === undefined) return { skip: ['blocked', `createRun of ${FIXTURE} answered ${res.status} ${readErrorCode(res.json) ?? ''}`.trim()] };
|
|
83
|
+
const terminal = await pollTerminal(PROFILE, evalRunId, { timeoutMs: 60_000 });
|
|
84
|
+
if (terminal.snapshot['status'] !== 'completed') return { skip: ['blocked', `eval run ${evalRunId} ended ${String(terminal.snapshot['status'])}, not completed`] };
|
|
85
|
+
const summary = await http(() => driver.get(`${PROFILE.runsPath}/${PROFILE.idSegment(evalRunId)}/eval-summary`));
|
|
86
|
+
if (summary === null || summary.status !== 200 || !isRecord(summary.json)) return { skip: ['blocked', `GET …/eval-summary answered ${summary?.status ?? 'nothing'}`] };
|
|
87
|
+
// Without a failed summary the refusal would prove nothing about the gate.
|
|
88
|
+
if (summary.json['passed'] !== false) return { skip: ['blocked', `${FIXTURE} MUST fail, but its summary says passed: ${JSON.stringify(summary.json['passed'])}`] };
|
|
89
|
+
|
|
90
|
+
const refusal = await http(() => driver.post(DEPLOYMENTS, { version: before['version'], transition: 'promote', evalRunId }));
|
|
91
|
+
if (refusal === null) return { skip: ['blocked', `POST ${DEPLOYMENTS} unreachable (fetch failed)`] };
|
|
92
|
+
if (refusal.status === 429) return { skip: ['blocked', `POST ${DEPLOYMENTS} answered 429 — the rate budget, not the gate`] };
|
|
93
|
+
const after = await http(() => driver.get(DEPLOYMENTS));
|
|
94
|
+
return { before, refusal, after: after?.status === 200 ? after.json : undefined };
|
|
95
|
+
})();
|
|
96
|
+
return once;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('v2 RFC 0082 §E — a promote citing a failed eval run is refused (host)', () => {
|
|
100
|
+
it('answers 422 eval_gate_unmet', async () => {
|
|
101
|
+
const g = await gate();
|
|
102
|
+
if (g) return softSkip(...g.skip);
|
|
103
|
+
const o = await promoteAgainstFailedEval();
|
|
104
|
+
if ('skip' in o) return softSkip(...o.skip);
|
|
105
|
+
expectAll(ID_REFUSED, judgeGateRefused({ status: o.refusal.status, json: o.refusal.json }));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('leaves the deployment record as it was', async () => {
|
|
109
|
+
const g = await gate();
|
|
110
|
+
if (g) return softSkip(...g.skip);
|
|
111
|
+
const o = await promoteAgainstFailedEval();
|
|
112
|
+
if ('skip' in o) return softSkip(...o.skip);
|
|
113
|
+
if (o.after === undefined) return softSkip('blocked', `GET ${DEPLOYMENTS} after the promote did not answer 200`);
|
|
114
|
+
expectAll(ID_UNCHANGED, judgeRecordUnchanged(o.before, o.after));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -239,7 +239,7 @@ describe('v2 RFC 0244 — wire shape (server-free)', () => {
|
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
it('the eval-suite fixtures validate against the v2 suite schema', () => {
|
|
242
|
-
for (const id of ['conformance-eval-trajectory', 'conformance-eval-trials', 'conformance-eval-fixture-miss', 'conformance-eval-trajectory-gate', 'conformance-eval-trajectory-gate-mean', 'conformance-eval-fixture-error', 'conformance-eval-counter-live', 'conformance-eval-counter-miss', 'conformance-eval-efficiency-ungated', 'conformance-eval-memory-isolation', 'conformance-eval-regression-base', 'conformance-eval-regression-candidate']) {
|
|
242
|
+
for (const id of ['conformance-eval-trajectory', 'conformance-eval-trials', 'conformance-eval-fixture-miss', 'conformance-eval-trajectory-gate', 'conformance-eval-trajectory-gate-mean', 'conformance-eval-fixture-error', 'conformance-eval-counter-live', 'conformance-eval-counter-miss', 'conformance-eval-efficiency-ungated', 'conformance-eval-memory-isolation', 'conformance-eval-gate-failing', 'conformance-eval-regression-base', 'conformance-eval-regression-candidate']) {
|
|
243
243
|
const r = suiteSchema(fixture(id));
|
|
244
244
|
expect(r.ok, req(ID_FIXTURES, 'fixtures.md §eval-suites', `${id} MUST validate: ${r.errors}`)).toBe(true);
|
|
245
245
|
}
|