@openwop/openwop-conformance 2.33.1 → 2.33.2

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.33.2] — 2026-09-21 — `v2-run-snapshot-etag` took its tag from a run that was still moving
4
+
5
+ - **The scenario slept a fixed 1 s after `POST /runs`, took the snapshot's `ETag`, and demanded `304`.** A host whose ETag tracks the run's event-log sequence — the strong tag `runs.md` §Snapshot asks for — is still appending while a noop executes, so between the two GETs the representation **changes** and `200` with a new tag is the only correct answer; a `304` there would be a stale-cache bug. **Measured on a tier-2 production host, 4 of 4:** immediately after create `etag1 ≠ etag2` and the conditional GET answers `200`; after the run settles the same request answers `304` every time. The row had passed three earlier cuts of that host and failed the fourth — a timing lottery, won whenever the run finished inside the sleep.
6
+ - **It now waits for the run to be terminal, and the violation is stated exactly:** a `200` whose `ETag` **equals** the `If-None-Match` that was sent. A `200` carrying a *different* tag means the representation moved and the tag sent was honestly stale, so the scenario takes the new tag and retries (up to four reads); a terminal run whose snapshot never holds still records `blocked`, not fail.
7
+ - A host with a **constant** ETag passed the old row for the wrong reason whenever it won the lottery; the non-matching control (`If-None-Match: "…no-such-tag"` MUST answer `200`) still catches a host that answers `304` to everything.
8
+ - False FAIL only. Found by the MyndHyve host session, reproduced by hand on production before being reported.
9
+ - Suite patch: corpus release stays `2.33.0`.
10
+
3
11
  ## [2.33.1] — 2026-09-21 — two false fails: a race between two reads, and a subscription nobody unregistered
4
12
 
5
13
  - **`v2-durability-recovery`: the "completed without re-execution" latch was decided from two non-atomic reads in the wrong order.** Each iteration read the LOG, then the STATUS, and latched when the status said `completed` and the — older — log showed no resumption. A host that re-dispatched between the two requests read as "completed un-re-executed" while being neither. `conformance-noop` re-executes in milliseconds, so the window is real: per run roughly (gap between the requests) / (500 ms poll), a few percent. **Measured on a tier-1 host:** after a genuine SIGKILL and a correct recovery by lease expiry (727 s inside a declared 750 s bound) the row failed with *"read status completed with 2 run.started"* — printing the re-execution it was denying, because the message used a later read than the latch. The same host and code had passed the two previous runs.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.33.1 @openwop/spec-artifacts@2.33.1
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.33.2 @openwop/spec-artifacts@2.33.2
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.33.1",
4
- "stampSha256": "132270b1cd8c97bed21d57a5b08c9abe89d10a56ff1aadf9ff3119edafb4d121"
3
+ "version": "2.33.2",
4
+ "stampSha256": "005732c466b247dadf5ee3d8c68186c6ce4f586febbaa260b7906fd496153965"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.33.1",
3
+ "version": "2.33.2",
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.33.1"
59
+ "@openwop/spec-artifacts": "2.33.2"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -28985,7 +28985,7 @@
28985
28985
  {
28986
28986
  "id": "openwop.it.v2-run-snapshot-etag.a-matching-if-none-match-receives-304-with-no-body-and-the-version-header-a-non",
28987
28987
  "file": "v2-run-snapshot-etag.test.ts",
28988
- "line": 34,
28988
+ "line": 35,
28989
28989
  "title": "a matching If-None-Match receives 304 with no body and the version header; a non-matching one receives 200",
28990
28990
  "explicitId": "openwop.requirement.0170.run-snapshot-etag",
28991
28991
  "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.33.1",
4
+ "version": "2.33.2",
5
5
  "corpusTag": "v2.33.0",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
@@ -201,7 +201,7 @@
201
201
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
202
202
  "spec/v1/alias-detectors.json": "40069d5976eeb6ba1384a648e57e5cfd673db3fce36175115fb8293bee9664d4",
203
203
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
204
- "spec/v1/core-standard-manifest.json": "71c38c738496eaac1d144561a7f6e752600f8afd71f938eb483c0c76b8a5263a",
204
+ "spec/v1/core-standard-manifest.json": "65a8576c2e5ea9deb9d463be5b02a8bfb0e125a8dcbe3fe2cf286a4fd6d5d53e",
205
205
  "spec/v1/deprecations.json": "307083ce29c23fd406015951f99a30d78d6187ff061d38dc9732f62191b40f3f",
206
206
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
207
207
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -282,5 +282,5 @@
282
282
  "spec/v2/release.json": "94a3ea302324d9f7df87ce62a186b0b98936de6ce5ac5ec01a3fb3208c38efc3",
283
283
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
284
284
  },
285
- "corpusCommit": "b9afb25c0bdfbbd2f11efad6f984df51e50344e5"
285
+ "corpusCommit": "65e40d60fbf43b1a0cd95a58e63d7464ed944ddb"
286
286
  }
@@ -16,6 +16,7 @@
16
16
  * @see spec/v2/core/versioning.md §1.4
17
17
  */
18
18
 
