@openwop/openwop-conformance 2.45.3 → 2.45.5
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 +32 -0
- package/README.md +2 -2
- package/dist/lib/scenario-disposition.js +26 -7
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-budget-tool-calls.json +81 -0
- package/fixtures.md +11 -0
- package/package.json +2 -2
- package/requirements.json +84 -14
- package/scenario-majors.json +8 -2
- package/schemas/CORPUS-STAMP.json +9 -9
- package/src/lib/backpressure-witness.ts +163 -0
- package/src/lib/budget-witness.ts +165 -0
- package/src/lib/major-profile.ts +93 -0
- package/src/lib/scenario-disposition.ts +20 -3
- package/src/lib/scratch-host.ts +130 -0
- package/src/scenarios/runner-ledger.test.ts +3 -6
- package/src/scenarios/v2-budget-enforcement.test.ts +94 -0
- package/src/scenarios/v2-production-backpressure.test.ts +74 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — a run budget is reserved, crossed, exhausted and enforced
|
|
3
|
+
* (`spec/v2/core/runs.md` §`budget` section). The v1 twin is
|
|
4
|
+
* `budget-enforcement`, which drives a host seam; at major 2 the budget rides
|
|
5
|
+
* on `createRun` (`configurable.budget`), so this witness is unaided. The
|
|
6
|
+
* logic is `lib/budget-witness.ts`; what differs between majors is the profile
|
|
7
|
+
* row in `lib/major-profile.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The suite creates a run of `conformance-budget-tool-calls` (three scripted
|
|
10
|
+
* tool calls) with `{ maxToolCalls: 2, thresholdPercent: 50, onExhaustion:
|
|
11
|
+
* "fail" }` and reads its log through the poll.
|
|
12
|
+
*
|
|
13
|
+
* lifecycle `budget.reserved`, `budget.threshold-crossed` (numeric
|
|
14
|
+
* `percent`) and `budget.exhausted`, in that order. Both
|
|
15
|
+
* enforce modes owe these.
|
|
16
|
+
* enforcement `enforce: hard`: `cap.breached` with kind
|
|
17
|
+
* `budget-tool-calls`, not before `budget.exhausted`, and the
|
|
18
|
+
* run fails `budget_exhausted`. `enforce: advisory`: the run is
|
|
19
|
+
* not stopped.
|
|
20
|
+
* content-free no `budget.*` or `cap.breached` payload carries a rate card,
|
|
21
|
+
* a unit price or a credential.
|
|
22
|
+
*
|
|
23
|
+
* Dispositions: no `budget`, `toolCalls` not in `budget.dimensions`, or the
|
|
24
|
+
* fixture unadvertised ⇒ `inapplicable`. The fixture is the opt-in: a host
|
|
25
|
+
* that advertises `budget` and has not seeded it is unwitnessed here, not
|
|
26
|
+
* blocked. A valid budgeted create that is refused ⇒ `executed-fail`.
|
|
27
|
+
*
|
|
28
|
+
* Not ported: v1's model-denied leg (`budget_model_denied`). It needs a
|
|
29
|
+
* fixture that resolves a model, and none does so without a seam.
|
|
30
|
+
*
|
|
31
|
+
* Proven both ways against the scratch host in `lib/budget-witness.test.ts`.
|
|
32
|
+
*
|
|
33
|
+
* @see spec/v2/core/runs.md §`budget` section
|
|
34
|
+
* @see conformance/fixtures.md §conformance-budget-tool-calls
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { describe, it, expect } from 'vitest';
|
|
38
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
39
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
40
|
+
import { req } from '../lib/requirement-ids.js';
|
|
41
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
42
|
+
import { drive, judge, type BudgetRun, type Finding } from '../lib/budget-witness.js';
|
|
43
|
+
|
|
44
|
+
const PROFILE = majorProfile(2);
|
|
45
|
+
const DOC = 'spec/v2/core/runs.md §budget section';
|
|
46
|
+
const ID_LIFECYCLE = 'openwop.requirement.runs.budget-lifecycle';
|
|
47
|
+
const ID_ENFORCEMENT = 'openwop.requirement.runs.budget-enforcement';
|
|
48
|
+
const ID_CONTENT_FREE = 'openwop.requirement.runs.budget-content-free';
|
|
49
|
+
|
|
50
|
+
/** One budgeted run per file: three legs read the same log. */
|
|
51
|
+
let once: Promise<BudgetRun> | undefined;
|
|
52
|
+
function budgetRun(): Promise<BudgetRun> {
|
|
53
|
+
once ??= (async (): Promise<BudgetRun> => {
|
|
54
|
+
let doc: Record<string, unknown> | null;
|
|
55
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
56
|
+
if (!doc) return { kind: 'skip', disposition: 'blocked', reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
|
|
57
|
+
return drive(PROFILE, doc);
|
|
58
|
+
})();
|
|
59
|
+
return once;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Leg = { readonly skip: { readonly disposition: 'inapplicable' | 'blocked'; readonly reason: string } } | { readonly findings: Finding[] };
|
|
63
|
+
|
|
64
|
+
/** The findings for one leg, or why there are none. A refused create fails the leg here. */
|
|
65
|
+
async function leg(id: string, rules: ReadonlyArray<Finding['rule']>): Promise<Leg> {
|
|
66
|
+
const r = await budgetRun();
|
|
67
|
+
if (r.kind === 'skip') return { skip: { disposition: r.disposition, reason: r.reason } };
|
|
68
|
+
if (r.kind === 'refused') {
|
|
69
|
+
expect(r.status, req(id, DOC, `a host that advertises budget and the fixture MUST accept a valid configurable.budget (got ${r.status} ${r.code ?? ''})`.trim())).toBe(201);
|
|
70
|
+
return { findings: [] }; // unreachable: a refusal is never 201
|
|
71
|
+
}
|
|
72
|
+
return { findings: judge(PROFILE, r.observation).filter((f) => rules.includes(f.rule)) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('v2 budget enforcement (runs.md §budget section)', () => {
|
|
76
|
+
it('a budgeted run emits budget.reserved, budget.threshold-crossed and budget.exhausted in order', async () => {
|
|
77
|
+
const l = await leg(ID_LIFECYCLE, ['lifecycle']);
|
|
78
|
+
if ('skip' in l) return softSkip(l.skip.disposition, l.skip.reason);
|
|
79
|
+
for (const f of l.findings) expect(f.ok, req(ID_LIFECYCLE, f.doc, f.message)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('a hard host stops the run budget_exhausted after cap.breached, and an advisory host does not stop it', async () => {
|
|
83
|
+
const l = await leg(ID_ENFORCEMENT, ['hard-stop', 'advisory']);
|
|
84
|
+
if ('skip' in l) return softSkip(l.skip.disposition, l.skip.reason);
|
|
85
|
+
if (l.findings.length === 0) return softSkip('inapplicable', 'budget.enforce is not advertised — neither the hard stop nor the advisory rule binds');
|
|
86
|
+
for (const f of l.findings) expect(f.ok, req(ID_ENFORCEMENT, f.doc, f.message)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('no budget.* or cap.breached payload carries pricing or a credential', async () => {
|
|
90
|
+
const l = await leg(ID_CONTENT_FREE, ['content-free']);
|
|
91
|
+
if ('skip' in l) return softSkip(l.skip.disposition, l.skip.reason);
|
|
92
|
+
for (const f of l.findings) expect(f.ok, req(ID_CONTENT_FREE, f.doc, f.message)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — a host at capacity answers `503 service_unavailable` with `Retry-After`
|
|
3
|
+
* (`spec/v2/core/conformance.md` §Production profile, `backpressure` facet).
|
|
4
|
+
* The v1 twin is `production-backpressure`; the logic both share is
|
|
5
|
+
* `lib/backpressure-witness.ts`, and what differs between majors is the
|
|
6
|
+
* profile row in `lib/major-profile.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Unaided. Gated on `production.backpressure.inflightCap`, the number a host
|
|
9
|
+
* advertises so the suite can saturate it: `inflightCap` event streams hold
|
|
10
|
+
* the slots and one more request is sent.
|
|
11
|
+
*
|
|
12
|
+
* refusal the extra request answers `503`, code `service_unavailable`,
|
|
13
|
+
* with `Retry-After`; where `retryAfterSeconds` is advertised
|
|
14
|
+
* the header equals it.
|
|
15
|
+
* retry timing the refusal carries no `details.retryAfter*`
|
|
16
|
+
* (`errors.md` §Retry timing). This is where v2 differs from
|
|
17
|
+
* v1, which required `details.retryAfter`.
|
|
18
|
+
*
|
|
19
|
+
* Dispositions: no `production`, no `backpressure`, no `inflightCap`, an
|
|
20
|
+
* `inflightCap` above 64 (the most streams the suite holds open), or the hold
|
|
21
|
+
* or probe fixture unadvertised ⇒ `inapplicable`. A slot that cannot be
|
|
22
|
+
* filled, or a probe with no response ⇒ `blocked`.
|
|
23
|
+
*
|
|
24
|
+
* v1's "discovery is exempt from the cap" leg is not ported: no v2 document
|
|
25
|
+
* states it.
|
|
26
|
+
*
|
|
27
|
+
* Run with `--no-file-parallelism`. Proven both ways against the scratch host
|
|
28
|
+
* in `lib/backpressure-witness.test.ts`.
|
|
29
|
+
*
|
|
30
|
+
* @see spec/v2/core/conformance.md §Production profile
|
|
31
|
+
* @see spec/v2/core/errors.md §Retry timing
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest';
|
|
35
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
36
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
37
|
+
import { req } from '../lib/requirement-ids.js';
|
|
38
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
39
|
+
import { judge, saturate, type Saturation } from '../lib/backpressure-witness.js';
|
|
40
|
+
|
|
41
|
+
const PROFILE = majorProfile(2);
|
|
42
|
+
const ID_REFUSAL = 'openwop.requirement.production.backpressure-refusal';
|
|
43
|
+
const ID_TIMING = 'openwop.requirement.0171.error-registry.no-retry-details';
|
|
44
|
+
|
|
45
|
+
/** One saturation per file: two legs read the same refusal. */
|
|
46
|
+
let once: Promise<Saturation> | undefined;
|
|
47
|
+
function saturation(): Promise<Saturation> {
|
|
48
|
+
once ??= (async (): Promise<Saturation> => {
|
|
49
|
+
let doc: Record<string, unknown> | null;
|
|
50
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
51
|
+
if (!doc) return { kind: 'skip', disposition: 'blocked', reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
|
|
52
|
+
return saturate(PROFILE, doc);
|
|
53
|
+
})();
|
|
54
|
+
return once;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('v2 production backpressure (conformance.md §Production profile)', () => {
|
|
58
|
+
it('with inflightCap slots held, the next request is refused 503 service_unavailable with Retry-After', async () => {
|
|
59
|
+
const s = await saturation();
|
|
60
|
+
if (s.kind === 'skip') return softSkip(s.disposition, s.reason);
|
|
61
|
+
for (const f of judge(PROFILE, s.refusal).filter((x) => x.rule !== 'retry-timing')) {
|
|
62
|
+
expect(f.ok, req(ID_REFUSAL, f.doc, f.message)).toBe(true);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('the refusal carries its retry timing in the Retry-After header only', async () => {
|
|
67
|
+
const s = await saturation();
|
|
68
|
+
if (s.kind === 'skip') return softSkip(s.disposition, s.reason);
|
|
69
|
+
if (s.refusal.status !== 503) return softSkip('blocked', `no 503 was observed at inflightCap + 1 (got ${s.refusal.status}) — there is no refusal whose details could be read`);
|
|
70
|
+
for (const f of judge(PROFILE, s.refusal).filter((x) => x.rule === 'retry-timing')) {
|
|
71
|
+
expect(f.ok, req(ID_TIMING, f.doc, f.message)).toBe(true);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|