@ripplo/testing 0.4.2 → 0.4.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/README.md CHANGED
@@ -75,7 +75,7 @@ export const dataProject = precondition("data:project")
75
75
  export const preconditions = { authLoggedIn, dataProject };
76
76
  ```
77
77
 
78
- `precondition(name)` → `.description()` → `.requires()` (optional) → `.contract<T>()`. Each field in `T` must be a `string` (run-scoped value).
78
+ `precondition(name)` → `.description()` → `.requires()` (optional) → `.contract<T>()`. Each field in `T` must be a primitive — `string`, `number`, or `boolean` (run-scoped value).
79
79
 
80
80
  ### Observers
81
81
 
@@ -91,6 +91,25 @@ export const orgNameIs = observer("org:name-is")
91
91
  export const observers = { orgNameIs };
92
92
  ```
93
93
 
94
+ Observer `.input<T>()` and precondition `.contract<T>()` fields can be any primitive — `string`, `number`, or `boolean`. Model each field as the type it actually is; no stringification at the boundary.
95
+
96
+ ```typescript
97
+ export const orgOverageCapIs = observer("org:overage-cap-is")
98
+ .description("Org has the given overageCapCents")
99
+ .budget("fast")
100
+ .input<{ orgId: string; expectedCapCents: number }>()
101
+ .contract();
102
+
103
+ export const orgHasLogo = observer("org:has-logo")
104
+ .description("Organization logo is set / unset")
105
+ .budget("fast")
106
+ .input<{ orgId: string; expectLogo: boolean }>()
107
+ .contract();
108
+
109
+ assert.backend(orgOverageCapIs, { orgId: project.orgId, expectedCapCents: 2500 });
110
+ assert.backend(orgHasLogo, { orgId: project.orgId, expectLogo: true });
111
+ ```
112
+
94
113
  **Budget tiers** (poll behavior, not numeric timeouts):
95
114
 
96
115
  | Tier | Window | Backoff | Use for |
@@ -275,11 +294,13 @@ export const engine = createEngine(ripplo, {
275
294
  `ctx` passed to each precondition `setup`:
276
295
 
277
296
  - `ctx.runId` — unique 12-char id for this run
278
- - `ctx.fixed(value)` — static test value
297
+ - `ctx.fixed<T extends string | number | boolean>(value: T)` — static test value (any primitive)
279
298
  - `ctx.uniqueId(prefix)` — `ripplo-test-<prefix>-<runId>`
280
299
  - `ctx.uniqueEmail()` — `ripplo-test-<runId>@test.ripplo.ai`
281
300
  - `ctx.setCookie(name, value, options?)` — forwarded to the test browser as `Set-Cookie`
282
301
 
302
+ Helpers return plain primitives — interpolate, JSON.stringify, or pass through observer params directly. The return type is type-branded, so a hardcoded literal in a `setup` return fails at compile time.
303
+
283
304
  ### Observer context
284
305
 
285
306
  - `ctx.pass()` — assertion satisfied; stop polling.
package/dist/assert.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as ObserverHandle, a as ObserverInput, b as ObserverBudgetTier } from './types-B7YljrTz.js';
1
+ import { O as ObserverHandle, a as ObserverInput, P as Primitive, b as ObserverBudgetTier } from './types-2i1Sm7_J.js';
2
2
  import { Variable, StaticStringRef, VariableRef } from './control.js';
3
3
  import { U as UnlabeledStep } from './step-De52hTLd.js';
4
4
  import { CheckLocator, AnyLocator } from './locators.js';
@@ -72,12 +72,12 @@ declare const assert: {
72
72
  * polling) and `ctx.fail(reason)` only for invariant violations (stop
73
73
  * early). The observer's `.budget()` governs poll interval and timeout.
74
74
  */
75
- backend<THandle extends ObserverHandle>(observer: THandle, params: ObserverInput<THandle> & Record<string, string>): UnlabeledStep<{
75
+ backend<THandle extends ObserverHandle>(observer: THandle, params: ObserverInput<THandle> & Record<string, Primitive>): UnlabeledStep<{
76
76
  budget: ObserverBudgetTier;
77
77
  observer: string;
78
78
  params: Record<string, {
79
79
  readonly type: "static";
80
- readonly value: string;
80
+ readonly value: Primitive;
81
81
  }>;
82
82
  type: "assertObserver";
83
83
  }>;
package/dist/assert.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readObserverBudget,
3
3
  readObserverName
4
- } from "./chunk-SBZJDJP4.js";
4
+ } from "./chunk-YQAEOH5W.js";
5
5
  import {
6
6
  toSpecLocator,
7
7
  toStringValueRef
@@ -48,7 +48,7 @@ var assert = {
48
48
  return createStep({
49
49
  budget: readObserverBudget(observer),
50
50
  observer: readObserverName(observer),
51
- params: paramsToStringRefs(params),
51
+ params: paramsToValueRefs(params),
52
52
  type: "assertObserver"
53
53
  });
54
54
  },
@@ -99,7 +99,7 @@ var assert = {
99
99
  return createStep({ locator: toSpecLocator(locator), type: "assertVisible" });
100
100
  }
101
101
  };