19
+ import { pollUntilTerminal, scaledTimeoutMs } from '../lib/polling.js';
19
20
  import { describe, it, expect } from 'vitest';
20
21
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
21
22
  import { v2Discovery } from '../lib/v2.js';
@@ -38,15 +39,39 @@ describe('v2 run-snapshot-etag (runs.md §Snapshot)', () => {
38
39
  const runId = (created.json as { runId?: unknown } | null)?.runId;
39
40
  if (created.status !== 201 || typeof runId !== 'string') return softSkip('blocked', `POST /runs answered ${created.status} ${readErrorCode(created.json) ?? ''} — create refused`.trim());
40
41
  const path = `/runs/${encodeURIComponent(runId)}`;
41
- // Let the noop settle so the tag is stable across the two conditional reads.
42
- await new Promise((r) => setTimeout(r, 1_000));
43
- const first = await http(() => driver.get(path));
44
- if (first === null || first.status !== 200) return softSkip('blocked', `GET /runs/{runId} answered ${first?.status ?? 'no response'}`);
45
- const etag = first.headers.get('etag');
46
- if (!etag) return softSkip('inapplicable', 'the snapshot carries no ETag — runs.md §Snapshot makes the ETag a SHOULD; the 304 rule applies only when it is present');
47
- const hit = await http(() => driver.get(path, { headers: { 'If-None-Match': etag } }));
48
- if (hit === null) return softSkip('blocked', 'conditional GET unreachable (fetch failed)');
49
- expect(hit.status, req(ID, DOC, `a request whose If-None-Match matches the ETag MUST receive 304 — got ${hit.status}`)).toBe(304);
42
+ // The tag MUST be taken from a representation that has stopped moving. Until
43
+ // 2.33.2 this slept a fixed 1 s and hoped. A host whose ETag tracks the run's
44
+ // event-log sequence (RFC 0115 — the strong tag the SHOULD asks for) is still
45
+ // appending while a noop executes, so between the two GETs the representation
46
+ // CHANGES and `200` with a new tag is the only correct answer; a `304` there
47
+ // would be a stale-cache bug. Measured on a tier-2 production host, 4 of 4:
48
+ // immediately after create etag1 != etag2 and the conditional GET answers 200;
49
+ // after the run settles the same request answers 304 every time. The row had
50
+ // passed three earlier cuts of that host — a timing lottery, won when the run
51
+ // happened to finish inside the sleep. It also let a host with a CONSTANT tag
52
+ // pass for the wrong reason, which the control below still catches.
53
+ await pollUntilTerminal(runId, { timeoutMs: scaledTimeoutMs(30_000) });
54
+ let etag: string | null = null;
55
+ let hit: OpenWOPResponse | null = null;
56
+ // A terminal run should be still, but "should" is not the claim under test:
57
+ // if the tag moves again, take the NEW tag and retry. The violation is a 200
58
+ // whose ETag EQUALS the If-None-Match that was sent — never a 200 per se.
59
+ for (let attempt = 0; attempt < 4; attempt++) {
60
+ const first = await http(() => driver.get(path));
61
+ if (first === null || first.status !== 200) return softSkip('blocked', `GET /runs/{runId} answered ${first?.status ?? 'no response'}`);
62
+ etag = first.headers.get('etag');
63
+ if (!etag) return softSkip('inapplicable', 'the snapshot carries no ETag — runs.md §Snapshot makes the ETag a SHOULD; the 304 rule applies only when it is present');
64
+ const sent = etag;
65
+ hit = await http(() => driver.get(path, { headers: { 'If-None-Match': sent } }));
66
+ if (hit === null) return softSkip('blocked', 'conditional GET unreachable (fetch failed)');
67
+ if (hit.status !== 200 || hit.headers.get('etag') === sent) break; // 304, or the real violation: decided below
68
+ await new Promise((r) => setTimeout(r, 500)); // 200 with a DIFFERENT tag: the representation moved; the tag sent was honestly stale
69
+ }
70
+ if (hit === null || etag === null) return softSkip('blocked', 'no conditional GET was made');
71
+ if (hit.status === 200 && hit.headers.get('etag') !== etag) {
72
+ return softSkip('blocked', `the snapshot of a TERMINAL run kept changing across four reads (last tag sent ${etag}, answered 200 with ${String(hit.headers.get('etag'))}) — the 304 rule cannot be witnessed against a representation that never holds still`);
73
+ }
74
+ expect(hit.status, req(ID, DOC, `a request whose If-None-Match matches the CURRENT ETag MUST receive 304 — got ${hit.status} while the response still carried the same tag ${etag}`)).toBe(304);
50
75
  expect(hit.text.length, req(ID, DOC, `the 304 MUST carry no body (got ${hit.text.length} byte(s))`)).toBe(0);
51
76
  expect(hit.headers.get('openwop-version'), req(ID, 'spec/v2/core/versioning.md §1.4', 'every response carries OpenWOP-Version, the 304 included')).not.toBeNull();
52
77
  const miss = await http(() => driver.get(path, { headers: { 'If-None-Match': '"openwop-conformance-no-such-tag"' } }));