@glubean/sdk 0.2.2 → 0.2.3
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/dist/bootstrap-registry.d.ts +38 -0
- package/dist/bootstrap-registry.d.ts.map +1 -0
- package/dist/bootstrap-registry.js +54 -0
- package/dist/bootstrap-registry.js.map +1 -0
- package/dist/contract-core.d.ts +13 -1
- package/dist/contract-core.d.ts.map +1 -1
- package/dist/contract-core.js +329 -5
- package/dist/contract-core.js.map +1 -1
- package/dist/contract-http/adapter.d.ts +1 -7
- package/dist/contract-http/adapter.d.ts.map +1 -1
- package/dist/contract-http/adapter.js +199 -192
- package/dist/contract-http/adapter.js.map +1 -1
- package/dist/contract-http/index.d.ts +1 -0
- package/dist/contract-http/index.d.ts.map +1 -1
- package/dist/contract-http/index.js +1 -0
- package/dist/contract-http/index.js.map +1 -1
- package/dist/contract-http/types.d.ts +71 -12
- package/dist/contract-http/types.d.ts.map +1 -1
- package/dist/contract-http/types.js +45 -1
- package/dist/contract-http/types.js.map +1 -1
- package/dist/contract-types.d.ts +211 -20
- package/dist/contract-types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -0
- package/dist/internal.js.map +1 -1
- package/dist/runner-input-channel.d.ts +95 -0
- package/dist/runner-input-channel.d.ts.map +1 -0
- package/dist/runner-input-channel.js +110 -0
- package/dist/runner-input-channel.js.map +1 -0
- package/package.json +1 -1
- package/dist/contract-http/flow-helpers.d.ts +0 -12
- package/dist/contract-http/flow-helpers.d.ts.map +0 -1
- package/dist/contract-http/flow-helpers.js +0 -34
- package/dist/contract-http/flow-helpers.js.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap attachment registry.
|
|
3
|
+
*
|
|
4
|
+
* Per contract-attachment-model.md v1.3:
|
|
5
|
+
* - `contract.bootstrap(ref, spec)` registers an overlay targeting a
|
|
6
|
+
* specific contract case (identified by testId = `${contractId}.${caseKey}`).
|
|
7
|
+
* - Each test id may have at most one bootstrap overlay. Duplicate
|
|
8
|
+
* registration throws.
|
|
9
|
+
* - Registry is consulted by the runner at runnable resolution time
|
|
10
|
+
* (single-case-execution-api §5.1 algorithm).
|
|
11
|
+
*
|
|
12
|
+
* The registry is a process-global side-effect map. Modules that export
|
|
13
|
+
* `contract.bootstrap(...)` register on evaluation. Scanner / runner are
|
|
14
|
+
* responsible for eager module loading per attachment model §7.4.
|
|
15
|
+
*/
|
|
16
|
+
import type { BootstrapAttachment, Bootstrap, ContractCaseRef } from "./contract-types.js";
|
|
17
|
+
interface BootstrapRegistration {
|
|
18
|
+
testId: string;
|
|
19
|
+
contractId: string;
|
|
20
|
+
caseKey: string;
|
|
21
|
+
protocol: string;
|
|
22
|
+
spec: Bootstrap<unknown, unknown>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register a bootstrap overlay for a specific case. Called by
|
|
26
|
+
* `contract.bootstrap(ref, spec)`.
|
|
27
|
+
*
|
|
28
|
+
* Throws if another overlay is already registered for the same testId.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerBootstrap<Needs, Params = void>(ref: ContractCaseRef<Needs, unknown>, spec: Bootstrap<Params, Needs>): BootstrapAttachment<Needs, Params>;
|
|
31
|
+
/** Look up a bootstrap registration by testId. Returns undefined if none. */
|
|
32
|
+
export declare function getBootstrap(testId: string): BootstrapRegistration | undefined;
|
|
33
|
+
/** Enumerate all registered bootstrap overlays. Used by scanner / CLI list. */
|
|
34
|
+
export declare function listBootstraps(): BootstrapRegistration[];
|
|
35
|
+
/** Test-only: clear the registry between test runs. Not part of public API. */
|
|
36
|
+
export declare function clearBootstrapRegistry(): void;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=bootstrap-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap-registry.d.ts","sourceRoot":"","sources":["../src/bootstrap-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,SAAS,EACT,eAAe,EAChB,MAAM,qBAAqB,CAAC;AAE7B,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CACnC;AAID;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EACpD,GAAG,EAAE,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,EACpC,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,GAC7B,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,CAuBpC;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,GACb,qBAAqB,GAAG,SAAS,CAEnC;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,IAAI,qBAAqB,EAAE,CAExD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap attachment registry.
|
|
3
|
+
*
|
|
4
|
+
* Per contract-attachment-model.md v1.3:
|
|
5
|
+
* - `contract.bootstrap(ref, spec)` registers an overlay targeting a
|
|
6
|
+
* specific contract case (identified by testId = `${contractId}.${caseKey}`).
|
|
7
|
+
* - Each test id may have at most one bootstrap overlay. Duplicate
|
|
8
|
+
* registration throws.
|
|
9
|
+
* - Registry is consulted by the runner at runnable resolution time
|
|
10
|
+
* (single-case-execution-api §5.1 algorithm).
|
|
11
|
+
*
|
|
12
|
+
* The registry is a process-global side-effect map. Modules that export
|
|
13
|
+
* `contract.bootstrap(...)` register on evaluation. Scanner / runner are
|
|
14
|
+
* responsible for eager module loading per attachment model §7.4.
|
|
15
|
+
*/
|
|
16
|
+
const _bootstrapRegistry = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Register a bootstrap overlay for a specific case. Called by
|
|
19
|
+
* `contract.bootstrap(ref, spec)`.
|
|
20
|
+
*
|
|
21
|
+
* Throws if another overlay is already registered for the same testId.
|
|
22
|
+
*/
|
|
23
|
+
export function registerBootstrap(ref, spec) {
|
|
24
|
+
const testId = `${ref.contractId}.${ref.caseKey}`;
|
|
25
|
+
if (_bootstrapRegistry.has(testId)) {
|
|
26
|
+
throw new Error(`contract.bootstrap: duplicate overlay for case "${testId}". ` +
|
|
27
|
+
`Only one bootstrap overlay per case is allowed. ` +
|
|
28
|
+
`If you need multiple variants, use the \`bootstrap.params\` schema.`);
|
|
29
|
+
}
|
|
30
|
+
_bootstrapRegistry.set(testId, {
|
|
31
|
+
testId,
|
|
32
|
+
contractId: ref.contractId,
|
|
33
|
+
caseKey: ref.caseKey,
|
|
34
|
+
protocol: ref.protocol,
|
|
35
|
+
spec: spec,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
__glubean_type: "bootstrap-attachment",
|
|
39
|
+
testId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Look up a bootstrap registration by testId. Returns undefined if none. */
|
|
43
|
+
export function getBootstrap(testId) {
|
|
44
|
+
return _bootstrapRegistry.get(testId);
|
|
45
|
+
}
|
|
46
|
+
/** Enumerate all registered bootstrap overlays. Used by scanner / CLI list. */
|
|
47
|
+
export function listBootstraps() {
|
|
48
|
+
return [..._bootstrapRegistry.values()];
|
|
49
|
+
}
|
|
50
|
+
/** Test-only: clear the registry between test runs. Not part of public API. */
|
|
51
|
+
export function clearBootstrapRegistry() {
|
|
52
|
+
_bootstrapRegistry.clear();
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=bootstrap-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap-registry.js","sourceRoot":"","sources":["../src/bootstrap-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAgBH,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAiC,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAoC,EACpC,IAA8B;IAE9B,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;IAElD,IAAI,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,mDAAmD,MAAM,KAAK;YAC5D,kDAAkD;YAClD,qEAAqE,CACxE,CAAC;IACJ,CAAC;IAED,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE;QAC7B,MAAM;QACN,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,IAAI,EAAE,IAAmC;KAC1C,CAAC,CAAC;IAEH,OAAO;QACL,cAAc,EAAE,sBAAsB;QACtC,MAAM;KAC+B,CAAC;AAC1C,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAC1B,MAAc;IAEd,OAAO,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACxC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,GAAG,kBAAkB,CAAC,MAAM,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,sBAAsB;IACpC,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC"}
|
package/dist/contract-core.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - `internal/40-discovery/proposals/contract-flow.md` v9
|
|
20
20
|
*/
|
|
21
21
|
import type { TestContext } from "./types.js";
|
|
22
|
-
import type { ContractProtocolAdapter, ExtractedFlowProjection, FieldMapping, FlowBuilder, FlowContract, FlowMeta, RuntimeFlowProjection } from "./contract-types.js";
|
|
22
|
+
import type { Bootstrap, BootstrapAttachment, ContractCaseRef, ContractProtocolAdapter, ExtractedFlowProjection, FieldMapping, FlowBuilder, FlowContract, FlowMeta, RuntimeFlowProjection } from "./contract-types.js";
|
|
23
23
|
/** Internal accessor for plugins / downstream (scanner, runner). */
|
|
24
24
|
export declare function getAdapter(protocol: string): ContractProtocolAdapter<any, any, any, any, any> | undefined;
|
|
25
25
|
/**
|
|
@@ -62,8 +62,20 @@ declare function register<Spec, RuntimeSchemas = unknown, RuntimeMeta = unknown,
|
|
|
62
62
|
type ContractNamespace = {
|
|
63
63
|
register: typeof register;
|
|
64
64
|
flow: typeof flow;
|
|
65
|
+
bootstrap: typeof bootstrap;
|
|
65
66
|
[protocol: string]: unknown;
|
|
66
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Register a bootstrap overlay for a contract case. Standalone-only
|
|
70
|
+
* execution path; flow NEVER invokes bootstrap (non-negotiable invariant
|
|
71
|
+
* from attachment model §0.4 / §14.0).
|
|
72
|
+
*
|
|
73
|
+
* `NoInfer<Needs>` on the spec parameter prevents TypeScript's multi-site
|
|
74
|
+
* inference from silently accepting a mismatched `run` return type —
|
|
75
|
+
* without it, TS merges inferences from ref + spec and produces a
|
|
76
|
+
* compatible Needs, masking real type errors (Spike 0 Finding 1).
|
|
77
|
+
*/
|
|
78
|
+
export declare function bootstrap<Needs, Params = void>(ref: ContractCaseRef<Needs, unknown>, spec: Bootstrap<Params, NoInfer<Needs>>): BootstrapAttachment<Needs, Params>;
|
|
67
79
|
export declare const contract: ContractNamespace;
|
|
68
80
|
/**
|
|
69
81
|
* Protocol-agnostic flow builder. See contract-flow.md v9 §4.1.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-core.d.ts","sourceRoot":"","sources":["../src/contract-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"contract-core.d.ts","sourceRoot":"","sources":["../src/contract-core.ts"],"names":[],"mappings":"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"}
|
package/dist/contract-core.js
CHANGED
|
@@ -19,6 +19,64 @@
|
|
|
19
19
|
* - `internal/40-discovery/proposals/contract-flow.md` v9
|
|
20
20
|
*/
|
|
21
21
|
import { registerTest } from "./internal.js";
|
|
22
|
+
import { getBootstrap, registerBootstrap } from "./bootstrap-registry.js";
|
|
23
|
+
import { getExplicitInput, getBootstrapInput, isForceStandalone, } from "./runner-input-channel.js";
|
|
24
|
+
/**
|
|
25
|
+
* Validate a value against a `needs` schema. Used by the v10 attachment model
|
|
26
|
+
* dispatcher after bootstrap overlay produces `resolvedInput` — before passing
|
|
27
|
+
* to `adapter.executeCase`. Keeps the invariant that adapter receives
|
|
28
|
+
* already-validated input.
|
|
29
|
+
*
|
|
30
|
+
* Handles both SchemaLike flavors (safeParse preferred, parse fallback).
|
|
31
|
+
* When neither is present, passes value through — the schema was purely
|
|
32
|
+
* type-level and carries no runtime check.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Run all registered cleanup callbacks in LIFO order. Each cleanup error is
|
|
36
|
+
* reported via console.error and does NOT mask the primary failure (the
|
|
37
|
+
* caller has already thrown or will throw whatever brought them here).
|
|
38
|
+
*
|
|
39
|
+
* Used by the v10 dispatcher's overlay path — same helper in all three
|
|
40
|
+
* failure sites (bootstrap run threw, needs validation failed, executeCase
|
|
41
|
+
* threw / succeeded). Before v3 feedback, the first two paths swallowed
|
|
42
|
+
* cleanup errors silently; this helper unifies reporting.
|
|
43
|
+
*
|
|
44
|
+
* Cleanup error reporting is `console.error` today. Spike 3 routes it
|
|
45
|
+
* through structured events so CI/agent consumers see it in test output.
|
|
46
|
+
*/
|
|
47
|
+
async function runCleanupsLifo(cleanups, testId) {
|
|
48
|
+
while (cleanups.length > 0) {
|
|
49
|
+
const cleanup = cleanups.pop();
|
|
50
|
+
try {
|
|
51
|
+
await cleanup();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error(`bootstrap cleanup error for ${testId}:`, err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function validateNeedsOutput(needsSchema, value, ctx) {
|
|
60
|
+
const sp = needsSchema.safeParse;
|
|
61
|
+
if (typeof sp === "function") {
|
|
62
|
+
const result = sp(value);
|
|
63
|
+
if (result.success)
|
|
64
|
+
return result.data;
|
|
65
|
+
const lines = result.error.issues.map((i) => ` - ${i.path?.join(".") ?? "<root>"}: ${i.message}`);
|
|
66
|
+
const sourceLabel = ctx.source === "bootstrap" ? "Bootstrap output"
|
|
67
|
+
: ctx.source === "explicit" ? "Explicit input"
|
|
68
|
+
: ctx.source === "bootstrap-params" ? "Bootstrap params"
|
|
69
|
+
: "Flow `in` output";
|
|
70
|
+
const schemaLabel = ctx.source === "bootstrap-params" ? "params schema" : "needs schema";
|
|
71
|
+
throw new Error(`${sourceLabel} for case "${ctx.testId}" does not satisfy ${schemaLabel}:\n${lines.join("\n")}`);
|
|
72
|
+
}
|
|
73
|
+
const p = needsSchema.parse;
|
|
74
|
+
if (typeof p === "function") {
|
|
75
|
+
return p(value);
|
|
76
|
+
}
|
|
77
|
+
// Schema declares neither safeParse nor parse — type-level only, pass through.
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
22
80
|
// =============================================================================
|
|
23
81
|
// Adapter registry
|
|
24
82
|
// =============================================================================
|
|
@@ -88,6 +146,10 @@ function register(protocol, adapter) {
|
|
|
88
146
|
}
|
|
89
147
|
_adapters.set(protocol, adapter);
|
|
90
148
|
// Attach contract[protocol] dispatcher dynamically.
|
|
149
|
+
// Generic `Cases` preserves per-case Needs/Output through ProtocolContract
|
|
150
|
+
// so `.case("key")` returns a properly-typed ContractCaseRef. Without this,
|
|
151
|
+
// `contract.bootstrap(ref, { run })` cannot type-check the run return
|
|
152
|
+
// against the specific case's needs.
|
|
91
153
|
contract[protocol] = (id, spec) => {
|
|
92
154
|
return dispatchContract(protocol, adapter, id, spec);
|
|
93
155
|
};
|
|
@@ -106,6 +168,11 @@ function dispatchContract(protocol, adapter, id, spec) {
|
|
|
106
168
|
};
|
|
107
169
|
// 1:1 key invariant between spec.cases and projection.cases
|
|
108
170
|
validateCaseKeys(protocol, spec.cases ?? {}, projection.cases);
|
|
171
|
+
// Forward-declared carrier reference. Populated below via Object.assign and
|
|
172
|
+
// captured by each test.fn closure so that v10 bootstrap overlay dispatch
|
|
173
|
+
// can pass `contract` to `adapter.executeCase({ ..., contract, ... })` at
|
|
174
|
+
// runtime. Safe because fn closures only execute after contractObj is set.
|
|
175
|
+
let contractObj;
|
|
109
176
|
const cases = spec.cases ?? {};
|
|
110
177
|
const contractTagsRaw = spec.tags;
|
|
111
178
|
const contractTags = Array.isArray(contractTagsRaw)
|
|
@@ -157,6 +224,201 @@ function dispatchContract(protocol, adapter, id, spec) {
|
|
|
157
224
|
ctx.skip(skipDeprecated);
|
|
158
225
|
if (skipDeferred)
|
|
159
226
|
ctx.skip(skipDeferred);
|
|
227
|
+
// ── §5.1 runnable resolution algorithm ───────────────────────────
|
|
228
|
+
//
|
|
229
|
+
// Step 1: explicit input wins over everything.
|
|
230
|
+
// - case has needs → validate, run raw with that input
|
|
231
|
+
// - case has no needs → reject (input has no schema target)
|
|
232
|
+
// Step 2: requireAttachment + no overlay → hard error
|
|
233
|
+
// (bypassed by setForceStandalone via internal channel; debug only)
|
|
234
|
+
// Step 3: overlay registered → run overlay path; bootstrap params
|
|
235
|
+
// come from getBootstrapInput; validated against overlay's
|
|
236
|
+
// `params` schema if structured form
|
|
237
|
+
// Step 4: needs declared, no overlay, no input → hard error
|
|
238
|
+
// Step 5: no needs, no overlay → run raw with no input
|
|
239
|
+
//
|
|
240
|
+
// The order below mirrors §5.1 exactly. Tests for each path live
|
|
241
|
+
// in `contract.test.ts` (search "§5.1").
|
|
242
|
+
const needsSchema = caseSpec.needs;
|
|
243
|
+
const requireAttachment = caseSpec.runnability?.requireAttachment;
|
|
244
|
+
// §5.1 Step 1 — explicit input always wins. Bootstrap overlay (even
|
|
245
|
+
// if registered) is NOT invoked. No "run bootstrap for side-effects
|
|
246
|
+
// then use my input" mode (per §5.1 invariants).
|
|
247
|
+
const explicit = getExplicitInput(testId);
|
|
248
|
+
if (explicit.has) {
|
|
249
|
+
if (!needsSchema) {
|
|
250
|
+
const requireHint = requireAttachment
|
|
251
|
+
? " Use --force-standalone for debug, or attach a bootstrap."
|
|
252
|
+
: "";
|
|
253
|
+
throw new Error(`case "${testId}" has no \`needs\` schema; explicit input is ` +
|
|
254
|
+
`meaningless (no schema to satisfy).${requireHint}`);
|
|
255
|
+
}
|
|
256
|
+
if (!contractObj) {
|
|
257
|
+
throw new Error(`Internal: contract carrier not assembled when running "${testId}".`);
|
|
258
|
+
}
|
|
259
|
+
if (!adapter.executeCase) {
|
|
260
|
+
throw new Error(`Explicit input provided for "${testId}" but the adapter for ` +
|
|
261
|
+
`protocol "${protocol}" has not been migrated to the ` +
|
|
262
|
+
`attachment model (missing executeCase method).`);
|
|
263
|
+
}
|
|
264
|
+
const validatedInput = validateNeedsOutput(needsSchema, explicit.value, { testId, source: "explicit" });
|
|
265
|
+
await adapter.executeCase({
|
|
266
|
+
ctx,
|
|
267
|
+
contract: contractObj,
|
|
268
|
+
caseKey,
|
|
269
|
+
resolvedInput: validatedInput,
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// §5.1 Step 3 — overlay path. Step 2 (requireAttachment + no
|
|
274
|
+
// overlay) is handled in the no-overlay branch below.
|
|
275
|
+
const overlay = getBootstrap(testId);
|
|
276
|
+
if (overlay) {
|
|
277
|
+
// Hard error if adapter hasn't implemented the attachment-model
|
|
278
|
+
// entry point. Better to fail loudly at dispatch than to run the
|
|
279
|
+
// raw case as if no overlay existed.
|
|
280
|
+
if (!adapter.executeCase) {
|
|
281
|
+
throw new Error(`Bootstrap overlay registered for "${testId}" but the adapter ` +
|
|
282
|
+
`for protocol "${protocol}" has not been migrated to the ` +
|
|
283
|
+
`attachment model (missing executeCase method). The overlay ` +
|
|
284
|
+
`would be silently ignored. Either remove the overlay or ` +
|
|
285
|
+
`migrate the adapter runtime.`);
|
|
286
|
+
}
|
|
287
|
+
if (!contractObj) {
|
|
288
|
+
// Unreachable at fn-call time (contractObj is assigned before
|
|
289
|
+
// any test.fn runs). Defensive check keeps TS narrowing clean.
|
|
290
|
+
throw new Error(`Internal: contract carrier not assembled when running "${testId}".`);
|
|
291
|
+
}
|
|
292
|
+
const cleanups = [];
|
|
293
|
+
const bootstrapCtx = Object.assign(Object.create(ctx), ctx, {
|
|
294
|
+
cleanup(fn) {
|
|
295
|
+
cleanups.push(fn);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
const runFn = typeof overlay.spec === "function"
|
|
299
|
+
? overlay.spec
|
|
300
|
+
: overlay.spec.run;
|
|
301
|
+
// §5.1 step 3a-3b: structured-form `params` schema validation.
|
|
302
|
+
// Bootstrap input comes from the runner channel (CLI
|
|
303
|
+
// `--bootstrap-json`, MCP `bootstrapInput`, programmatic
|
|
304
|
+
// `bootstrapInput`).
|
|
305
|
+
const paramsSchema = typeof overlay.spec === "object" && overlay.spec !== null
|
|
306
|
+
? overlay.spec.params
|
|
307
|
+
: undefined;
|
|
308
|
+
const providedBootstrap = getBootstrapInput(testId);
|
|
309
|
+
// Plain-function overlay (or structured form without `params`)
|
|
310
|
+
// does NOT accept bootstrap input. If the runner supplied one,
|
|
311
|
+
// the user almost certainly forgot to declare a `params` schema
|
|
312
|
+
// or mistyped the testId — silently dropping the input would
|
|
313
|
+
// mask the bug. Hard-error before invoking run().
|
|
314
|
+
if (!paramsSchema && providedBootstrap.has) {
|
|
315
|
+
throw new Error(`case "${testId}": runner supplied bootstrap input but the ` +
|
|
316
|
+
`registered overlay does not declare a \`params\` schema. ` +
|
|
317
|
+
`Plain-function overlays (and structured overlays without ` +
|
|
318
|
+
`\`params\`) cannot consume bootstrap input — declare ` +
|
|
319
|
+
`\`params: SchemaLike<...>\` on the overlay spec, or remove ` +
|
|
320
|
+
`the bootstrap input from the runner invocation.`);
|
|
321
|
+
}
|
|
322
|
+
let validatedParams = undefined;
|
|
323
|
+
if (paramsSchema) {
|
|
324
|
+
// §5.1 step 3a: "Take bootstrapInput from runner options
|
|
325
|
+
// (may be empty if no params)". Validation happens whether
|
|
326
|
+
// or not the runner supplied input — schema may have
|
|
327
|
+
// defaults, or all fields may be optional.
|
|
328
|
+
validatedParams = validateNeedsOutput(paramsSchema, providedBootstrap.has ? providedBootstrap.value : undefined, { testId, source: "bootstrap-params" });
|
|
329
|
+
}
|
|
330
|
+
let resolvedInput;
|
|
331
|
+
try {
|
|
332
|
+
resolvedInput = await runFn(bootstrapCtx, validatedParams);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
await runCleanupsLifo(cleanups, testId);
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
// v10 Phase 2b Step 2: validate bootstrap output against `needs`
|
|
339
|
+
// schema before handing off to adapter. Adapter's contract is
|
|
340
|
+
// "receives already-validated input"; validation at the runner
|
|
341
|
+
// boundary matches single-case-execution-api.md §7.
|
|
342
|
+
if (needsSchema) {
|
|
343
|
+
try {
|
|
344
|
+
resolvedInput = validateNeedsOutput(needsSchema, resolvedInput, { testId, source: "bootstrap" });
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
await runCleanupsLifo(cleanups, testId);
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await adapter.executeCase({
|
|
353
|
+
ctx,
|
|
354
|
+
contract: contractObj,
|
|
355
|
+
caseKey,
|
|
356
|
+
resolvedInput,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
await runCleanupsLifo(cleanups, testId);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// ── No-overlay standalone path ───────────────────────────────────
|
|
365
|
+
//
|
|
366
|
+
// First: bootstrap input supplied but no overlay registered. The
|
|
367
|
+
// user provided `--bootstrap-json` / `bootstrapInput` for a case
|
|
368
|
+
// that has no `contract.bootstrap()`. Almost always a typo or a
|
|
369
|
+
// missing overlay file; silently dropping the input would mask
|
|
370
|
+
// the bug. Hard-error before any other resolution.
|
|
371
|
+
const providedBootstrapNoOverlay = getBootstrapInput(testId);
|
|
372
|
+
if (providedBootstrapNoOverlay.has) {
|
|
373
|
+
throw new Error(`case "${testId}": runner supplied bootstrap input but no ` +
|
|
374
|
+
`bootstrap overlay is registered for this case. Register an ` +
|
|
375
|
+
`overlay via \`contract.bootstrap(ref, { params, run })\`, ` +
|
|
376
|
+
`or remove the bootstrap input from the runner invocation. ` +
|
|
377
|
+
`(If you meant to bypass the overlay and pass case input ` +
|
|
378
|
+
`directly, use --input-json / inputJson instead.)`);
|
|
379
|
+
}
|
|
380
|
+
// §5.1 step 2: requireAttachment + no overlay → hard error
|
|
381
|
+
// (UNLESS isForceStandalone(testId) — debug bypass per §6.3).
|
|
382
|
+
// §5.1 step 4: needs declared, no overlay, no input → hard error.
|
|
383
|
+
// §5.1 step 5: no needs, no overlay → run raw with no input.
|
|
384
|
+
//
|
|
385
|
+
// Order matters: step 2 fires before step 4 because requireAttachment
|
|
386
|
+
// is the stronger declaration ("don't run bare under any input
|
|
387
|
+
// condition"). A case with both needs + requireAttachment + no
|
|
388
|
+
// overlay + no input gets the requireAttachment error message,
|
|
389
|
+
// which carries the more actionable hint (attach a bootstrap or
|
|
390
|
+
// pass --input-json depending on needs presence).
|
|
391
|
+
if (requireAttachment) {
|
|
392
|
+
if (isForceStandalone(testId)) {
|
|
393
|
+
// §6.3 debug escape valve. Author opted out of the guard via
|
|
394
|
+
// --force-standalone; emit a runtime warning so it's visible
|
|
395
|
+
// in test output, then fall through to step 4/5.
|
|
396
|
+
// eslint-disable-next-line no-console
|
|
397
|
+
console.warn(`⚠ case "${testId}": bypassing runnability.requireAttachment ` +
|
|
398
|
+
`via --force-standalone (debug). Do not use this in normal runs.`);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
throw new Error(`case "${testId}" sets \`runnability.requireAttachment: true\` ` +
|
|
402
|
+
`but no bootstrap overlay is registered. Per attachment model ` +
|
|
403
|
+
`§7.2: register a bootstrap overlay via \`contract.bootstrap(...)\`, ` +
|
|
404
|
+
`attach the case to a flow step, or pass \`--input-json\` (if ` +
|
|
405
|
+
`the case declares \`needs\`). Raw execution is disabled for ` +
|
|
406
|
+
`this case by its own declaration.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// §5.1 step 4: needs case with no overlay + no input → hard error.
|
|
410
|
+
// (Step 1 already returned for cases with explicit input above.)
|
|
411
|
+
if (needsSchema) {
|
|
412
|
+
throw new Error(`case "${testId}" declares \`needs\` but has no bootstrap overlay ` +
|
|
413
|
+
`and no explicit input. Per attachment model §5.1: register a ` +
|
|
414
|
+
`bootstrap overlay via \`contract.bootstrap(...)\`, or pass ` +
|
|
415
|
+
`\`--input-json\` to provide explicit input.`);
|
|
416
|
+
}
|
|
417
|
+
// §5.1 step 5: no needs, no overlay (or requireAttachment+force-
|
|
418
|
+
// standalone) → run case with no input. Static body / headers /
|
|
419
|
+
// params / query work as-is; function-valued action fields receive
|
|
420
|
+
// undefined (author responsibility — typically a no-needs case has
|
|
421
|
+
// no function-valued fields).
|
|
160
422
|
await adapter.execute(ctx, caseSpec, spec);
|
|
161
423
|
},
|
|
162
424
|
};
|
|
@@ -205,14 +467,18 @@ function dispatchContract(protocol, adapter, id, spec) {
|
|
|
205
467
|
// implement is a compile error, not a silent runtime hole.
|
|
206
468
|
const extracted = adapter.normalize(enrichedProjection);
|
|
207
469
|
const arr = [...tests];
|
|
208
|
-
|
|
470
|
+
contractObj = Object.assign(arr, {
|
|
209
471
|
_projection: enrichedProjection,
|
|
210
472
|
_extracted: extracted,
|
|
211
473
|
_spec: spec,
|
|
212
474
|
case(key) {
|
|
213
|
-
//
|
|
214
|
-
// function-valued body/params/
|
|
215
|
-
|
|
475
|
+
// v10: .case(key) is pure case-ref creation — no flow-only validation
|
|
476
|
+
// here. Flow-safety checks (e.g. HTTP's "function-valued body/params/
|
|
477
|
+
// headers can't be resolved in flow mode") moved to FlowBuilder.step()
|
|
478
|
+
// where the ref's flow usage is actually being declared. The same
|
|
479
|
+
// ref used via contract.bootstrap(overlay) legitimately uses
|
|
480
|
+
// function-valued fields with `resolvedInput` — v9's
|
|
481
|
+
// validateCaseForFlow incorrectly rejected that at .case() time.
|
|
216
482
|
return makeContractCaseRef(protocol, id, projection.target, key, contractObj, spec);
|
|
217
483
|
},
|
|
218
484
|
});
|
|
@@ -261,9 +527,23 @@ function makeContractCaseRef(protocol, contractId, target, caseKey, contract, _s
|
|
|
261
527
|
contract,
|
|
262
528
|
};
|
|
263
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Register a bootstrap overlay for a contract case. Standalone-only
|
|
532
|
+
* execution path; flow NEVER invokes bootstrap (non-negotiable invariant
|
|
533
|
+
* from attachment model §0.4 / §14.0).
|
|
534
|
+
*
|
|
535
|
+
* `NoInfer<Needs>` on the spec parameter prevents TypeScript's multi-site
|
|
536
|
+
* inference from silently accepting a mismatched `run` return type —
|
|
537
|
+
* without it, TS merges inferences from ref + spec and produces a
|
|
538
|
+
* compatible Needs, masking real type errors (Spike 0 Finding 1).
|
|
539
|
+
*/
|
|
540
|
+
export function bootstrap(ref, spec) {
|
|
541
|
+
return registerBootstrap(ref, spec);
|
|
542
|
+
}
|
|
264
543
|
export const contract = {
|
|
265
544
|
register,
|
|
266
545
|
flow,
|
|
546
|
+
bootstrap,
|
|
267
547
|
};
|
|
268
548
|
// =============================================================================
|
|
269
549
|
// Flow: FlowBuilder + runFlow + normalizeFlow + tracePureFn
|
|
@@ -304,6 +584,14 @@ export function flow(idOrMeta) {
|
|
|
304
584
|
throw new Error(`contract.flow(${JSON.stringify(meta.id)}).step: adapter for "${ref.protocol}" ` +
|
|
305
585
|
`does not implement executeCaseInFlow — this protocol cannot appear in a flow.`);
|
|
306
586
|
}
|
|
587
|
+
// v10: flow-safety validation fires here (at step-declaration time),
|
|
588
|
+
// not at .case() time. The ref itself is pure; only when it enters a
|
|
589
|
+
// flow do we enforce adapter-specific rules like HTTP's "function-
|
|
590
|
+
// valued body/params/headers can't resolve in flow mode". This lets
|
|
591
|
+
// contract.bootstrap(ref) attach to the same ref (where function-
|
|
592
|
+
// valued fields legitimately receive resolvedInput).
|
|
593
|
+
const contractRef = ref.contract;
|
|
594
|
+
adapter.validateCaseForFlow?.(contractRef._spec, ref.caseKey, ref.contractId);
|
|
307
595
|
const step = {
|
|
308
596
|
kind: "contract-call",
|
|
309
597
|
name: bindings?.name,
|
|
@@ -461,7 +749,43 @@ export async function runFlow(flowContract, ctx) {
|
|
|
461
749
|
throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
|
|
462
750
|
`adapter for "${step.ref.protocol}" does not implement executeCaseInFlow`);
|
|
463
751
|
}
|
|
464
|
-
|
|
752
|
+
// v10 Phase 2d Step 2+3 follow-up (RFR v2 / v2.1 P1): enforce `needs`
|
|
753
|
+
// schema at flow boundary, not just standalone. The conditional-tuple
|
|
754
|
+
// `step()` signature catches TS authoring shape mismatches, but runtime
|
|
755
|
+
// validation is the only line of defense against:
|
|
756
|
+
// - `as any` / JS callers that bypass the TS check
|
|
757
|
+
// - Zod parse / coerce / default semantics (schema can transform
|
|
758
|
+
// the value, not just validate it)
|
|
759
|
+
// - State drift producing invalid values at runtime despite valid
|
|
760
|
+
// authoring types
|
|
761
|
+
// Standalone overlay path already validates via the same helper in
|
|
762
|
+
// `dispatchContract`'s test.fn closure; flow mirrors that to keep
|
|
763
|
+
// `needs` a true contract semantic rather than a TS-only boundary.
|
|
764
|
+
//
|
|
765
|
+
// Two-branch guard:
|
|
766
|
+
// needs present + bindings.in missing → throw (contract requires input)
|
|
767
|
+
// needs present + bindings.in present → validate unconditionally
|
|
768
|
+
// needs absent → no guard
|
|
769
|
+
const contractSpec = step.contract._spec;
|
|
770
|
+
const caseSpec = contractSpec?.cases?.[step.caseKey];
|
|
771
|
+
const needsSchema = caseSpec?.needs;
|
|
772
|
+
const hasIn = typeof step.bindings?.in === "function";
|
|
773
|
+
if (needsSchema && !hasIn) {
|
|
774
|
+
throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
|
|
775
|
+
`case "${step.ref.contractId}.${step.caseKey}" declares \`needs\` ` +
|
|
776
|
+
`but the step has no \`bindings.in\`. The TypeScript conditional ` +
|
|
777
|
+
`tuple on FlowBuilder.step() usually prevents this at compile time; ` +
|
|
778
|
+
`this runtime check catches \`as any\` / JS bypass. Provide an ` +
|
|
779
|
+
`\`in: (state) => <logical input>\` binding or remove the \`needs\` ` +
|
|
780
|
+
`schema from the case.`);
|
|
781
|
+
}
|
|
782
|
+
let resolvedInputs = step.bindings?.in?.(state);
|
|
783
|
+
if (needsSchema) {
|
|
784
|
+
resolvedInputs = validateNeedsOutput(needsSchema, resolvedInputs, {
|
|
785
|
+
testId: `${step.ref.contractId}.${step.caseKey}`,
|
|
786
|
+
source: "flow",
|
|
787
|
+
});
|
|
788
|
+
}
|
|
465
789
|
const response = await adapter.executeCaseInFlow({
|
|
466
790
|
ctx,
|
|
467
791
|
contract: step.contract,
|