102
- function paramsToStringRefs(params) {
102
+ function paramsToValueRefs(params) {
103
103
  const out = {};
104
104
  Object.entries(params).forEach(([key, value]) => {
105
105
  out[key] = { type: "static", value };
@@ -1,4 +1,4 @@
1
- import { O as ObserverHandle, P as Precondition, k as PreconditionData, i as TestDefinition, f as ObserverDefinition, j as PreconditionDefinition, U as UnimplementedItems, e as ObserverContext, g as ObserverOutcome, S as SetupContext, l as TestValue, T as TeardownContext } from './types-B7YljrTz.js';
1
+ import { P as Primitive, O as ObserverHandle, h as Precondition, m as PreconditionData, j as TestDefinition, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, e as ObserverContext, g as ObserverOutcome, S as SetupContext, k as TestValue, T as TeardownContext } from './types-2i1Sm7_J.js';
2
2
  import { ObserverBudget } from '@ripplo/spec';
3
3
  import { S as Step } from './step-De52hTLd.js';
4
4
 
@@ -15,9 +15,9 @@ interface ObserverNeedsBudget {
15
15
  readonly budget: (tier: ObserverBudget) => ObserverNeedsInput;
16
16
  }
17
17
  interface ObserverNeedsInput {
18
- readonly input: <TInput extends Record<string, string>>() => ObserverReady<TInput>;
18
+ readonly input: <TInput extends Record<string, Primitive>>() => ObserverReady<TInput>;
19
19
  }
20
- interface ObserverReady<TInput extends Record<string, string>> {
20
+ interface ObserverReady<TInput extends Record<string, Primitive>> {
21
21
  readonly contract: () => ObserverHandle<TInput>;
22
22
  }
23
23
 
@@ -31,12 +31,12 @@ type ResolveDeps<TDeps extends PreconditionRecord> = {
31
31
  readonly [K in keyof TDeps]: PreconditionData<TDeps[K]>;
32
32
  };
33
33
  interface PreconditionNeedsSetup {
34
- readonly contract: <TData extends Record<string, string>>() => Precondition<TData>;
34
+ readonly contract: <TData extends Record<string, Primitive>>() => Precondition<TData>;
35
35
  readonly description: (text: string) => PreconditionNeedsSetup;
36
36
  readonly requires: <TDeps extends PreconditionRecord>(deps: TDeps) => PreconditionNeedsSetupWithDeps<TDeps>;
37
37
  }
38
38
  interface PreconditionNeedsSetupWithDeps<TDeps extends PreconditionRecord> {
39
- readonly contract: <TData extends Record<string, string>>() => Precondition<TData, ResolveDeps<TDeps>>;
39
+ readonly contract: <TData extends Record<string, Primitive>>() => Precondition<TData, ResolveDeps<TDeps>>;
40
40
  }
41
41
  interface TestNeedsName {
42
42
  readonly name: (displayName: string) => TestNeedsRequires;
@@ -52,11 +52,11 @@ interface TestNeedsRequires {
52
52
  */
53
53
  readonly requires: <TReqs extends PreconditionRecord>(reqs: TReqs) => TestNeedsOutcome<ResolveDeps<TReqs>>;
54
54
  }
55
- interface TestNeedsOutcome<TVars extends Record<string, Record<string, string>>> {
55
+ interface TestNeedsOutcome<TVars extends Record<string, Record<string, Primitive>>> {
56
56
  readonly expectedOutcome: (text: string) => TestNeedsStartsAt<TVars>;
57
57
  }
58
58
 
59
- interface TestNeedsStartsAt<TVars extends Record<string, Record<string, string>>> {
59
+ interface TestNeedsStartsAt<TVars extends Record<string, Record<string, Primitive>>> {
60
60
  /**
61
61
  * Skip implementation — compiles as a planning stub and is reported in
62
62
  * `getUnimplemented()`. The test appears in the lockfile but is not run.
@@ -69,7 +69,7 @@ interface TestNeedsStartsAt<TVars extends Record<string, Record<string, string>>
69
69
  */
70
70
  readonly startsAt: (fn: (vars: TVars) => string) => TestNeedsSteps<TVars>;
71
71
  }
72
- interface TestNeedsSteps<TVars extends Record<string, Record<string, string>>> {
72
+ interface TestNeedsSteps<TVars extends Record<string, Record<string, Primitive>>> {
73
73
  /**
74
74
  * Declare the ordered step list. Receives destructured precondition data as
75
75
  * typed proxies (same as `.startsAt`). Every step must be labeled with
@@ -87,19 +87,19 @@ interface TestNeedsCoverage {
87
87
  readonly coverage: (...ids: ReadonlyArray<CoverageStatementId>) => TestDefinition;
88
88
  }
89
89
 
90
- type PreconditionSetupResult<TData extends Record<string, string>> = {
91
- readonly [K in keyof TData]: TestValue;
90
+ type PreconditionSetupResult<TData extends Record<string, Primitive>> = {
91
+ readonly [K in keyof TData]: TestValue<TData[K]>;
92
92
  };
93
- interface PreconditionImpl<TData extends Record<string, string>, TDeps extends Record<string, Record<string, string>>> {
93
+ interface PreconditionImpl<TData extends Record<string, Primitive>, TDeps extends Record<string, Record<string, Primitive>>> {
94
94
  readonly setup: (ctx: SetupContext, deps: TDeps) => Promise<PreconditionSetupResult<TData>>;
95
95
  readonly teardown: (ctx: TeardownContext<TData>) => Promise<void>;
96
96
  }
97
- type ObserverImplFn<TInput extends Record<string, string>> = (ctx: ObserverContext, params: TInput) => Promise<ObserverOutcome>;
97
+ type ObserverImplFn<TInput extends Record<string, Primitive>> = (ctx: ObserverContext, params: TInput) => Promise<ObserverOutcome>;
98
98
  interface TestOptions {
99
99
  readonly uiOnly?: boolean;
100
100
  }
101
101
  type PreconditionRegistry = Record<string, Precondition>;
102
- type ObserverRegistry = Record<string, ObserverHandle<Record<string, string>>>;
102
+ type ObserverRegistry = Record<string, ObserverHandle<Record<string, Primitive>>>;
103
103
  interface RipploRegistries<P extends PreconditionRegistry, O extends ObserverRegistry> {
104
104
  readonly observers: O;
105
105
  readonly preconditions: P;
@@ -19,7 +19,7 @@ var teardownRequestSchema = z.object({
19
19
  });
20
20
  var observerRequestSchema = z.object({
21
21
  observer: z.string().min(1).max(200),
22
- params: z.record(z.string().max(200), z.string())
22
+ params: z.record(z.string().max(200), z.union([z.string(), z.number(), z.boolean()]))
23
23
  });
24
24
  var observerOutcomeSchema = z.discriminatedUnion("kind", [
25
25
  z.object({ kind: z.literal("pass") }),
@@ -59,11 +59,12 @@ var DEFAULT_IGNORE_PATHS = [
59
59
  "**/prisma/migrations/**",
60
60
  "**/scripts/**"
61
61
  ];
62
- function readTestValue(value) {
63
- return value.value;
62
+ function brandTestValue(value) {
63
+ return value;
64
64
  }
65
- function createTestValue(value) {
66
- return { value };
65
+ function isPrimitive(value) {
66
+ const t = typeof value;
67
+ return t === "string" || t === "number" || t === "boolean";
67
68
  }
68
69
  function readPreconditionName(p) {
69
70
  return p.name;
@@ -97,8 +98,8 @@ function makeObserverHandle({
97
98
  export {
98
99
  DEFAULT_WATCH_PATHS,
99
100
  DEFAULT_IGNORE_PATHS,
100
- readTestValue,
101
- createTestValue,
101
+ brandTestValue,
102
+ isPrimitive,
102
103
  readPreconditionName,
103
104
  readPreconditionDescription,
104
105
  readPreconditionDependsOn,
@@ -1,6 +1,6 @@
1
1
  import { Observer, Precondition, WorkflowSpec } from '@ripplo/spec';
2
- import { d as RipploBuilder } from './builder-0oT23S0W.js';
3
- import './types-B7YljrTz.js';
2
+ import { d as RipploBuilder } from './builder-mhjAdyNW.js';
3
+ import './types-2i1Sm7_J.js';
4
4
  import './step-De52hTLd.js';
5
5
 
6
6
  interface CompileResult {
package/dist/elysia.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
3
- import './builder-0oT23S0W.js';
4
- import './types-B7YljrTz.js';
2
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
3
+ import './builder-mhjAdyNW.js';
4
+ import './types-2i1Sm7_J.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/elysia.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serializeCookie,
7
7
  teardownRequestSchema,
8
8
  verifyWebhookSignature
9
- } from "./chunk-UFHSNW4E.js";
9
+ } from "./chunk-EHAZBEHO.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/elysia.ts
@@ -1,9 +1,9 @@
1
- import { c as PreconditionRegistry, a as ObserverRegistry, O as ObserverImplFn, P as PreconditionImpl, e as RipploInstance } from './builder-0oT23S0W.js';
2
- import { g as ObserverOutcome, C as CookieEntry, f as ObserverDefinition, j as PreconditionDefinition, U as UnimplementedItems, O as ObserverHandle, P as Precondition } from './types-B7YljrTz.js';
1
+ import { c as PreconditionRegistry, a as ObserverRegistry, O as ObserverImplFn, P as PreconditionImpl, e as RipploInstance } from './builder-mhjAdyNW.js';
2
+ import { P as Primitive, g as ObserverOutcome, C as CookieEntry, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, O as ObserverHandle, h as Precondition } from './types-2i1Sm7_J.js';
3
3
 
4
4
  interface EngineResult {
5
5
  readonly cookies: ReadonlyArray<CookieEntry>;
6
- readonly data: Record<string, Record<string, string>>;
6
+ readonly data: Record<string, Record<string, Primitive>>;
7
7
  readonly error: string | undefined;
8
8
  readonly executed: ReadonlyArray<string>;
9
9
  readonly runId: string;
@@ -18,12 +18,12 @@ interface ObserverExecutionResult {
18
18
  readonly success: boolean;
19
19
  }
20
20
  interface RipploEngine {
21
- readonly executeObserver: (name: string, params: Record<string, string>) => Promise<ObserverExecutionResult>;
21
+ readonly executeObserver: (name: string, params: Record<string, Primitive>) => Promise<ObserverExecutionResult>;
22
22
  readonly executePreconditions: (names: ReadonlyArray<string>, options?: ExecuteBatchOptions) => Promise<EngineResult>;
23
23
  readonly getObservers: () => ReadonlyArray<ObserverDefinition>;
24
24
  readonly getPreconditions: () => ReadonlyArray<PreconditionDefinition>;
25
25
  readonly getUnimplemented: () => UnimplementedItems;
26
- readonly teardown: (names: ReadonlyArray<string>, data: Record<string, Record<string, string>>) => Promise<void>;
26
+ readonly teardown: (names: ReadonlyArray<string>, data: Record<string, Record<string, Primitive>>) => Promise<void>;
27
27
  }
28
28
  declare const NOT_IMPLEMENTED_BRAND: unique symbol;
29
29
  interface NotImplemented {
@@ -32,7 +32,7 @@ interface NotImplemented {
32
32
  }
33
33
  declare function notImplemented(reason?: string): NotImplemented;
34
34
  type PreconditionImplFor<P> = P extends Precondition<infer TData, infer TDeps> ? PreconditionImpl<TData, TDeps> : never;
35
- type ObserverImplFnFor<O> = O extends ObserverHandle<infer TInput> ? TInput extends Record<string, string> ? ObserverImplFn<TInput> : never : never;
35
+ type ObserverImplFnFor<O> = O extends ObserverHandle<infer TInput> ? TInput extends Record<string, Primitive> ? ObserverImplFn<TInput> : never : never;
36
36
  interface EngineImpls<P extends PreconditionRegistry, O extends ObserverRegistry> {
37
37
  readonly observers: {
38
38
  readonly [K in keyof O]: NotImplemented | ObserverImplFnFor<O[K]>;
package/dist/express.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Router } from 'express';
2
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
3
- import './builder-0oT23S0W.js';
4
- import './types-B7YljrTz.js';
2
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
3
+ import './builder-mhjAdyNW.js';
4
+ import './types-2i1Sm7_J.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/express.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  serializeCookie,
6
6
  teardownRequestSchema,
7
7
  verifyWebhookSignature
8
- } from "./chunk-UFHSNW4E.js";
8
+ } from "./chunk-EHAZBEHO.js";
9
9
  import "./chunk-4MGIQFAJ.js";
10
10
 
11
11
  // src/adapters/express.ts
package/dist/fastify.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FastifyInstance } from 'fastify';
2
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
3
- import './builder-0oT23S0W.js';
4
- import './types-B7YljrTz.js';
2
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
3
+ import './builder-mhjAdyNW.js';
4
+ import './types-2i1Sm7_J.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/fastify.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serializeCookie,
7
7
  teardownRequestSchema,
8
8
  verifyWebhookSignature
9
- } from "./chunk-UFHSNW4E.js";
9
+ } from "./chunk-EHAZBEHO.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/fastify.ts
package/dist/hono.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
3
- import './builder-0oT23S0W.js';
4
- import './types-B7YljrTz.js';
2
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
3
+ import './builder-mhjAdyNW.js';
4
+ import './types-2i1Sm7_J.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/hono.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serializeCookie,
7
7
  teardownRequestSchema,
8
8
  verifyWebhookSignature
9
- } from "./chunk-UFHSNW4E.js";
9
+ } from "./chunk-EHAZBEHO.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/hono.ts
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export { C as CoverageRegistry, O as ObserverImplFn, a as ObserverRegistry, P as PreconditionImpl, b as PreconditionRecord, c as PreconditionRegistry, R as ResolveDeps, d as RipploBuilder, e as RipploInstance, f as RipploRegistries, g as createRipplo, o as observer, p as precondition, t as test } from './builder-0oT23S0W.js';
1
+ export { C as CoverageRegistry, O as ObserverImplFn, a as ObserverRegistry, P as PreconditionImpl, b as PreconditionRecord, c as PreconditionRegistry, R as ResolveDeps, d as RipploBuilder, e as RipploInstance, f as RipploRegistries, g as createRipplo, o as observer, p as precondition, t as test } from './builder-mhjAdyNW.js';
2
2
  import { CompileResult } from './compiler.js';
3
3
  export { CompiledTest, compile } from './compiler.js';
4
- export { E as EngineImpls, a as EngineResult, b as ExecuteBatchOptions, N as NotImplemented, O as ObserverImplFnFor, P as PreconditionImplFor, R as RipploEngine, c as createEngine, n as notImplemented } from './engine-BdKDGBYw.js';
5
- import { C as CookieEntry } from './types-B7YljrTz.js';
6
- export { c as CookieOptions, D as DEFAULT_IGNORE_PATHS, d as DEFAULT_WATCH_PATHS, e as ObserverContext, f as ObserverDefinition, O as ObserverHandle, a as ObserverInput, g as ObserverOutcome, P as Precondition, h as PreconditionDeps, S as SetupContext, T as TeardownContext, i as TestDefinition } from './types-B7YljrTz.js';
4
+ export { E as EngineImpls, a as EngineResult, b as ExecuteBatchOptions, N as NotImplemented, O as ObserverImplFnFor, P as PreconditionImplFor, R as RipploEngine, c as createEngine, n as notImplemented } from './engine-B6MM4AEw.js';
5
+ import { C as CookieEntry } from './types-2i1Sm7_J.js';
6
+ export { c as CookieOptions, D as DEFAULT_IGNORE_PATHS, d as DEFAULT_WATCH_PATHS, e as ObserverContext, f as ObserverDefinition, O as ObserverHandle, a as ObserverInput, g as ObserverOutcome, h as Precondition, i as PreconditionDeps, P as Primitive, S as SetupContext, T as TeardownContext, j as TestDefinition, k as TestValue } from './types-2i1Sm7_J.js';
7
7
  export { D as DslNodeInput } from './step-De52hTLd.js';
8
8
  import '@ripplo/spec';
9
9
 
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  DEFAULT_IGNORE_PATHS,
3
3
  DEFAULT_WATCH_PATHS,
4
- createTestValue,
4
+ brandTestValue,
5
+ isPrimitive,
5
6
  makeObserverHandle,
6
7
  readObserverBudget,
7
8
  readObserverDescription,
@@ -9,9 +10,8 @@ import {
9
10
  readPreconditionDepMapping,
10
11
  readPreconditionDependsOn,
11
12
  readPreconditionDescription,
12
- readPreconditionName,
13
- readTestValue
14
- } from "./chunk-SBZJDJP4.js";
13
+ readPreconditionName
14
+ } from "./chunk-YQAEOH5W.js";
15
15
  import {
16
16
  compile
17
17
  } from "./chunk-GWSEDWEF.js";
@@ -20,9 +20,52 @@ import {
20
20
  buildSetCookieHeader,
21
21
  serializeCookie,
22
22
  verifyWebhookSignature
23
- } from "./chunk-UFHSNW4E.js";
23
+ } from "./chunk-EHAZBEHO.js";
24
24
  import "./chunk-4MGIQFAJ.js";
25
25
 
26
+ // src/chainable.ts
27
+ var BuilderChainError = class _BuilderChainError extends Error {
28
+ missingMethod;
29
+ validNext;
30
+ constructor(missingMethod, validNext) {
31
+ const valid = validNext.length === 0 ? "<chain is terminal>" : validNext.map((m) => `.${m}`).join(", ");
32
+ super(`\`.${missingMethod}\` is not a valid step in this builder chain. Valid next: ${valid}.`);
33
+ this.name = "BuilderChainError";
34
+ this.missingMethod = missingMethod;
35
+ this.validNext = validNext;
36
+ if (typeof Error.captureStackTrace === "function") {
37
+ Error.captureStackTrace(this, _BuilderChainError);
38
+ }
39
+ }
40
+ };
41
+ var PASSTHROUGH_PROPS = /* @__PURE__ */ new Set([
42
+ "then",
43
+ "catch",
44
+ "finally",
45
+ "toJSON",
46
+ "toString",
47
+ "valueOf",
48
+ "constructor",
49
+ "nodeType"
50
+ ]);
51
+ function chainable(target) {
52
+ return new Proxy(target, {
53
+ get(t, prop, receiver) {
54
+ if (Reflect.has(t, prop)) {
55
+ return Reflect.get(t, prop, receiver);
56
+ }
57
+ if (typeof prop === "symbol") {
58
+ return void 0;
59
+ }
60
+ if (PASSTHROUGH_PROPS.has(prop)) {
61
+ return void 0;
62
+ }
63
+ const validNext = Object.keys(t).filter((k) => typeof Reflect.get(t, k) === "function");
64
+ throw new BuilderChainError(prop, validNext);
65
+ }
66
+ });
67
+ }
68
+
26
69
  // src/observer.ts
27
70
  function createPassOutcome() {
28
71
  return { kind: "pass" };
@@ -34,21 +77,21 @@ function createFailOutcome(reason) {
34
77
  return { kind: "fail", reason };
35
78
  }
36
79
  function buildReady(state) {
37
- return {
80
+ return chainable({
38
81
  contract: () => makeObserverHandle(state)
39
- };
82
+ });
40
83
  }
41
84
  function buildNeedsInput(state) {
42
- return {
85
+ return chainable({
43
86
  input: () => buildReady(state)
44
- };
87
+ });
45
88
  }
46
89
  function buildObserver(name) {
47
- return {
48
- description: (description) => ({
90
+ return chainable({
91
+ description: (description) => chainable({
49
92
  budget: (budget) => buildNeedsInput({ budget, description, name })
50
93
  })
51
- };
94
+ });
52
95
  }
53
96
 
54
97
  // src/builder.ts
@@ -146,7 +189,7 @@ function buildDepMapping(deps) {
146
189
  }
147
190
  function buildPreconditionStart(name) {
148
191
  let description = "";
149
- const self = {
192
+ const self = chainable({
150
193
  contract: () => makePrecondition({
151
194
  dependsOn: [],
152
195
  depMapping: [],
@@ -160,7 +203,7 @@ function buildPreconditionStart(name) {
160
203
  requires(deps) {
161
204
  return buildPreconditionWithDeps({ deps, name, getDescription: () => description });
162
205
  }
163
- };
206
+ });
164
207
  return self;
165
208
  }
166
209
  function buildPreconditionWithDeps({
@@ -171,26 +214,26 @@ function buildPreconditionWithDeps({
171
214
  const description = getDescription();
172
215
  const depMapping = buildDepMapping(deps);
173
216
  const dependsOn = depMapping.map(([, depName]) => depName);
174
- return {
217
+ return chainable({
175
218
  contract: () => makePrecondition({
176
219
  dependsOn,
177
220
  depMapping,
178
221
  description,
179
222
  name
180
223
  })
181
- };
224
+ });
182
225
  }
183
226
  function buildTestName(id, uiOnly) {
184
- return {
227
+ return chainable({
185
228
  name: (displayName) => buildTestRequires({ id, name: displayName, uiOnly })
186
- };
229
+ });
187
230
  }
188
231
  function castOutcome(value) {
189
232
  return value;
190
233
  }
191
234
  function buildTestRequires({ id, name, uiOnly }) {
192
235
  let description = "";
193
- const self = {
236
+ const self = chainable({
194
237
  description(text) {
195
238
  description = text;
196
239
  return self;
@@ -212,7 +255,7 @@ function buildTestRequires({ id, name, uiOnly }) {
212
255
  })
213
256
  );
214
257
  }
215
- };
258
+ });
216
259
  return self;
217
260
  }
218
261
  function buildTestOutcome({
@@ -224,7 +267,7 @@ function buildTestOutcome({
224
267
  uiOnly
225
268
  }) {
226
269
  const description = getDescription();
227
- return {
270
+ return chainable({
228
271
  expectedOutcome(text) {
229
272
  return buildTestStartsAt({
230
273
  description,
@@ -236,7 +279,7 @@ function buildTestOutcome({
236
279
  uiOnly
237
280
  });
238
281
  }
239
- };
282
+ });
240
283
  }
241
284
  function buildTestStartsAt({
242
285
  description,
@@ -247,7 +290,7 @@ function buildTestStartsAt({
247
290
  requiresKeys,
248
291
  uiOnly
249
292
  }) {
250
- return {
293
+ return chainable({
251
294
  notImplemented: () => ({
252
295
  coverage: [],
253
296
  description,
@@ -273,7 +316,7 @@ function buildTestStartsAt({
273
316
  uiOnly
274
317
  });
275
318
  }
276
- };
319
+ });
277
320
  }
278
321
  function buildTestSteps({
279
322
  description,
@@ -285,8 +328,8 @@ function buildTestSteps({
285
328
  startsAtFn,
286
329
  uiOnly
287
330
  }) {
288
- return {
289
- steps: (stepsFn) => ({
331
+ return chainable({
332
+ steps: (stepsFn) => chainable({
290
333
  coverage: (...ids) => ({
291
334
  coverage: ids,
292
335
  description,
@@ -301,7 +344,7 @@ function buildTestSteps({
301
344
  uiOnly
302
345
  })
303
346
  })
304
- };
347
+ });
305
348
  }
306
349
 
307
350
  // src/lint.ts
@@ -930,7 +973,12 @@ async function executeOnePrecondition(state, name) {
930
973
  const result = await def.setup(state.ctx, state.data);
931
974
  const resolved = {};
932
975
  Object.entries(result).forEach(([key, value]) => {
933
- resolved[key] = readTestValue(value);
976
+ if (!isPrimitive(value)) {
977
+ throw new TypeError(
978
+ `Precondition "${name}" returned non-primitive value at key "${key}". Setup return values must be string, number, or boolean \u2014 produced via ctx.fixed/uniqueEmail/uniqueId.`
979
+ );
980
+ }
981
+ resolved[key] = value;
934
982
  });
935
983
  state.data[name] = resolved;
936
984
  state.executed.push(name);
@@ -1002,13 +1050,13 @@ function createSetupContext({
1002
1050
  }) {
1003
1051
  return {
1004
1052
  runId,
1005
- fixed: (value) => createTestValue(value),
1053
+ fixed: (value) => brandTestValue(value),
1006
1054
  setCookie: (name, value, options) => {
1007
1055
  const resolvedOptions = options != null && options.domain == null && defaultDomain != null ? { ...options, domain: defaultDomain } : options ?? void 0;
1008
1056
  cookies.push({ name, options: resolvedOptions, value });
1009
1057
  },
1010
- uniqueEmail: () => createTestValue(`ripplo-test-${runId}@test.ripplo.ai`),
1011
- uniqueId: (prefix) => createTestValue(`ripplo-test-${prefix}-${runId}`)
1058
+ uniqueEmail: () => brandTestValue(`ripplo-test-${runId}@test.ripplo.ai`),
1059
+ uniqueId: (prefix) => brandTestValue(`ripplo-test-${prefix}-${runId}`)
1012
1060
  };
1013
1061
  }
1014
1062
  function deriveDefaultDomain(baseUrl) {
package/dist/koa.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Middleware } from 'koa';
2
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
3
- import './builder-0oT23S0W.js';
4
- import './types-B7YljrTz.js';
2
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
3
+ import './builder-mhjAdyNW.js';
4
+ import './types-2i1Sm7_J.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/koa.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serializeCookie,
7
7
  teardownRequestSchema,
8
8
  verifyWebhookSignature
9
- } from "./chunk-UFHSNW4E.js";
9
+ } from "./chunk-EHAZBEHO.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/koa.ts
@@ -1,8 +1,8 @@
1
1
  import { Codec } from '@ripplo/spec';
2
2
  import { z } from 'zod';
3
3
  import { CompileResult } from './compiler.js';
4
- import './builder-0oT23S0W.js';
5
- import './types-B7YljrTz.js';
4
+ import './builder-mhjAdyNW.js';
5
+ import './types-2i1Sm7_J.js';
6
6
  import './step-De52hTLd.js';
7
7
 
8
8
  declare const LOCKFILE_RELATIVE_PATH = ".ripplo/ripplo.lock";
@@ -647,7 +647,7 @@ declare const lockfileBodySchema: z.ZodObject<{
647
647
  observer: z.ZodString;
648
648
  params: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
649
649
  type: z.ZodLiteral<"static">;
650
- value: z.ZodString;
650
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
651
651
  }, z.core.$strip>, z.ZodObject<{
652
652
  name: z.ZodString;
653
653
  type: z.ZodLiteral<"variable">;
package/dist/lockfile.js CHANGED
@@ -194,6 +194,13 @@ var numericValueRefSchema = z5.discriminatedUnion("type", [
194
194
  z5.object({ type: z5.literal("static"), value: z5.number().int().nonnegative() }),
195
195
  variableRefSchema
196
196
  ]);
197
+ var primitiveValueRefSchema = z5.discriminatedUnion("type", [
198
+ z5.object({
199
+ type: z5.literal("static"),
200
+ value: z5.union([z5.string(), z5.number(), z5.boolean()])
201
+ }),
202
+ variableRefSchema
203
+ ]);
197
204
 
198
205
  // ../spec/src/variables.ts
199
206
  import { z as z6 } from "zod";
@@ -413,7 +420,7 @@ var assertObserverNode = z7.object({
413
420
  ...nodeBase,
414
421
  budget: z7.enum(["fast", "slow", "async"]),
415
422
  observer: z7.string().min(1).max(200),
416
- params: z7.record(z7.string().max(200), stringValueRefSchema),
423
+ params: z7.record(z7.string().max(200), primitiveValueRefSchema),
417
424
  type: z7.literal("assertObserver")
418
425
  });
419
426
  var specNodeSchema = z7.discriminatedUnion("type", [
package/dist/nestjs.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DynamicModule } from '@nestjs/common';
2
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
3
- import './builder-0oT23S0W.js';
4
- import './types-B7YljrTz.js';
2
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
3
+ import './builder-mhjAdyNW.js';
4
+ import './types-2i1Sm7_J.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/nestjs.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serializeCookie,
7
7
  teardownRequestSchema,
8
8
  verifyWebhookSignature
9
- } from "./chunk-UFHSNW4E.js";
9
+ } from "./chunk-EHAZBEHO.js";
10
10
  import {
11
11
  __decorateClass,
12
12
  __decorateParam
package/dist/nextjs.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { R as RipploEngine } from './engine-BdKDGBYw.js';
2
- import './builder-0oT23S0W.js';
3
- import './types-B7YljrTz.js';
1
+ import { R as RipploEngine } from './engine-B6MM4AEw.js';
2
+ import './builder-mhjAdyNW.js';
3
+ import './types-2i1Sm7_J.js';
4
4
  import './step-De52hTLd.js';
5
5
  import '@ripplo/spec';
6
6
 
package/dist/nextjs.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serializeCookie,
7
7
  teardownRequestSchema,
8
8
  verifyWebhookSignature
9
- } from "./chunk-UFHSNW4E.js";
9
+ } from "./chunk-EHAZBEHO.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/nextjs.ts
@@ -16,23 +16,24 @@ interface CookieEntry {
16
16
  readonly value: string;
17
17
  }
18
18
  declare const TEST_VALUE_BRAND: unique symbol;
19
- interface TestValue {
19
+ type Primitive = boolean | number | string;
20
+ type TestValue<T extends Primitive> = T & {
20
21
  readonly [TEST_VALUE_BRAND]: true;
21
- }
22
+ };
22
23
  interface SetupContext {
23
24
  readonly runId: string;
24
- fixed(value: string): TestValue;
25
+ fixed<T extends Primitive>(value: T): TestValue<T>;
25
26
  setCookie(name: string, value: string, options?: CookieOptions): void;
26
- uniqueEmail(): TestValue;
27
- uniqueId(prefix: string): TestValue;
27
+ uniqueEmail(): TestValue<string>;
28
+ uniqueId(prefix: string): TestValue<string>;
28
29
  }
29
- interface TeardownContext<TData extends Record<string, string>> {
30
+ interface TeardownContext<TData extends Record<string, Primitive>> {
30
31
  readonly data: TData;
31
32
  }
32
33
  declare const PRECONDITION_DATA: unique symbol;
33
34
  declare const PRECONDITION_DEPS: unique symbol;
34
35
  declare const PRECONDITION_NAME: unique symbol;
35
- interface Precondition<TData extends Record<string, string> = Record<string, string>, TDeps extends Record<string, Record<string, string>> = Record<string, Record<string, string>>> {
36
+ interface Precondition<TData extends Record<string, Primitive> = Record<string, Primitive>, TDeps extends Record<string, Record<string, Primitive>> = Record<string, Record<string, Primitive>>> {
36
37
  readonly [PRECONDITION_DATA]: TData;
37
38
  readonly [PRECONDITION_DEPS]: TDeps;
38
39
  readonly [PRECONDITION_NAME]: string;
@@ -46,10 +47,10 @@ interface PreconditionDefinition {
46
47
  readonly implemented: boolean;
47
48
  readonly name: string;
48
49
  readonly returns: ReadonlyArray<string>;
49
- readonly teardown: ((ctx: TeardownContext<Record<string, string>>) => Promise<void>) | undefined;
50
- readonly setup: (ctx: SetupContext, deps: Record<string, Record<string, string>>) => Promise<Record<string, TestValue>>;
50
+ readonly teardown: ((ctx: TeardownContext<Record<string, Primitive>>) => Promise<void>) | undefined;
51
+ readonly setup: (ctx: SetupContext, deps: Record<string, Record<string, Primitive>>) => Promise<Record<string, Primitive>>;
51
52
  }
52
- type VarsFn<T> = (vars: Record<string, Record<string, string>>) => T;
53
+ type VarsFn<T> = (vars: Record<string, Record<string, Primitive>>) => T;
53
54
  interface TestDefinition {
54
55
  readonly coverage: ReadonlyArray<string>;
55
56
  readonly description: string;
@@ -100,7 +101,7 @@ interface ObserverDefinition {
100
101
  readonly description: string;
101
102
  readonly implemented: boolean;
102
103
  readonly name: string;
103
- readonly run: (ctx: ObserverContext, params: Record<string, string>) => Promise<ObserverOutcome>;
104
+ readonly run: (ctx: ObserverContext, params: Record<string, Primitive>) => Promise<ObserverOutcome>;
104
105
  }
105
106
 
106
- export { type CookieEntry as C, DEFAULT_IGNORE_PATHS as D, type ObserverHandle as O, type Precondition as P, type SetupContext as S, type TeardownContext as T, type UnimplementedItems as U, type ObserverInput as a, type ObserverBudgetTier as b, type CookieOptions as c, DEFAULT_WATCH_PATHS as d, type ObserverContext as e, type ObserverDefinition as f, type ObserverOutcome as g, type PreconditionDeps as h, type TestDefinition as i, type PreconditionDefinition as j, type PreconditionData as k, type TestValue as l };
107
+ export { type CookieEntry as C, DEFAULT_IGNORE_PATHS as D, type ObserverHandle as O, type Primitive as P, type SetupContext as S, type TeardownContext as T, type UnimplementedItems as U, type ObserverInput as a, type ObserverBudgetTier as b, type CookieOptions as c, DEFAULT_WATCH_PATHS as d, type ObserverContext as e, type ObserverDefinition as f, type ObserverOutcome as g, type Precondition as h, type PreconditionDeps as i, type TestDefinition as j, type TestValue as k, type PreconditionDefinition as l, type PreconditionData as m };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ripplo/testing",
3
3
  "description": "TypeScript DSL for defining and running Ripplo e2e workflow tests",
4
- "version": "0.4.2",
4
+ "version": "0.4.3",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -99,8 +99,8 @@
99
99
  "tsup": "^8.5.1",
100
100
  "typescript": "catalog:",
101
101
  "vitest": "^4.1.4",
102
- "@ripplo/spec": "^0.0.0",
103
- "@ripplo/eslint-config": "0.0.0"
102
+ "@ripplo/eslint-config": "0.0.0",
103
+ "@ripplo/spec": "^0.0.0"
104
104
  },
105
105
  "peerDependencies": {
106
106
  "@nestjs/common": "^10.0.0 || ^11.0.0",