@openwop/openwop-conformance 2.0.0-rc.52 → 2.0.0-rc.53
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@openwop/spec-artifacts",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
4
|
-
"stampSha256": "
|
|
3
|
+
"version": "2.0.0-rc.53",
|
|
4
|
+
"stampSha256": "c922f572cdf72529a1d3daca8a6c16b5c4a2e12165f5c7cee3b04638912f414c"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.53",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
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.0.0-rc.
|
|
59
|
+
"@openwop/spec-artifacts": "2.0.0-rc.53"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirement-aliases.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "old id → new id. Add a row when a test title is reworded so bundles citing the old id still resolve.",
|
|
3
3
|
"aliases": {
|
|
4
|
-
"openwop.it.v2-run-cancel.cancelling-a-terminal-run-answers-409-run-terminal-or-200-idempotently-the-prose": "openwop.it.v2-run-cancel.cancelling-a-terminal-run-is-refused-409-run-terminal"
|
|
4
|
+
"openwop.it.v2-run-cancel.cancelling-a-terminal-run-answers-409-run-terminal-or-200-idempotently-the-prose": "openwop.it.v2-run-cancel.cancelling-a-terminal-run-is-refused-409-run-terminal",
|
|
5
|
+
"openwop.it.pause-resume.pause-on-an-already-paused-run-is-a-no-op-200-202-idempotent": "openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless"
|
|
5
6
|
}
|
|
6
7
|
}
|
package/requirements.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
5
|
"files": 564,
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
6
|
+
"tests": 2149,
|
|
7
|
+
"withStableId": 2148,
|
|
8
8
|
"interpolatedTitles": 1,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2058
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -18727,80 +18727,131 @@
|
|
|
18727
18727
|
{
|
|
18728
18728
|
"id": "openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running",
|
|
18729
18729
|
"file": "pause-resume.test.ts",
|
|
18730
|
-
"line":
|
|
18730
|
+
"line": 62,
|
|
18731
18731
|
"title": "pause transitions to paused; resume returns the run to running",
|
|
18732
18732
|
"explicitId": "openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running",
|
|
18733
18733
|
"citations": [
|
|
18734
18734
|
{
|
|
18735
|
-
"section":
|
|
18736
|
-
"requirement": ":pause MUST return 202 on a pausable run"
|
|
18735
|
+
"section": null,
|
|
18736
|
+
"requirement": ":pause MUST return 202 on a pausable run",
|
|
18737
|
+
"interpolated": true
|
|
18737
18738
|
},
|
|
18738
18739
|
{
|
|
18739
|
-
"section":
|
|
18740
|
-
"requirement": ":resume MUST return 202 on a paused run"
|
|
18740
|
+
"section": null,
|
|
18741
|
+
"requirement": ":resume MUST return 202 on a paused run",
|
|
18742
|
+
"interpolated": true
|
|
18741
18743
|
}
|
|
18742
18744
|
]
|
|
18743
18745
|
},
|
|
18744
18746
|
{
|
|
18745
18747
|
"id": "openwop.it.pause-resume.resuming-a-running-not-paused-run-returns-409-with-details-runstatus",
|
|
18746
18748
|
"file": "pause-resume.test.ts",
|
|
18747
|
-
"line":
|
|
18749
|
+
"line": 104,
|
|
18748
18750
|
"title": "resuming a running (not paused) run returns 409 with details.runStatus",
|
|
18749
18751
|
"explicitId": "openwop.it.pause-resume.resuming-a-running-not-paused-run-returns-409-with-details-runstatus",
|
|
18750
18752
|
"citations": [
|
|
18751
18753
|
{
|
|
18752
|
-
"section":
|
|
18753
|
-
"requirement": ":resume on a non-paused run MUST return 409"
|
|
18754
|
+
"section": null,
|
|
18755
|
+
"requirement": ":resume on a non-paused run MUST return 409",
|
|
18756
|
+
"interpolated": true
|
|
18757
|
+
},
|
|
18758
|
+
{
|
|
18759
|
+
"section": null,
|
|
18760
|
+
"requirement": "the 409 MUST carry details.runStatus with the actual state",
|
|
18761
|
+
"interpolated": true
|
|
18754
18762
|
}
|
|
18755
18763
|
]
|
|
18756
18764
|
},
|
|
18757
18765
|
{
|
|
18758
|
-
"id": "openwop.it.pause-resume.pause-on-an-already-paused-run-
|
|
18766
|
+
"id": "openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless",
|
|
18759
18767
|
"file": "pause-resume.test.ts",
|
|
18760
|
-
"line":
|
|
18761
|
-
"title": ":pause on an already-paused run
|
|
18762
|
-
"explicitId": "openwop.it.pause-resume.pause-on-an-already-paused-run-
|
|
18768
|
+
"line": 138,
|
|
18769
|
+
"title": ":pause on an already-paused run returns 409 with details.runStatus paused, unless the request carries the original Idempotency-Key",
|
|
18770
|
+
"explicitId": "openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless",
|
|
18763
18771
|
"citations": [
|
|
18764
18772
|
{
|
|
18765
|
-
"section":
|
|
18766
|
-
"requirement": ":pause
|
|
18773
|
+
"section": null,
|
|
18774
|
+
"requirement": ":pause MUST return 202 on a pausable run",
|
|
18775
|
+
"interpolated": true
|
|
18776
|
+
},
|
|
18777
|
+
{
|
|
18778
|
+
"section": null,
|
|
18779
|
+
"requirement": ":pause on an already-paused run without a matching Idempotency-Key MUST return 409",
|
|
18780
|
+
"interpolated": true
|
|
18781
|
+
},
|
|
18782
|
+
{
|
|
18783
|
+
"section": null,
|
|
18784
|
+
"requirement": "the 409 MUST carry details.runStatus: paused",
|
|
18785
|
+
"interpolated": true
|
|
18786
|
+
},
|
|
18787
|
+
{
|
|
18788
|
+
"section": null,
|
|
18789
|
+
"requirement": ":pause with an Idempotency-Key MUST return 202 on a pausable run",
|
|
18790
|
+
"interpolated": true
|
|
18791
|
+
},
|
|
18792
|
+
{
|
|
18793
|
+
"section": null,
|
|
18794
|
+
"requirement": "a :pause carrying the original Idempotency-Key MUST return 202 with the cached response, not 409",
|
|
18795
|
+
"interpolated": true
|
|
18767
18796
|
}
|
|
18768
18797
|
]
|
|
18769
18798
|
},
|
|
18770
18799
|
{
|
|
18771
18800
|
"id": "openwop.it.pause-resume.pause-on-a-completed-cancelled-failed-run-must-return-409",
|
|
18772
18801
|
"file": "pause-resume.test.ts",
|
|
18773
|
-
"line":
|
|
18802
|
+
"line": 199,
|
|
18774
18803
|
"title": ":pause on a completed/cancelled/failed run MUST return 409",
|
|
18775
18804
|
"explicitId": "openwop.it.pause-resume.pause-on-a-completed-cancelled-failed-run-must-return-409",
|
|
18776
18805
|
"citations": [
|
|
18777
18806
|
{
|
|
18778
|
-
"section":
|
|
18779
|
-
"requirement": ":pause on a terminal run MUST return 409"
|
|
18807
|
+
"section": null,
|
|
18808
|
+
"requirement": ":pause on a terminal run MUST return 409",
|
|
18809
|
+
"interpolated": true
|
|
18810
|
+
}
|
|
18811
|
+
]
|
|
18812
|
+
},
|
|
18813
|
+
{
|
|
18814
|
+
"id": "openwop.it.pause-resume.under-drain-current-node-node-completed-precedes-run-paused-in-the-log",
|
|
18815
|
+
"file": "pause-resume.test.ts",
|
|
18816
|
+
"line": 223,
|
|
18817
|
+
"title": "under drain-current-node node.completed precedes run.paused in the log",
|
|
18818
|
+
"explicitId": "openwop.it.pause-resume.under-drain-current-node-node-completed-precedes-run-paused-in-the-log",
|
|
18819
|
+
"citations": [
|
|
18820
|
+
{
|
|
18821
|
+
"section": null,
|
|
18822
|
+
"requirement": ":pause with drainPolicy drain-current-node MUST return 202 (pause requested)",
|
|
18823
|
+
"interpolated": true
|
|
18824
|
+
},
|
|
18825
|
+
{
|
|
18826
|
+
"section": null,
|
|
18827
|
+
"requirement": null,
|
|
18828
|
+
"interpolated": true
|
|
18780
18829
|
}
|
|
18781
18830
|
]
|
|
18782
18831
|
},
|
|
18783
18832
|
{
|
|
18784
18833
|
"id": "openwop.it.pause-resume.pause-must-not-silently-override-an-active-interrupt-suspend",
|
|
18785
18834
|
"file": "pause-resume.test.ts",
|
|
18786
|
-
"line":
|
|
18835
|
+
"line": 262,
|
|
18787
18836
|
"title": ":pause MUST NOT silently override an active interrupt suspend",
|
|
18788
18837
|
"explicitId": "openwop.it.pause-resume.pause-must-not-silently-override-an-active-interrupt-suspend",
|
|
18789
18838
|
"citations": [
|
|
18790
18839
|
{
|
|
18791
|
-
"section":
|
|
18792
|
-
"requirement": ":pause-during-suspend MUST surface the active waiting-* status in the conflict envelope"
|
|
18840
|
+
"section": null,
|
|
18841
|
+
"requirement": ":pause-during-suspend MUST surface the active waiting-* status in the conflict envelope",
|
|
18842
|
+
"interpolated": true
|
|
18793
18843
|
},
|
|
18794
18844
|
{
|
|
18795
|
-
"section":
|
|
18796
|
-
"requirement": ":pause-during-suspend MUST NOT silently discard the active interrupt"
|
|
18845
|
+
"section": null,
|
|
18846
|
+
"requirement": ":pause-during-suspend MUST NOT silently discard the active interrupt",
|
|
18847
|
+
"interpolated": true
|
|
18797
18848
|
}
|
|
18798
18849
|
]
|
|
18799
18850
|
},
|
|
18800
18851
|
{
|
|
18801
18852
|
"id": "openwop.it.pause-resume.every-drainpolicy-advertised-by-the-host-is-accepted-on-pause",
|
|
18802
18853
|
"file": "pause-resume.test.ts",
|
|
18803
|
-
"line":
|
|
18854
|
+
"line": 318,
|
|
18804
18855
|
"title": "every drainPolicy advertised by the host is accepted on :pause",
|
|
18805
18856
|
"explicitId": "openwop.it.pause-resume.every-drainpolicy-advertised-by-the-host-is-accepted-on-pause",
|
|
18806
18857
|
"citations": [
|
|
@@ -1,7 +1,7 @@
|
|
|
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.0.0-rc.
|
|
4
|
+
"version": "2.0.0-rc.53",
|
|
5
5
|
"corpusTag": null,
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
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": "44b449eba2eb915b84aadf3c3d0f06bdf7e0b8ade2952b6a17711459d7b3999e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "6e4ad967752e5c6e87c48f656f4f4cbe3f85423faebc69ab60bd3bf9bf2b329e",
|
|
14
|
+
"api/v2/openapi.yaml": "aa8ee2c55604517ac9b2ac7923ad8a957f9fa4bc331bf1b91cdd5236933d6921",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
200
200
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
201
201
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
202
|
-
"spec/v1/core-standard-manifest.json": "
|
|
202
|
+
"spec/v1/core-standard-manifest.json": "c7f3665189cd982747f26b98dfa977293c0c359a1fb986a3a5a3b8b95f9edd83",
|
|
203
203
|
"spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
|
|
204
204
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
205
205
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -230,8 +230,8 @@
|
|
|
230
230
|
"spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
|
|
231
231
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
232
232
|
"spec/v2/profiles.json": "5019ac8209540bfbeee9c3232530d4e599be0ab926d817ff48edcec8419eb209",
|
|
233
|
-
"spec/v2/release.json": "
|
|
233
|
+
"spec/v2/release.json": "6be17bd4ee80838e3ee85dd7372a88532835448832fccd6b7834a5f1f0c6b326",
|
|
234
234
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
235
235
|
},
|
|
236
|
-
"corpusCommit": "
|
|
236
|
+
"corpusCommit": "693550295565e5f6072c9a852a3ea499275396fb"
|
|
237
237
|
}
|
|
@@ -2,17 +2,31 @@
|
|
|
2
2
|
* Track 13: operator-driven pause/resume (rest-endpoints.md v1.1).
|
|
3
3
|
*
|
|
4
4
|
* Exercises the `POST /v1/runs/{runId}:pause` and `:resume` endpoints
|
|
5
|
-
* against a long-running fixture (`conformance-
|
|
5
|
+
* against a long-running fixture (`conformance-cancellable` or `conformance-delay`;
|
|
6
|
+
* both take `inputs.delayMs` — conformance/fixtures.md).
|
|
6
7
|
*
|
|
7
8
|
* Verifies:
|
|
8
|
-
* 1. :pause on a running run transitions to `paused
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 1. :pause (immediate) on a running run transitions to `paused`; :resume
|
|
10
|
+
* returns it to `running` and it reaches a terminal.
|
|
11
|
+
* 2. :resume on a non-paused run returns 409 with details.runStatus.
|
|
12
|
+
* 3. :pause on an already-paused run returns 409 (details.runStatus paused)
|
|
13
|
+
* without a matching Idempotency-Key, and 202 with the cached response
|
|
14
|
+
* when the same key is sent — rest-endpoints.md §:pause Idempotency.
|
|
15
|
+
* 4. :pause on a terminal run returns 409 with details.runStatus.
|
|
16
|
+
* 5. drain-current-node lets the executing node reach a terminal FIRST:
|
|
17
|
+
* node.completed precedes run.paused in the log.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
19
|
+
* rc.53 (2026-09-05): a tier-1 host un-skipping this file found it
|
|
20
|
+
* contradicting rest-endpoints.md in three places — it expected 200/202 on a
|
|
21
|
+
* second :pause (the prose says 409 unless the Idempotency-Key matches), it
|
|
22
|
+
* asserted an error code `conflict` that v1 never named (v1 mandates 409 +
|
|
23
|
+
* details.runStatus and names only run_terminal for a terminal run), and it
|
|
24
|
+
* paused a 30 s node under drain-current-node and polled 10 s for `paused`
|
|
25
|
+
* (drain lets the node finish; a host that yields mid-node under drain is the
|
|
26
|
+
* non-conforming one). It also sent `delaySeconds`, a field the fixtures do
|
|
27
|
+
* not define. v1 is frozen; the scenario was the defect in every case.
|
|
28
|
+
*
|
|
29
|
+
* Capability gating: skips when no long-running fixture is advertised.
|
|
16
30
|
*
|
|
17
31
|
* @see spec/v1/rest-endpoints.md §pause/resume
|
|
18
32
|
*/
|
|
@@ -31,35 +45,43 @@ const FIXTURE =
|
|
|
31
45
|
null;
|
|
32
46
|
|
|
33
47
|
const SKIP = !FIXTURE;
|
|
48
|
+
const DOC_PAUSE = 'rest-endpoints.md POST /v1/runs/{runId}:pause';
|
|
49
|
+
const DOC_RESUME = 'rest-endpoints.md POST /v1/runs/{runId}:resume';
|
|
50
|
+
|
|
51
|
+
async function cancel(runId: string): Promise<void> {
|
|
52
|
+
await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, { reason: 'conformance-cleanup' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function eventTypes(runId: string): Promise<string[]> {
|
|
56
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
|
|
57
|
+
const events = (res.json as { events?: unknown } | null)?.events;
|
|
58
|
+
return Array.isArray(events) ? events.map((e) => String((e as { type?: unknown }).type)) : [];
|
|
59
|
+
}
|
|
34
60
|
|
|
35
61
|
describe.skipIf(SKIP)('pause/resume: running → paused → running → terminal', () => {
|
|
36
62
|
it('pause transitions to paused; resume returns the run to running', async () => {
|
|
37
63
|
const create = await driver.post('/v1/runs', {
|
|
38
64
|
workflowId: FIXTURE!,
|
|
39
|
-
inputs: {
|
|
65
|
+
inputs: { delayMs: 30_000 },
|
|
40
66
|
});
|
|
41
67
|
expect(create.status).toBe(201);
|
|
42
68
|
const runId = (create.json as { runId: string }).runId;
|
|
43
69
|
|
|
44
70
|
await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
|
|
45
71
|
|
|
72
|
+
// `immediate` snapshots between events, so `paused` is observable within
|
|
73
|
+
// seconds. Under drain-current-node this 30 s node would finish first
|
|
74
|
+
// (rest-endpoints.md); that semantic is witnessed by the drain leg below.
|
|
46
75
|
const pause = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {
|
|
47
76
|
reason: 'conformance-test',
|
|
48
|
-
drainPolicy: '
|
|
77
|
+
drainPolicy: 'immediate',
|
|
49
78
|
});
|
|
50
79
|
if (pause.status === 404) {
|
|
51
|
-
|
|
52
|
-
// eslint-disable-next-line no-console
|
|
53
|
-
console.warn(
|
|
54
|
-
'[pause-resume] host returned 404 for :pause — endpoint not implemented; skipping rest',
|
|
55
|
-
);
|
|
56
|
-
await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
57
|
-
reason: 'conformance-cleanup',
|
|
58
|
-
});
|
|
80
|
+
await cancel(runId);
|
|
59
81
|
return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early ([pause-resume] host returned 404 for :pause — endpoint not implemented; skipping rest) (seam, prior step, or fixture unavailable)');
|
|
60
82
|
}
|
|
61
|
-
expect(pause.status, req('openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running',
|
|
62
|
-
|
|
83
|
+
expect(pause.status, req('openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running',
|
|
84
|
+
DOC_PAUSE,
|
|
63
85
|
':pause MUST return 202 on a pausable run',
|
|
64
86
|
)).toBe(202);
|
|
65
87
|
|
|
@@ -68,8 +90,8 @@ describe.skipIf(SKIP)('pause/resume: running → paused → running → terminal
|
|
|
68
90
|
const resume = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:resume`, {
|
|
69
91
|
reason: 'conformance-test',
|
|
70
92
|
});
|
|
71
|
-
expect(resume.status, req('openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running',
|
|
72
|
-
|
|
93
|
+
expect(resume.status, req('openwop.it.pause-resume.pause-transitions-to-paused-resume-returns-the-run-to-running',
|
|
94
|
+
DOC_RESUME,
|
|
73
95
|
':resume MUST return 202 on a paused run',
|
|
74
96
|
)).toBe(202);
|
|
75
97
|
|
|
@@ -82,7 +104,7 @@ describe.skipIf(SKIP)('pause/resume: :resume on a non-paused run returns 409', (
|
|
|
82
104
|
it('resuming a running (not paused) run returns 409 with details.runStatus', async () => {
|
|
83
105
|
const create = await driver.post('/v1/runs', {
|
|
84
106
|
workflowId: FIXTURE!,
|
|
85
|
-
inputs: {
|
|
107
|
+
inputs: { delayMs: 30_000 },
|
|
86
108
|
});
|
|
87
109
|
expect(create.status).toBe(201);
|
|
88
110
|
const runId = (create.json as { runId: string }).runId;
|
|
@@ -91,61 +113,85 @@ describe.skipIf(SKIP)('pause/resume: :resume on a non-paused run returns 409', (
|
|
|
91
113
|
|
|
92
114
|
const resume = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:resume`, {});
|
|
93
115
|
if (resume.status === 404) {
|
|
94
|
-
await
|
|
95
|
-
reason: 'conformance-cleanup',
|
|
96
|
-
});
|
|
116
|
+
await cancel(runId);
|
|
97
117
|
return softSkip('blocked', 'precondition not met — `resume.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
98
118
|
}
|
|
99
|
-
expect(resume.status, req('openwop.it.pause-resume.resuming-a-running-not-paused-run-returns-409-with-details-runstatus',
|
|
100
|
-
|
|
119
|
+
expect(resume.status, req('openwop.it.pause-resume.resuming-a-running-not-paused-run-returns-409-with-details-runstatus',
|
|
120
|
+
DOC_RESUME,
|
|
101
121
|
':resume on a non-paused run MUST return 409',
|
|
102
122
|
)).toBe(409);
|
|
103
123
|
|
|
124
|
+
// v1 mandates the status and details.runStatus; it names no code for
|
|
125
|
+
// this case (the registry is open — error-envelope.schema.json), so
|
|
126
|
+
// none is asserted. `run_state_conflict` is the v2 answer (runs.md).
|
|
104
127
|
const body = resume.json as { error?: string; details?: { runStatus?: string } };
|
|
105
|
-
expect(body.
|
|
106
|
-
|
|
128
|
+
expect(typeof body.details?.runStatus, req('openwop.it.pause-resume.resuming-a-running-not-paused-run-returns-409-with-details-runstatus',
|
|
129
|
+
DOC_RESUME,
|
|
130
|
+
'the 409 MUST carry details.runStatus with the actual state',
|
|
131
|
+
)).toBe('string');
|
|
107
132
|
|
|
108
|
-
await
|
|
109
|
-
reason: 'conformance-cleanup',
|
|
110
|
-
});
|
|
133
|
+
await cancel(runId);
|
|
111
134
|
});
|
|
112
135
|
});
|
|
113
136
|
|
|
114
|
-
describe.skipIf(SKIP)('pause/resume: pause is
|
|
115
|
-
it(':pause on an already-paused run
|
|
137
|
+
describe.skipIf(SKIP)('pause/resume: a second :pause is 409 without a matching Idempotency-Key and 202 with one', () => {
|
|
138
|
+
it(':pause on an already-paused run returns 409 with details.runStatus paused, unless the request carries the original Idempotency-Key', async () => {
|
|
116
139
|
const create = await driver.post('/v1/runs', {
|
|
117
140
|
workflowId: FIXTURE!,
|
|
118
|
-
inputs: {
|
|
141
|
+
inputs: { delayMs: 30_000 },
|
|
119
142
|
});
|
|
120
143
|
expect(create.status).toBe(201);
|
|
121
144
|
const runId = (create.json as { runId: string }).runId;
|
|
122
145
|
await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
|
|
123
146
|
|
|
124
|
-
const first = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {});
|
|
147
|
+
const first = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, { drainPolicy: 'immediate' });
|
|
125
148
|
if (first.status === 404) {
|
|
126
|
-
await
|
|
127
|
-
reason: 'conformance-cleanup',
|
|
128
|
-
});
|
|
149
|
+
await cancel(runId);
|
|
129
150
|
return softSkip('blocked', 'precondition not met — `first.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
130
151
|
}
|
|
131
|
-
expect(
|
|
152
|
+
expect(first.status, req('openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless',
|
|
153
|
+
DOC_PAUSE,
|
|
154
|
+
':pause MUST return 202 on a pausable run',
|
|
155
|
+
)).toBe(202);
|
|
132
156
|
await pollUntilStatus(runId, 'paused', { timeoutMs: 10_000 });
|
|
133
157
|
|
|
134
|
-
//
|
|
135
|
-
// already paused
|
|
136
|
-
//
|
|
158
|
+
// rest-endpoints.md §:pause Idempotency: "a :pause against a run that is
|
|
159
|
+
// already paused returns 409 (with the existing pause's pausedAt in
|
|
160
|
+
// details) unless the request carries Idempotency-Key matching the
|
|
161
|
+
// original pause, in which case the host returns 202 with the cached
|
|
162
|
+
// response." Until rc.53 this leg asserted 200/202 here, citing an
|
|
163
|
+
// "additive contract" the prose does not contain.
|
|
137
164
|
const second = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {});
|
|
138
|
-
expect(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
expect(second.status, req('openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless',
|
|
166
|
+
DOC_PAUSE,
|
|
167
|
+
':pause on an already-paused run without a matching Idempotency-Key MUST return 409',
|
|
168
|
+
)).toBe(409);
|
|
169
|
+
const body = second.json as { details?: { runStatus?: string } };
|
|
170
|
+
expect(body.details?.runStatus, req('openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless',
|
|
171
|
+
DOC_PAUSE,
|
|
172
|
+
'the 409 MUST carry details.runStatus: paused',
|
|
173
|
+
)).toBe('paused');
|
|
174
|
+
await cancel(runId);
|
|
175
|
+
|
|
176
|
+
// The keyed form: the same Idempotency-Key on both pauses collapses the
|
|
177
|
+
// second into the cached 202.
|
|
178
|
+
const keyed = await driver.post('/v1/runs', { workflowId: FIXTURE!, inputs: { delayMs: 30_000 } });
|
|
179
|
+
expect(keyed.status).toBe(201);
|
|
180
|
+
const keyedId = (keyed.json as { runId: string }).runId;
|
|
181
|
+
await pollUntilStatus(keyedId, 'running', { timeoutMs: 10_000 });
|
|
182
|
+
const key = `openwop-conformance-pause-${keyedId.replace(/[^A-Za-z0-9._~-]/g, '-')}`.slice(0, 128);
|
|
183
|
+
const k1 = await driver.post(`/v1/runs/${encodeURIComponent(keyedId)}:pause`, { drainPolicy: 'immediate' }, { headers: { 'Idempotency-Key': key } });
|
|
184
|
+
expect(k1.status, req('openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless',
|
|
185
|
+
DOC_PAUSE,
|
|
186
|
+
':pause with an Idempotency-Key MUST return 202 on a pausable run',
|
|
187
|
+
)).toBe(202);
|
|
188
|
+
await pollUntilStatus(keyedId, 'paused', { timeoutMs: 10_000 });
|
|
189
|
+
const k2 = await driver.post(`/v1/runs/${encodeURIComponent(keyedId)}:pause`, { drainPolicy: 'immediate' }, { headers: { 'Idempotency-Key': key } });
|
|
190
|
+
expect(k2.status, req('openwop.it.pause-resume.pause-on-an-already-paused-run-returns-409-with-details-runstatus-paused-unless',
|
|
191
|
+
DOC_PAUSE,
|
|
192
|
+
'a :pause carrying the original Idempotency-Key MUST return 202 with the cached response, not 409',
|
|
193
|
+
)).toBe(202);
|
|
194
|
+
await cancel(keyedId);
|
|
149
195
|
});
|
|
150
196
|
});
|
|
151
197
|
|
|
@@ -160,19 +206,58 @@ describe.skipIf(SKIP)('pause/resume: :pause on a terminal run returns 409', () =
|
|
|
160
206
|
|
|
161
207
|
const pause = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {});
|
|
162
208
|
if (pause.status === 404) return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
163
|
-
expect(pause.status, req('openwop.it.pause-resume.pause-on-a-completed-cancelled-failed-run-must-return-409',
|
|
164
|
-
|
|
209
|
+
expect(pause.status, req('openwop.it.pause-resume.pause-on-a-completed-cancelled-failed-run-must-return-409',
|
|
210
|
+
DOC_PAUSE,
|
|
165
211
|
':pause on a terminal run MUST return 409',
|
|
166
212
|
)).toBe(409);
|
|
167
213
|
|
|
214
|
+
// v1 mandates details.runStatus; `run_terminal` is the one code v1 names
|
|
215
|
+
// for a mutation on a terminal run (rest-endpoints.md, bulk-cancel), and
|
|
216
|
+
// the registry is open, so the code is not asserted here.
|
|
168
217
|
const body = pause.json as { error?: string; details?: { runStatus?: string } };
|
|
169
|
-
expect(body.error).toBe('conflict');
|
|
170
|
-
// Spec requires `details.runStatus` to disclose the terminal state so
|
|
171
|
-
// the caller can decide whether to retry or surface the conflict.
|
|
172
218
|
expect(['completed', 'failed', 'cancelled']).toContain(body.details?.runStatus);
|
|
173
219
|
});
|
|
174
220
|
});
|
|
175
221
|
|
|
222
|
+
describe.skipIf(SKIP)('pause/resume: drain-current-node lets the executing node reach a terminal first', () => {
|
|
223
|
+
it('under drain-current-node node.completed precedes run.paused in the log', async () => {
|
|
224
|
+
// A short node: drain has something to wait for that finishes inside the
|
|
225
|
+
// poll window. A 30 s node under drain pauses after 30 s — that is the
|
|
226
|
+
// semantic, not a bug (rest-endpoints.md: "lets the executing node reach
|
|
227
|
+
// a terminal before the run transitions to paused").
|
|
228
|
+
const create = await driver.post('/v1/runs', {
|
|
229
|
+
workflowId: FIXTURE!,
|
|
230
|
+
inputs: { delayMs: 3_000 },
|
|
231
|
+
});
|
|
232
|
+
expect(create.status).toBe(201);
|
|
233
|
+
const runId = (create.json as { runId: string }).runId;
|
|
234
|
+
await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
|
|
235
|
+
|
|
236
|
+
const pause = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {
|
|
237
|
+
reason: 'conformance-drain',
|
|
238
|
+
drainPolicy: 'drain-current-node',
|
|
239
|
+
});
|
|
240
|
+
if (pause.status === 404) {
|
|
241
|
+
await cancel(runId);
|
|
242
|
+
return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
243
|
+
}
|
|
244
|
+
expect(pause.status, req('openwop.it.pause-resume.under-drain-current-node-node-completed-precedes-run-paused-in-the-log',
|
|
245
|
+
DOC_PAUSE,
|
|
246
|
+
':pause with drainPolicy drain-current-node MUST return 202 (pause requested)',
|
|
247
|
+
)).toBe(202);
|
|
248
|
+
|
|
249
|
+
await pollUntilStatus(runId, 'paused', { timeoutMs: 20_000 });
|
|
250
|
+
const types = await eventTypes(runId);
|
|
251
|
+
const done = types.indexOf('node.completed');
|
|
252
|
+
const paused = types.indexOf('run.paused');
|
|
253
|
+
expect(done >= 0 && paused > done, req('openwop.it.pause-resume.under-drain-current-node-node-completed-precedes-run-paused-in-the-log',
|
|
254
|
+
DOC_PAUSE,
|
|
255
|
+
`drain-current-node MUST let the executing node reach a terminal before the run transitions to paused — node.completed MUST precede run.paused (types: ${types.join(', ')})`,
|
|
256
|
+
)).toBe(true);
|
|
257
|
+
await cancel(runId);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
176
261
|
describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
|
|
177
262
|
it(':pause MUST NOT silently override an active interrupt suspend', async () => {
|
|
178
263
|
// If the host seeds an approval fixture, drive a suspend then attempt
|
|
@@ -198,18 +283,15 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
|
|
|
198
283
|
reason: 'race-test',
|
|
199
284
|
});
|
|
200
285
|
if (pause.status === 404) {
|
|
201
|
-
|
|
202
|
-
await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
203
|
-
reason: 'conformance-cleanup',
|
|
204
|
-
});
|
|
286
|
+
await cancel(runId);
|
|
205
287
|
return softSkip('blocked', 'precondition not met — `pause.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
206
288
|
}
|
|
207
289
|
|
|
208
290
|
// Either rejection (preferred) or stacked-pause is OK; silent override is not.
|
|
209
291
|
if (pause.status === 409) {
|
|
210
292
|
const body = pause.json as { details?: { runStatus?: string } };
|
|
211
|
-
expect(body.details?.runStatus, req('openwop.it.pause-resume.pause-must-not-silently-override-an-active-interrupt-suspend',
|
|
212
|
-
|
|
293
|
+
expect(body.details?.runStatus, req('openwop.it.pause-resume.pause-must-not-silently-override-an-active-interrupt-suspend',
|
|
294
|
+
DOC_PAUSE,
|
|
213
295
|
':pause-during-suspend MUST surface the active waiting-* status in the conflict envelope',
|
|
214
296
|
)).toMatch(/^waiting-/);
|
|
215
297
|
} else {
|
|
@@ -220,13 +302,11 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
|
|
|
220
302
|
const status = (snap.json as { status: string }).status;
|
|
221
303
|
expect(
|
|
222
304
|
status === 'paused' || status.startsWith('waiting-'),
|
|
223
|
-
req('openwop.it.pause-resume.pause-must-not-silently-override-an-active-interrupt-suspend',
|
|
305
|
+
req('openwop.it.pause-resume.pause-must-not-silently-override-an-active-interrupt-suspend', DOC_PAUSE, ':pause-during-suspend MUST NOT silently discard the active interrupt'),
|
|
224
306
|
).toBe(true);
|
|
225
307
|
}
|
|
226
308
|
|
|
227
|
-
await
|
|
228
|
-
reason: 'conformance-cleanup',
|
|
229
|
-
});
|
|
309
|
+
await cancel(runId);
|
|
230
310
|
});
|
|
231
311
|
});
|
|
232
312
|
|
|
@@ -249,7 +329,7 @@ describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities
|
|
|
249
329
|
for (const policy of drainPolicies) {
|
|
250
330
|
const create = await driver.post('/v1/runs', {
|
|
251
331
|
workflowId: FIXTURE!,
|
|
252
|
-
inputs: {
|
|
332
|
+
inputs: { delayMs: 30_000 },
|
|
253
333
|
});
|
|
254
334
|
expect(create.status).toBe(201);
|
|
255
335
|
const runId = (create.json as { runId: string }).runId;
|
|
@@ -260,14 +340,12 @@ describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities
|
|
|
260
340
|
reason: `conformance-drainpolicy-${policy}`,
|
|
261
341
|
drainPolicy: policy,
|
|
262
342
|
});
|
|
263
|
-
expect(pause.status, req('openwop.it.pause-resume.every-drainpolicy-advertised-by-the-host-is-accepted-on-pause',
|
|
343
|
+
expect(pause.status, req('openwop.it.pause-resume.every-drainpolicy-advertised-by-the-host-is-accepted-on-pause',
|
|
264
344
|
'capabilities.md §`runs.pauseResume.drainPolicies` + rest-endpoints.md POST /v1/runs/{runId}:pause',
|
|
265
345
|
`host-advertised drainPolicy='${policy}' MUST be accepted on :pause`,
|
|
266
346
|
)).toBe(202);
|
|
267
347
|
|
|
268
|
-
await
|
|
269
|
-
reason: 'conformance-cleanup',
|
|
270
|
-
});
|
|
348
|
+
await cancel(runId);
|
|
271
349
|
}
|
|
272
350
|
});
|
|
273
351
|
});
|