@openwop/openwop-conformance 2.46.40 → 2.46.42
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 +11 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-all-terminals-skipped.json +71 -0
- package/fixtures/conformance-error-route-only.json +56 -0
- package/fixtures/conformance-subworkflow-skip-parent.json +76 -0
- package/fixtures.md +34 -0
- package/package.json +2 -2
- package/requirements.json +69 -4
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +14 -14
- package/src/lib/proposal-pack-readback-witness.ts +14 -2
- package/src/lib/skipped-terminals-witness.ts +73 -0
- package/src/scenarios/v2-all-terminals-skipped.test.ts +136 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## [2.46.42] — 2026-11-03 — The fresh chain pack is exact-pinned (RFC 0253)
|
|
2
|
+
|
|
3
|
+
- **`freshChainPack` pins its node** (`core.noop@1.0.0`). At 2.46.41 it referenced `core.noop` with no version, which `workflow-chain-packs.md` §Exact pins makes a host refuse, so a conforming host failed the chain apply, read-back and conflict legs.
|
|
4
|
+
- **`packContentViolations` checks chain pins**, and a self-test fails an unpinned or ranged node reference.
|
|
5
|
+
|
|
6
|
+
## [2.46.41] — 2026-11-02 — Every terminal skipped ends completed (RFC 0257)
|
|
7
|
+
|
|
8
|
+
- **New scenario `v2-all-terminals-skipped` (RFC 0257).** `0257.shape` (server-free) and five host legs: a trigger-rule skip, a condition skip with its positive control, a replay fork, and a `core.subWorkflow` parent. Each leg gates on its fixture in `fixtures[]`.
|
|
9
|
+
- **New fixtures** `conformance-error-route-only`, `conformance-all-terminals-skipped` and `conformance-subworkflow-skip-parent`, with catalog rows and contracts in `fixtures.md`.
|
|
10
|
+
- **Judges** in `lib/skipped-terminals-witness.ts` with five self-tests. Before merge every host leg failed its targeted sabotage on a fake host and passed a conforming one.
|
|
11
|
+
|
|
1
12
|
## [2.46.40] — 2026-11-01 — No suite change
|
|
2
13
|
|
|
3
14
|
- **Version only.** Ships with corpus 2.46.40, which closes two gap-register rows; no scenario, fixture or library changed.
|
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 666 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 226 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: 666 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,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-all-terminals-skipped",
|
|
3
|
+
"name": "Conformance: All terminals skipped",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0257. check echoes payload; act runs only when payload.ok is truthy (left resolves as a dot path into check's node.completed outputs). payload {ok:false} ends run.completed with skippedTerminals [\"act\"]; payload {ok:true} ends run.completed with no marker. Conformance scenario: v2-all-terminals-skipped.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "check",
|
|
9
|
+
"typeId": "core.identity",
|
|
10
|
+
"name": "Check",
|
|
11
|
+
"position": {
|
|
12
|
+
"x": 0,
|
|
13
|
+
"y": 0
|
|
14
|
+
},
|
|
15
|
+
"config": {},
|
|
16
|
+
"inputs": {
|
|
17
|
+
"payload": {
|
|
18
|
+
"type": "variable",
|
|
19
|
+
"variableName": "payload"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "act",
|
|
25
|
+
"typeId": "core.noop",
|
|
26
|
+
"name": "Act",
|
|
27
|
+
"position": {
|
|
28
|
+
"x": 200,
|
|
29
|
+
"y": 0
|
|
30
|
+
},
|
|
31
|
+
"config": {},
|
|
32
|
+
"inputs": {}
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"edges": [
|
|
36
|
+
{
|
|
37
|
+
"id": "check-act",
|
|
38
|
+
"sourceNodeId": "check",
|
|
39
|
+
"targetNodeId": "act",
|
|
40
|
+
"condition": {
|
|
41
|
+
"type": "truthy",
|
|
42
|
+
"left": "payload.ok"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"triggers": [
|
|
47
|
+
{
|
|
48
|
+
"id": "manual",
|
|
49
|
+
"type": "manual",
|
|
50
|
+
"enabled": true
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"variables": [
|
|
54
|
+
{
|
|
55
|
+
"name": "payload",
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "{ok: boolean} \u2014 whether act runs.",
|
|
58
|
+
"required": true
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"metadata": {
|
|
62
|
+
"tags": [
|
|
63
|
+
"conformance",
|
|
64
|
+
"skipped-terminals"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"settings": {
|
|
68
|
+
"timeout": 60000,
|
|
69
|
+
"maxRetries": 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-error-route-only",
|
|
3
|
+
"name": "Conformance: Error route only",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0257. start succeeds, and the only terminal on-fail is reached over an any_failed edge, so it is skipped with no condition anywhere. The run MUST end run.completed with skippedTerminals [\"on-fail\"]. Conformance scenario: v2-all-terminals-skipped.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "start",
|
|
9
|
+
"typeId": "core.noop",
|
|
10
|
+
"name": "Start",
|
|
11
|
+
"position": {
|
|
12
|
+
"x": 0,
|
|
13
|
+
"y": 0
|
|
14
|
+
},
|
|
15
|
+
"config": {},
|
|
16
|
+
"inputs": {}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "on-fail",
|
|
20
|
+
"typeId": "core.noop",
|
|
21
|
+
"name": "On failure",
|
|
22
|
+
"position": {
|
|
23
|
+
"x": 200,
|
|
24
|
+
"y": 0
|
|
25
|
+
},
|
|
26
|
+
"config": {},
|
|
27
|
+
"inputs": {}
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"edges": [
|
|
31
|
+
{
|
|
32
|
+
"id": "start-on-fail",
|
|
33
|
+
"sourceNodeId": "start",
|
|
34
|
+
"targetNodeId": "on-fail",
|
|
35
|
+
"triggerRule": "any_failed"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"triggers": [
|
|
39
|
+
{
|
|
40
|
+
"id": "manual",
|
|
41
|
+
"type": "manual",
|
|
42
|
+
"enabled": true
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"variables": [],
|
|
46
|
+
"metadata": {
|
|
47
|
+
"tags": [
|
|
48
|
+
"conformance",
|
|
49
|
+
"skipped-terminals"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"settings": {
|
|
53
|
+
"timeout": 60000,
|
|
54
|
+
"maxRetries": 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-subworkflow-skip-parent",
|
|
3
|
+
"name": "Conformance: Sub-workflow (child reaches no terminal)",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0257 \u00a7B. Dispatches conformance-error-route-only, which completes with skippedTerminals [\"on-fail\"]. The core.subWorkflow node MUST complete with childStatus completed and childSkippedTerminals [\"on-fail\"] under the default fail-parent, and the parent completes. Conformance scenario: v2-all-terminals-skipped.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "parent-start",
|
|
9
|
+
"typeId": "core.noop",
|
|
10
|
+
"name": "Parent Start",
|
|
11
|
+
"position": {
|
|
12
|
+
"x": 0,
|
|
13
|
+
"y": 0
|
|
14
|
+
},
|
|
15
|
+
"config": {},
|
|
16
|
+
"inputs": {}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "subwf-call",
|
|
20
|
+
"typeId": "core.subWorkflow",
|
|
21
|
+
"name": "Invoke Child",
|
|
22
|
+
"position": {
|
|
23
|
+
"x": 200,
|
|
24
|
+
"y": 0
|
|
25
|
+
},
|
|
26
|
+
"config": {
|
|
27
|
+
"workflowId": "conformance-error-route-only",
|
|
28
|
+
"waitForCompletion": true,
|
|
29
|
+
"onChildFailure": "fail-parent"
|
|
30
|
+
},
|
|
31
|
+
"inputs": {}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "parent-end",
|
|
35
|
+
"typeId": "core.noop",
|
|
36
|
+
"name": "Parent End",
|
|
37
|
+
"position": {
|
|
38
|
+
"x": 400,
|
|
39
|
+
"y": 0
|
|
40
|
+
},
|
|
41
|
+
"config": {},
|
|
42
|
+
"inputs": {}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"edges": [
|
|
46
|
+
{
|
|
47
|
+
"id": "ep1",
|
|
48
|
+
"sourceNodeId": "parent-start",
|
|
49
|
+
"targetNodeId": "subwf-call"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "ep2",
|
|
53
|
+
"sourceNodeId": "subwf-call",
|
|
54
|
+
"targetNodeId": "parent-end"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"triggers": [
|
|
58
|
+
{
|
|
59
|
+
"id": "manual",
|
|
60
|
+
"type": "manual",
|
|
61
|
+
"enabled": true
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"variables": [],
|
|
65
|
+
"metadata": {
|
|
66
|
+
"tags": [
|
|
67
|
+
"conformance",
|
|
68
|
+
"subworkflow",
|
|
69
|
+
"skipped-terminals"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"settings": {
|
|
73
|
+
"timeout": 60000,
|
|
74
|
+
"maxRetries": 0
|
|
75
|
+
}
|
|
76
|
+
}
|
package/fixtures.md
CHANGED
|
@@ -52,6 +52,9 @@ All fixtures MUST advertise:
|
|
|
52
52
|
| Approval (refine) | `conformance-approval-refine` | RFC 0183 — refine resolution carries action + refineFeedback |
|
|
53
53
|
| Approval (edit-accept) | `conformance-approval-edit-accept` | RFC 0183 — edit-accept resolution carries action + editedArtifactData |
|
|
54
54
|
| Approval (reject routed) | `conformance-approval-reject-routed` | RFC 0223 G1 — a rejected gate is routed over an `any_failed` edge; its `all_success` sibling never runs | `completed` after reject | unbounded (suspends) |
|
|
55
|
+
| Error route only | `conformance-error-route-only` | RFC 0257 — the only terminal sits behind `any_failed`; it is skipped with no condition anywhere | `completed` with `skippedTerminals` | < 1s |
|
|
56
|
+
| All terminals skipped | `conformance-all-terminals-skipped` | RFC 0257 — a `truthy` edge on `payload.ok` declines (`{ok:false}`) or fires (`{ok:true}`) | `completed`, with or without `skippedTerminals` | < 1s |
|
|
57
|
+
| Sub-workflow (child reaches no terminal) | `conformance-subworkflow-skip-parent` | RFC 0257 §B — a `core.subWorkflow` child completes with `skippedTerminals`; the parent continues | `completed` | < 2s |
|
|
55
58
|
| Approval (timeout) | `conformance-approval-timeout` | RFC 0223 G2 — a 1500 ms `timeoutMs`, no `onTimeout`: the host's timer resolves the gate rejected | `failed` (`approval_rejected`) after the timeout | ~1.5 s + the host's sweep |
|
|
56
59
|
| Approval (timeout, onTimeout approve) | `conformance-approval-timeout-approve` | openwop#1696 — a 1500 ms `timeoutMs` with `onTimeout: approve`: a timeout never grants, so the gate still resolves rejected | `failed` (`approval_rejected`) after the timeout | ~1.5 s + the host's sweep |
|
|
57
60
|
| Approval (reject loops back) | `conformance-approval-reject-loopback` | openwop#1697 — a rejected gate routed back to itself through `revise` is asked again under a new `key` | `waiting-approval` again after one reject (the suite cancels) | unbounded (suspends) |
|
|
@@ -284,6 +287,37 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
|
|
|
284
287
|
- **Terminal status (after reject)**: `completed`.
|
|
285
288
|
- **Why it is separate from `conformance-approval`**: that fixture has no edges, and giving it some would force every host to re-register a registered definition.
|
|
286
289
|
|
|
290
|
+
### `conformance-error-route-only`
|
|
291
|
+
|
|
292
|
+
- **Purpose**: witness RFC 0257 on a graph with no edge condition (`run-event-payloads.schema.json` `$defs/runCompleted.skippedTerminals`).
|
|
293
|
+
- **Inputs**: none.
|
|
294
|
+
- **Behavior**:
|
|
295
|
+
1. `start` (`core.noop`) MUST run to `node.completed`.
|
|
296
|
+
2. `on-fail` (incoming edge `triggerRule: any_failed`) MUST NOT run. It is the only terminal.
|
|
297
|
+
3. The run MUST end `run.completed` with `skippedTerminals` `["on-fail"]`, and the log MUST NOT carry `run.failed`.
|
|
298
|
+
- **Terminal status**: `completed`.
|
|
299
|
+
|
|
300
|
+
### `conformance-all-terminals-skipped`
|
|
301
|
+
|
|
302
|
+
- **Purpose**: witness RFC 0257 on a declining edge condition, with a positive control.
|
|
303
|
+
- **Inputs**: `payload` (object, required) — `{ok: boolean}`.
|
|
304
|
+
- **Behavior**:
|
|
305
|
+
1. `check` (`core.identity`) echoes `payload`; its `node.completed` `outputs` is `{payload}`.
|
|
306
|
+
2. The edge to `act` carries `{type: "truthy", left: "payload.ok"}`, resolved as a dot path into `check`'s `outputs`.
|
|
307
|
+
3. With `{ok: false}`, `act` MUST NOT run and the run MUST end `run.completed` with `skippedTerminals` `["act"]`.
|
|
308
|
+
4. With `{ok: true}`, `act` MUST run and the run MUST end `run.completed` with no `skippedTerminals`.
|
|
309
|
+
- **Terminal status**: `completed` on both inputs.
|
|
310
|
+
|
|
311
|
+
### `conformance-subworkflow-skip-parent`
|
|
312
|
+
|
|
313
|
+
- **Purpose**: witness RFC 0257 §B (`spec/v2/core/execution.md` §`subWorkflow`, Output).
|
|
314
|
+
- **Inputs**: none. **Requires** `conformance-error-route-only` registered as the child.
|
|
315
|
+
- **Behavior**:
|
|
316
|
+
1. `subwf-call` (`core.subWorkflow`, `onChildFailure: fail-parent`) dispatches the child, which ends `run.completed` with `skippedTerminals` `["on-fail"]`.
|
|
317
|
+
2. `subwf-call` MUST emit `node.completed` with `outputs.childStatus` `"completed"` and `outputs.childSkippedTerminals` `["on-fail"]`.
|
|
318
|
+
3. `parent-end` runs, and the parent ends `run.completed`.
|
|
319
|
+
- **Terminal status**: `completed`.
|
|
320
|
+
|
|
287
321
|
### `conformance-approval-timeout`
|
|
288
322
|
|
|
289
323
|
- **Purpose**: witness the timeout disposition (RFC 0223 §A, gap G2).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.46.
|
|
3
|
+
"version": "2.46.42",
|
|
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.42"
|
|
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": 749,
|
|
6
|
+
"tests": 2874,
|
|
7
|
+
"withStableId": 2874,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2744
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -28028,6 +28028,71 @@
|
|
|
28028
28028
|
}
|
|
28029
28029
|
]
|
|
28030
28030
|
},
|
|
28031
|
+
{
|
|
28032
|
+
"id": "openwop.it.v2-all-terminals-skipped.skippedterminals-is-an-optional-non-empty-unique-nodeid-list-on-run-completed",
|
|
28033
|
+
"file": "v2-all-terminals-skipped.test.ts",
|
|
28034
|
+
"line": 81,
|
|
28035
|
+
"title": "skippedTerminals is an optional, non-empty, unique nodeId list on run.completed",
|
|
28036
|
+
"explicitId": "openwop.requirement.0257.shape",
|
|
28037
|
+
"citations": [
|
|
28038
|
+
{
|
|
28039
|
+
"section": "run-event-payloads.schema.json $defs/runCompleted.skippedTerminals; execution.md §subWorkflow (RFC 0257)",
|
|
28040
|
+
"requirement": "run.completed without skippedTerminals MUST validate"
|
|
28041
|
+
},
|
|
28042
|
+
{
|
|
28043
|
+
"section": "run-event-payloads.schema.json $defs/runCompleted.skippedTerminals; execution.md §subWorkflow (RFC 0257)",
|
|
28044
|
+
"requirement": "run.completed with skippedTerminals MUST validate"
|
|
28045
|
+
},
|
|
28046
|
+
{
|
|
28047
|
+
"section": "run-event-payloads.schema.json $defs/runCompleted.skippedTerminals; execution.md §subWorkflow (RFC 0257)",
|
|
28048
|
+
"requirement": "an empty skippedTerminals MUST fail (minItems 1)"
|
|
28049
|
+
},
|
|
28050
|
+
{
|
|
28051
|
+
"section": "run-event-payloads.schema.json $defs/runCompleted.skippedTerminals; execution.md §subWorkflow (RFC 0257)",
|
|
28052
|
+
"requirement": "a repeated id MUST fail (uniqueItems)"
|
|
28053
|
+
}
|
|
28054
|
+
]
|
|
28055
|
+
},
|
|
28056
|
+
{
|
|
28057
|
+
"id": "openwop.it.v2-all-terminals-skipped.a-terminal-skipped-by-its-trigger-rule-with-no-condition-ends-run-completed-with",
|
|
28058
|
+
"file": "v2-all-terminals-skipped.test.ts",
|
|
28059
|
+
"line": 91,
|
|
28060
|
+
"title": "a terminal skipped by its trigger rule, with no condition, ends run.completed with skippedTerminals",
|
|
28061
|
+
"explicitId": "openwop.requirement.0257.trigger-rule-skip-completes",
|
|
28062
|
+
"citations": []
|
|
28063
|
+
},
|
|
28064
|
+
{
|
|
28065
|
+
"id": "openwop.it.v2-all-terminals-skipped.a-terminal-skipped-by-a-declining-edge-condition-ends-run-completed-with-skipped",
|
|
28066
|
+
"file": "v2-all-terminals-skipped.test.ts",
|
|
28067
|
+
"line": 98,
|
|
28068
|
+
"title": "a terminal skipped by a declining edge condition ends run.completed with skippedTerminals",
|
|
28069
|
+
"explicitId": "openwop.requirement.0257.condition-skip-completes",
|
|
28070
|
+
"citations": []
|
|
28071
|
+
},
|
|
28072
|
+
{
|
|
28073
|
+
"id": "openwop.it.v2-all-terminals-skipped.a-run-in-which-a-terminal-completed-carries-no-skippedterminals-the-positive-con",
|
|
28074
|
+
"file": "v2-all-terminals-skipped.test.ts",
|
|
28075
|
+
"line": 105,
|
|
28076
|
+
"title": "a run in which a terminal completed carries no skippedTerminals (the positive control)",
|
|
28077
|
+
"explicitId": "openwop.requirement.0257.terminal-completed-no-marker",
|
|
28078
|
+
"citations": []
|
|
28079
|
+
},
|
|
28080
|
+
{
|
|
28081
|
+
"id": "openwop.it.v2-all-terminals-skipped.a-replay-fork-reproduces-the-terminal-event-and-skippedterminals",
|
|
28082
|
+
"file": "v2-all-terminals-skipped.test.ts",
|
|
28083
|
+
"line": 112,
|
|
28084
|
+
"title": "a replay fork reproduces the terminal event and skippedTerminals",
|
|
28085
|
+
"explicitId": "openwop.requirement.0257.replay-same-disposition",
|
|
28086
|
+
"citations": []
|
|
28087
|
+
},
|
|
28088
|
+
{
|
|
28089
|
+
"id": "openwop.it.v2-all-terminals-skipped.a-core-subworkflow-parent-completes-on-a-child-that-reached-no-terminal-and-carr",
|
|
28090
|
+
"file": "v2-all-terminals-skipped.test.ts",
|
|
28091
|
+
"line": 129,
|
|
28092
|
+
"title": "a core.subWorkflow parent completes on a child that reached no terminal, and carries its list",
|
|
28093
|
+
"explicitId": "openwop.requirement.0257.subworkflow-child-skipped",
|
|
28094
|
+
"citations": []
|
|
28095
|
+
},
|
|
28031
28096
|
{
|
|
28032
28097
|
"id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-with-no-routing-edge-fails-the-run-with-approval-rejected",
|
|
28033
28098
|
"file": "v2-approval-reject-disposition.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": 666,
|
|
5
5
|
"v1": 455,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 226
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1308,6 +1308,9 @@
|
|
|
1308
1308
|
"v2-ai-providers-advertisement.test.ts": [
|
|
1309
1309
|
2
|
|
1310
1310
|
],
|
|
1311
|
+
"v2-all-terminals-skipped.test.ts": [
|
|
1312
|
+
2
|
|
1313
|
+
],
|
|
1311
1314
|
"v2-approval-reject-disposition.test.ts": [
|
|
1312
1315
|
2
|
|
1313
1316
|
],
|
|
@@ -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.42",
|
|
5
|
+
"corpusTag": "v2.46.42",
|
|
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": "3cc08eb5617a000616d002aeccbbf464cb3eb5ebb5ee1c633c4e0a119d7e56ec",
|
|
13
|
+
"api/v2/asyncapi.yaml": "35ff697a0a8ff45407b764f1322c0ade27204b8dbec9791f5ec560ca3fcd75d6",
|
|
14
|
+
"api/v2/openapi.yaml": "ed36faabd190c2a4f966340d2fa219c1891f7fbaebf1a1b2e895b5caf209e56e",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
"schemas/v2/residency.schema.json": "3fd23c9732beb140504e81560fc426eb6764d547e56da5fb92c5a5c7bf04db3a",
|
|
170
170
|
"schemas/v2/run-ancestry-response.schema.json": "334c17f2062dea62726ca1d07b8be3f169cdc422af843c1def5fb317039814c9",
|
|
171
171
|
"schemas/v2/run-diff-response.schema.json": "8bdb2045c274580038bd74781f7d062f5049839797c164a89de4ed89edc53a74",
|
|
172
|
-
"schemas/v2/run-event-payloads.schema.json": "
|
|
172
|
+
"schemas/v2/run-event-payloads.schema.json": "4a1dca1a62b1bb4a02a785072d2f4a94df87dbf56857a21c7efde6accc47ff28",
|
|
173
173
|
"schemas/v2/run-event.schema.json": "2f0d7416bd376c56affe97b6b2b49981532e8e9ff45c0a162cf806c78b3e7fb7",
|
|
174
174
|
"schemas/v2/run-list-response.schema.json": "c2dbc34768dcf64528914a213aa3cb55952e8e3e90a64b04a9423b2a19749d38",
|
|
175
175
|
"schemas/v2/run-options.schema.json": "f0e9f494d80086c2ed2cf9fe87df54e5b4e66601699c4a1089f4edbc7ba58867",
|
|
@@ -191,8 +191,8 @@
|
|
|
191
191
|
"schemas/v2/webhook-dead-letter-page.schema.json": "78f3284722c2b42d52ff8a1e6d9b9092ba1ac35c2dbe036f3ff2470b34086d1e",
|
|
192
192
|
"schemas/v2/webhook-delivery.schema.json": "5f5361ede12098b5c2d685f0aa70fcb07219fd6b68c6ad6af393ca8dd266d56a",
|
|
193
193
|
"schemas/v2/webhook-verification.schema.json": "76e4e0d81143ffd2649bc7adba2aa34f2525e2c4e9f0300a425d908b3163196b",
|
|
194
|
-
"schemas/v2/workflow-chain-pack-manifest.schema.json": "
|
|
195
|
-
"schemas/v2/workflow-definition.schema.json": "
|
|
194
|
+
"schemas/v2/workflow-chain-pack-manifest.schema.json": "ecd69cbe0aaab11d01e2099cf875df93e306b18578b17fa1d7053c294722160a",
|
|
195
|
+
"schemas/v2/workflow-definition.schema.json": "6e28c45580d23a5d01a14a52ddf74f916225bc199390c29ceb7b1d41fdd09aa9",
|
|
196
196
|
"schemas/v2/workload-identity.schema.json": "1f589debfac6d70e0908da511550ec69a27b4bcc8314551cd843b6d923605efc",
|
|
197
197
|
"schemas/v2/workspace-file-create.schema.json": "b6b5260857318c3b67de0231ed15e3a746f525880a763e7b36a7854178dd10d0",
|
|
198
198
|
"schemas/v2/workspace-file.schema.json": "d2ed39e8c229dde33b56905c8df6f8b2ff056c97e1c5f615e3124baa54a6254f",
|
|
@@ -203,14 +203,14 @@
|
|
|
203
203
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
204
204
|
"spec/v1/alias-detectors.json": "202a443678e592a515e33aad7f6ab679a07d8408809f2d4b0f59d156c84ffc65",
|
|
205
205
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
206
|
-
"spec/v1/core-standard-manifest.json": "
|
|
206
|
+
"spec/v1/core-standard-manifest.json": "affd665ea0bd4be705e910e42193a9de94edc2520c6dcc8dad7ea5aa10a0aea2",
|
|
207
207
|
"spec/v1/deprecations.json": "901fdab0a7212627f1838cbb06ae7b7204f0dff528299e56d3d938deecea27c1",
|
|
208
208
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
209
209
|
"spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
211
211
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
212
212
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
213
|
-
"spec/v1/gaps.json": "
|
|
213
|
+
"spec/v1/gaps.json": "7fb24588b5526d0d7b6105102029373593d986c2177c5a85dfc8f0c29ce69c07",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
215
|
"spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
|
|
216
216
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
"spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
|
|
225
225
|
"spec/v2/core/errors.md": "0ba4fa59f0f3dc025d43147135dabede6c19d583677b761eafe693fc2169352c",
|
|
226
226
|
"spec/v2/core/events.md": "5834308616f2e722e217db776edea37338c283196b6d2a7c241256575ccca825",
|
|
227
|
-
"spec/v2/core/execution.md": "
|
|
227
|
+
"spec/v2/core/execution.md": "3c059a5886a4a624d993ea1be2395277d9eea4ee9a39d794d1d4c51a0090c708",
|
|
228
228
|
"spec/v2/core/form-content-packs.md": "d56022c207c8406fed4b2e9501bd14e70a4300e371c0cfc371787899ae0bd997",
|
|
229
229
|
"spec/v2/core/headers.md": "71f5024adfa754789cf751001308baa87decff6a3afd514c75d573674cbf50ee",
|
|
230
230
|
"spec/v2/core/host-services.md": "68fbe359ec9efc453105a225998b8d0dada07ae61bfd05e324dd8649af31261d",
|
|
@@ -305,9 +305,9 @@
|
|
|
305
305
|
"spec/v2/path-manifest.json": "3720e502614f301efc7556e0bfcbfa7b7ad8a37c579256ba6bca82417a2728f1",
|
|
306
306
|
"spec/v2/peer-dependency-aliases.json": "cbf62f551f9295b0ac7ba6cc0466abb153563354a57b0622f3ec6e6689cba47e",
|
|
307
307
|
"spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
|
|
308
|
-
"spec/v2/release.json": "
|
|
308
|
+
"spec/v2/release.json": "fd0be285b05d95e06af11bc6d8c3615c58f0bf8f91a06b0829a9c649a1300875",
|
|
309
309
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
310
|
-
"spec/v2/surface-baseline.json": "
|
|
310
|
+
"spec/v2/surface-baseline.json": "25d5ab5877a70cfa817c9fa1c1d5bed638b021e07bdf5c1d41a5124d3176c3e9"
|
|
311
311
|
},
|
|
312
|
-
"corpusCommit": "
|
|
312
|
+
"corpusCommit": "a3cc5c1b3896e1bc1c56480ed6460dc7f9070b4c"
|
|
313
313
|
}
|
|
@@ -39,7 +39,11 @@ export function packMatches(served: unknown, artifact: unknown): boolean {
|
|
|
39
39
|
|
|
40
40
|
const ENGINES = { openwop: '>=2.0 <3.0.0' };
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* A fresh one-chain `private.*` chain pack the host has never seen. Its node
|
|
44
|
+
* reference is exact-pinned: `workflow-chain-packs.md` §Exact pins makes a host
|
|
45
|
+
* refuse a chain whose reference carries no version.
|
|
46
|
+
*/
|
|
43
47
|
export function freshChainPack(nonce: string, description: string): Rec {
|
|
44
48
|
const name = `private.conformance.c${nonce}`;
|
|
45
49
|
return {
|
|
@@ -47,7 +51,7 @@ export function freshChainPack(nonce: string, description: string): Rec {
|
|
|
47
51
|
chains: [{
|
|
48
52
|
chainId: `${name}.passthrough`, version: '1.0.0', label: 'Pass through', description,
|
|
49
53
|
parameters: { type: 'object', properties: {} },
|
|
50
|
-
dag: { nodes: [{ id: 'n1', typeId: 'core.noop' }] },
|
|
54
|
+
dag: { nodes: [{ id: 'n1', typeId: 'core.noop@1.0.0' }] },
|
|
51
55
|
}],
|
|
52
56
|
};
|
|
53
57
|
}
|
|
@@ -80,6 +84,14 @@ export function packContentViolations(kind: PackKind, pack: Rec): string[] {
|
|
|
80
84
|
const out: string[] = [];
|
|
81
85
|
if (typeof pack['name'] !== 'string' || !pack['name'].startsWith('private.')) out.push('name outside private.*');
|
|
82
86
|
if ('signing' in pack) out.push('carries signing');
|
|
87
|
+
if (kind === 'workflow-chain-pack') {
|
|
88
|
+
for (const c of Array.isArray(pack['chains']) ? pack['chains'].filter(isRec) : []) {
|
|
89
|
+
const dag = isRec(c['dag']) ? c['dag'] : {};
|
|
90
|
+
for (const n of Array.isArray(dag['nodes']) ? dag['nodes'].filter(isRec) : []) {
|
|
91
|
+
if (typeof n['typeId'] !== 'string' || !/@\d+\.\d+\.\d+$/.test(n['typeId'])) out.push(`${String(c['chainId'])}: node ${String(n['id'])} is not exact-pinned`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
83
95
|
if (kind === 'agent-pack') {
|
|
84
96
|
if (Array.isArray(pack['nodes']) && pack['nodes'].length > 0) out.push('carries nodes');
|
|
85
97
|
const rt = isRec(pack['runtime']) ? pack['runtime'] : {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0257 — pure judges for `v2-all-terminals-skipped.test.ts`. The scenario
|
|
3
|
+
* runs advertised fixtures and records each run's event log; these functions
|
|
4
|
+
* decide each leg from those logs, so the self-tests can fail every wrong host
|
|
5
|
+
* answer without a host.
|
|
6
|
+
*
|
|
7
|
+
* The rules are `$defs/runCompleted.skippedTerminals` in
|
|
8
|
+
* `schemas/v2/run-event-payloads.schema.json` and `spec/v2/core/execution.md`
|
|
9
|
+
* §`subWorkflow` (Output).
|
|
10
|
+
*
|
|
11
|
+
* @see RFCS/0257-run-status-when-every-terminal-is-skipped.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface SkipFinding { readonly ok: boolean; readonly message: string }
|
|
15
|
+
|
|
16
|
+
type Rec = Record<string, unknown>;
|
|
17
|
+
export interface Ev { readonly type?: unknown; readonly nodeId?: unknown; readonly sequence?: unknown; readonly payload?: unknown }
|
|
18
|
+
|
|
19
|
+
const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
20
|
+
const f = (ok: boolean, message: string): SkipFinding => ({ ok, message });
|
|
21
|
+
const TERMINAL = new Set(['run.completed', 'run.failed', 'run.cancelled']);
|
|
22
|
+
const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
|
|
23
|
+
|
|
24
|
+
/** The run's terminal event, if the log has exactly the one the core requires. */
|
|
25
|
+
export function terminalOf(log: readonly Ev[]): Ev | null {
|
|
26
|
+
const t = log.filter((e) => typeof e.type === 'string' && TERMINAL.has(e.type));
|
|
27
|
+
return t.length === 1 ? (t[0] ?? null) : null;
|
|
28
|
+
}
|
|
29
|
+
const payloadOf = (e: Ev | null): Rec => (e !== null && isRec(e.payload) ? e.payload : {});
|
|
30
|
+
const shownType = (e: Ev | null): string => (e === null ? 'no single terminal event' : String(e.type));
|
|
31
|
+
|
|
32
|
+
/** No node failed and every terminal was skipped: `run.completed` naming exactly `expected`. */
|
|
33
|
+
export function judgeSkippedCompletes(log: readonly Ev[], expected: readonly string[]): SkipFinding[] {
|
|
34
|
+
const t = terminalOf(log);
|
|
35
|
+
const p = payloadOf(t);
|
|
36
|
+
return [
|
|
37
|
+
f(t?.type === 'run.completed', `a run with no failed node and every terminal skipped MUST end run.completed (got ${shownType(t)})`),
|
|
38
|
+
f(!log.some((e) => e.type === 'run.failed'), 'the log MUST NOT carry run.failed for this state'),
|
|
39
|
+
f(same(p['skippedTerminals'], expected), `run.completed MUST carry skippedTerminals ${JSON.stringify(expected)}: every terminal once, in nodes[] order (got ${JSON.stringify(p['skippedTerminals'])})`),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A terminal completed: `run.completed` with no `skippedTerminals`. */
|
|
44
|
+
export function judgeNoMarker(log: readonly Ev[]): SkipFinding[] {
|
|
45
|
+
const t = terminalOf(log);
|
|
46
|
+
return [
|
|
47
|
+
f(t?.type === 'run.completed', `the positive control MUST end run.completed (got ${shownType(t)})`),
|
|
48
|
+
f(t !== null && !('skippedTerminals' in payloadOf(t)), 'a run.completed in which a terminal completed MUST NOT carry skippedTerminals'),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A replay fork reproduces the source's terminal event type and marker. */
|
|
53
|
+
export function judgeReplaySame(source: readonly Ev[], fork: readonly Ev[]): SkipFinding[] {
|
|
54
|
+
const s = terminalOf(source);
|
|
55
|
+
const k = terminalOf(fork);
|
|
56
|
+
return [
|
|
57
|
+
f(k !== null && k.type === s?.type, `the replay fork MUST end with the source's terminal event (${shownType(s)}; got ${shownType(k)})`),
|
|
58
|
+
f(k !== null && same(payloadOf(k)['skippedTerminals'], payloadOf(s)['skippedTerminals']), `the replay fork MUST carry the source's skippedTerminals ${JSON.stringify(payloadOf(s)['skippedTerminals'])} (got ${JSON.stringify(payloadOf(k)['skippedTerminals'])})`),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The parent's `core.subWorkflow` node completes with `childStatus` and `childSkippedTerminals`. */
|
|
63
|
+
export function judgeParent(log: readonly Ev[], subNodeId: string, expected: readonly string[]): SkipFinding[] {
|
|
64
|
+
const t = terminalOf(log);
|
|
65
|
+
const done = log.find((e) => e.type === 'node.completed' && (e.nodeId ?? payloadOf(e)['nodeId']) === subNodeId) ?? null;
|
|
66
|
+
const out = isRec(payloadOf(done)['outputs']) ? (payloadOf(done)['outputs'] as Rec) : {};
|
|
67
|
+
return [
|
|
68
|
+
f(t?.type === 'run.completed', `a parent whose child completed with no terminal MUST NOT fail on it (got ${shownType(t)})`),
|
|
69
|
+
f(done !== null, `the core.subWorkflow node ${subNodeId} MUST emit node.completed`),
|
|
70
|
+
f(out['childStatus'] === 'completed', `outputs.childStatus MUST be "completed" (got ${JSON.stringify(out['childStatus'])})`),
|
|
71
|
+
f(same(out['childSkippedTerminals'], expected), `outputs.childSkippedTerminals MUST equal the child's skippedTerminals ${JSON.stringify(expected)} (got ${JSON.stringify(out['childSkippedTerminals'])})`),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — a run in which no node failed and every terminal node was skipped ends
|
|
3
|
+
* `run.completed` with `skippedTerminals` (RFC 0257; the rules are
|
|
4
|
+
* `$defs/runCompleted.skippedTerminals` in
|
|
5
|
+
* `schemas/v2/run-event-payloads.schema.json` and `spec/v2/core/execution.md`
|
|
6
|
+
* §`subWorkflow`, Output).
|
|
7
|
+
*
|
|
8
|
+
* Server-free (always on):
|
|
9
|
+
* 1. `0257.shape` — `skippedTerminals` is an optional nodeId list with
|
|
10
|
+
* `minItems: 1` and `uniqueItems`; an empty list fails validation.
|
|
11
|
+
*
|
|
12
|
+
* Host legs — each gated on its fixture in discovery `fixtures[]`
|
|
13
|
+
* (unadvertised → `inapplicable`); judges in `lib/skipped-terminals-witness.ts`.
|
|
14
|
+
* 2. `0257.trigger-rule-skip-completes` — `conformance-error-route-only`:
|
|
15
|
+
* the only terminal sits behind `any_failed`, no condition anywhere.
|
|
16
|
+
* 3. `0257.condition-skip-completes` — `conformance-all-terminals-skipped`
|
|
17
|
+
* with `payload {ok: false}`: the `truthy` edge on `payload.ok` declines.
|
|
18
|
+
* 4. `0257.terminal-completed-no-marker` — the same fixture with
|
|
19
|
+
* `{ok: true}`: `act` completes and there is no marker. A host that
|
|
20
|
+
* ignores the condition runs `act` on both inputs and fails leg 3.
|
|
21
|
+
* 5. `0257.replay-same-disposition` — gated on `replay.modes ∋ replay`: a
|
|
22
|
+
* replay fork of the leg-2 run ends with the same event and marker.
|
|
23
|
+
* 6. `0257.subworkflow-child-skipped` — gated on `subWorkflow`:
|
|
24
|
+
* `conformance-subworkflow-skip-parent` completes, and its
|
|
25
|
+
* `core.subWorkflow` node carries `childStatus: "completed"` and
|
|
26
|
+
* `childSkippedTerminals`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
31
|
+
import { gateFamily, v2RefValidator } from '../lib/v2.js';
|
|
32
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
33
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
34
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
35
|
+
import { req } from '../lib/requirement-ids.js';
|
|
36
|
+
import { scaledTimeoutMs } from '../lib/polling.js';
|
|
37
|
+
import { judgeNoMarker, judgeParent, judgeReplaySame, judgeSkippedCompletes, terminalOf, type Ev, type SkipFinding } from '../lib/skipped-terminals-witness.js';
|
|
38
|
+
|
|
39
|
+
const DOC = 'run-event-payloads.schema.json $defs/runCompleted.skippedTerminals; execution.md §subWorkflow (RFC 0257)';
|
|
40
|
+
const ID_SHAPE = 'openwop.requirement.0257.shape';
|
|
41
|
+
const ID_TRIGGER_RULE = 'openwop.requirement.0257.trigger-rule-skip-completes';
|
|
42
|
+
const ID_CONDITION = 'openwop.requirement.0257.condition-skip-completes';
|
|
43
|
+
const ID_NO_MARKER = 'openwop.requirement.0257.terminal-completed-no-marker';
|
|
44
|
+
const ID_REPLAY = 'openwop.requirement.0257.replay-same-disposition';
|
|
45
|
+
const ID_SUBWORKFLOW = 'openwop.requirement.0257.subworkflow-child-skipped';
|
|
46
|
+
const ROUTE_ONLY = 'conformance-error-route-only';
|
|
47
|
+
const CONDITION = 'conformance-all-terminals-skipped';
|
|
48
|
+
const PARENT = 'conformance-subworkflow-skip-parent';
|
|
49
|
+
|
|
50
|
+
const enc = (id: string): string => encodeURIComponent(id);
|
|
51
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
|
|
52
|
+
|
|
53
|
+
async function logOf(runId: string): Promise<Ev[]> {
|
|
54
|
+
const res = await http(() => driver.get(`/runs/${enc(runId)}/events/poll?timeout=1`));
|
|
55
|
+
const events = (res?.json as { events?: unknown } | null)?.events;
|
|
56
|
+
return Array.isArray(events) ? (events as Ev[]).filter((e) => typeof e.sequence === 'number').sort((a, b) => (a.sequence as number) - (b.sequence as number)) : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Start a fixture and wait for its terminal event; the log, or why there is none. */
|
|
60
|
+
async function runToEnd(workflowId: string, inputs?: Record<string, unknown>): Promise<{ runId: string; log: Ev[] } | { reason: string }> {
|
|
61
|
+
const created = await http(() => driver.post('/runs', { workflowId, ...(inputs ? { inputs } : {}) }));
|
|
62
|
+
const runId = (created?.json as { runId?: unknown } | null)?.runId;
|
|
63
|
+
if (created === null || created.status !== 201 || typeof runId !== 'string') return { reason: `POST /runs (${workflowId}) answered ${created?.status ?? 'nothing'} ${readErrorCode(created?.json) ?? ''}`.trim() };
|
|
64
|
+
return settle(runId);
|
|
65
|
+
}
|
|
66
|
+
async function settle(runId: string): Promise<{ runId: string; log: Ev[] } | { reason: string }> {
|
|
67
|
+
const deadline = Date.now() + scaledTimeoutMs(20_000);
|
|
68
|
+
for (;;) {
|
|
69
|
+
const log = await logOf(runId);
|
|
70
|
+
if (log.some((e) => typeof e.type === 'string' && /^run\.(completed|failed|cancelled)$/.test(e.type))) return { runId, log };
|
|
71
|
+
if (Date.now() > deadline) return { reason: `run ${runId} logged no terminal event within the bound (${log.length} event(s))` };
|
|
72
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function expectAll(id: string, findings: readonly SkipFinding[]): void {
|
|
77
|
+
for (const x of findings) expect(x.ok, req(id, DOC, x.message)).toBe(true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('v2 RFC 0257 — skippedTerminals wire shape (server-free)', () => {
|
|
81
|
+
it('skippedTerminals is an optional, non-empty, unique nodeId list on run.completed', () => {
|
|
82
|
+
const v = v2RefValidator('run-event-payloads.schema.json#/$defs/runCompleted');
|
|
83
|
+
expect(v({ outputs: {} }).ok, req(ID_SHAPE, DOC, 'run.completed without skippedTerminals MUST validate')).toBe(true);
|
|
84
|
+
expect(v({ outputs: {}, skippedTerminals: ['session'] }).ok, req(ID_SHAPE, DOC, 'run.completed with skippedTerminals MUST validate')).toBe(true);
|
|
85
|
+
expect(v({ outputs: {}, skippedTerminals: [] }).ok, req(ID_SHAPE, DOC, 'an empty skippedTerminals MUST fail (minItems 1)')).toBe(false);
|
|
86
|
+
expect(v({ outputs: {}, skippedTerminals: ['a', 'a'] }).ok, req(ID_SHAPE, DOC, 'a repeated id MUST fail (uniqueItems)')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('v2 RFC 0257 — every terminal skipped, no node failed (host)', () => {
|
|
91
|
+
it('a terminal skipped by its trigger rule, with no condition, ends run.completed with skippedTerminals', async () => {
|
|
92
|
+
if (!isFixtureAdvertised(ROUTE_ONLY)) return softSkip('inapplicable', `fixture ${ROUTE_ONLY} is not advertised`);
|
|
93
|
+
const r = await runToEnd(ROUTE_ONLY);
|
|
94
|
+
if ('reason' in r) return softSkip('blocked', r.reason);
|
|
95
|
+
expectAll(ID_TRIGGER_RULE, judgeSkippedCompletes(r.log, ['on-fail']));
|
|
96
|
+
}, 60_000);
|
|
97
|
+
|
|
98
|
+
it('a terminal skipped by a declining edge condition ends run.completed with skippedTerminals', async () => {
|
|
99
|
+
if (!isFixtureAdvertised(CONDITION)) return softSkip('inapplicable', `fixture ${CONDITION} is not advertised`);
|
|
100
|
+
const r = await runToEnd(CONDITION, { payload: { ok: false } });
|
|
101
|
+
if ('reason' in r) return softSkip('blocked', r.reason);
|
|
102
|
+
expectAll(ID_CONDITION, judgeSkippedCompletes(r.log, ['act']));
|
|
103
|
+
}, 60_000);
|
|
104
|
+
|
|
105
|
+
it('a run in which a terminal completed carries no skippedTerminals (the positive control)', async () => {
|
|
106
|
+
if (!isFixtureAdvertised(CONDITION)) return softSkip('inapplicable', `fixture ${CONDITION} is not advertised`);
|
|
107
|
+
const r = await runToEnd(CONDITION, { payload: { ok: true } });
|
|
108
|
+
if ('reason' in r) return softSkip('blocked', r.reason);
|
|
109
|
+
expectAll(ID_NO_MARKER, judgeNoMarker(r.log));
|
|
110
|
+
}, 60_000);
|
|
111
|
+
|
|
112
|
+
it('a replay fork reproduces the terminal event and skippedTerminals', async () => {
|
|
113
|
+
const replay = await gateFamily('replay');
|
|
114
|
+
if (!replay) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay)');
|
|
115
|
+
if (!(Array.isArray(replay['modes']) && replay['modes'].includes('replay'))) return softSkip('inapplicable', 'replay.modes does not list replay');
|
|
116
|
+
if (!isFixtureAdvertised(ROUTE_ONLY)) return softSkip('inapplicable', `fixture ${ROUTE_ONLY} is not advertised`);
|
|
117
|
+
const src = await runToEnd(ROUTE_ONLY);
|
|
118
|
+
if ('reason' in src) return softSkip('blocked', src.reason);
|
|
119
|
+
const first = src.log.find((e) => e.type === 'node.started');
|
|
120
|
+
if (first === undefined || terminalOf(src.log) === null) return softSkip('blocked', 'the source run logged no node.started or no single terminal event to fork from');
|
|
121
|
+
const fork = await http(() => driver.post(`/runs/${enc(src.runId)}:fork`, { mode: 'replay', fromSeq: first.sequence }));
|
|
122
|
+
const forkId = (fork?.json as { runId?: unknown } | null)?.runId;
|
|
123
|
+
if (fork === null || fork.status !== 201 || typeof forkId !== 'string') return softSkip('blocked', `POST /runs/{runId}:fork answered ${fork?.status ?? 'nothing'} ${readErrorCode(fork?.json) ?? ''} — forkRun owns that contract`.trim());
|
|
124
|
+
const k = await settle(forkId);
|
|
125
|
+
if ('reason' in k) return softSkip('blocked', k.reason);
|
|
126
|
+
expectAll(ID_REPLAY, judgeReplaySame(src.log, k.log));
|
|
127
|
+
}, 90_000);
|
|
128
|
+
|
|
129
|
+
it('a core.subWorkflow parent completes on a child that reached no terminal, and carries its list', async () => {
|
|
130
|
+
if (!(await gateFamily('subWorkflow'))) return softSkip('inapplicable', 'subWorkflow family not advertised (gate recorded under openwop.family.subWorkflow)');
|
|
131
|
+
if (!isFixtureAdvertised(PARENT) || !isFixtureAdvertised(ROUTE_ONLY)) return softSkip('inapplicable', `fixtures ${PARENT} and ${ROUTE_ONLY} are not both advertised`);
|
|
132
|
+
const r = await runToEnd(PARENT);
|
|
133
|
+
if ('reason' in r) return softSkip('blocked', r.reason);
|
|
134
|
+
expectAll(ID_SUBWORKFLOW, judgeParent(r.log, 'subwf-call', ['on-fail']));
|
|
135
|
+
}, 90_000);
|
|
136
|
+
});
|