@pikku/core 0.12.114 → 0.12.117

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,3 +1,55 @@
1
+ ## 0.12.117
2
+
3
+ ### Patch Changes
4
+
5
+ - 8a0ecb7: `pikku scenario run <env> --run browser --strict` now refuses every route off the run surface, rather than only reporting one at the end. An action step that would fall back to its `default` binding throws `ScenarioNoSurfaceBinding` even when it declares `default`, and a `then` witnessed only server-side throws the new `ScenarioUnwitnessedAssertion`. Without `--strict` nothing changes: the fallback still runs and the unwitnessed assertion is still counted into the coverage line.
6
+
7
+ It exists because a run is becoming a source of documentation. A docs build runs the suite `--run browser --strict`, so a feature that cannot be driven end to end through the UI cannot produce a page — no flow where three steps are screenshots and the fourth quietly happened over RPC with nothing to show. The refusal names the step, the surface the run asked for and the surfaces that did bind it, which turns "what still has no browser binding" into a worklist the run prints rather than something to go looking for.
8
+
9
+ `--strict` on `--run default` is accepted and does nothing, since nothing on the default surface can fall back or be witnessed elsewhere. `PikkuScenarioService.setRunSurface` takes the flag as a second argument, and `isStrictSurface()` reads it back.
10
+
11
+ ## 0.12.116
12
+
13
+ ### Patch Changes
14
+
15
+ - 87971bd: `browser.screenshot('the order, confirmed', { showcase: true })` marks one shot as fit to publish outside the run, and the artifact ledger carries the flag. A gallery, a docs page or a marketing card can then be built from the scenario run itself instead of a second browser pass configured somewhere else, and the author of the step — the only one who knows the page is at a moment worth showing a stranger — is who decides.
16
+
17
+ Each filed screenshot also carries an `id`: the same shot under one key across runs. `path` leads with the order the run happened in, so inserting a step ahead of a shot renumbers it and anything meant to outlive one run (a caption override, a diff against last week's build) loses track of it.
18
+
19
+ Two supporting fixes in `@pikku/playwright`: contexts open at a pinned `viewport` (1440x900, overridable per config or via `E2E_VIEWPORT_WIDTH`/`E2E_VIEWPORT_HEIGHT`) and screenshots are taken with animations disabled, so two runs of the same scenario photograph the same thing. `{ fullPage: true }` is available for shots of a whole scrollable page.
20
+
21
+ - b312867: A project can now serve several MCP endpoints, one per connector.
22
+
23
+ Until now every MCP tool in a project was pooled onto a single `/mcp`, so a hub offering three connectors offered one endpoint listing all three connectors' tools at once. A client pointed at it saw tools it had no business calling, and the only way to give a connector an endpoint of its own was to give it a deployment of its own — three deploys, three bills, three service graphs.
24
+
25
+ `wireAddon` gains `mcpEndpoint`. `true` serves that instance's tools at `/mcp/<name>`; a string is the path, used as given. Leaving it unset keeps the tools on the shared endpoint, which is where they have always been, so nothing existing moves.
26
+
27
+ A surfaced instance now gets its own manifest (`.pikku/mcp/mcp.<name>.gen.json`, carrying the path it answers on), its own deploy unit (`mcp-<name>`, routed on that path), and its own MCP server — with its own tool list, so a client pointed at one endpoint never sees another's tools. The plumbing for the per-surface manifest and unit already existed in `deploy apply`; nothing had ever produced one.
28
+
29
+ `pikku dev` mounts every endpoint the generated tree describes, not just the default one. Without that a project that moved its tools onto their own endpoints would have served nothing locally at all — the default manifest it reads is empty precisely because they moved — and the only way to try a connector would have been to deploy it.
30
+
31
+ The node and bun transports take `mcpSurfaces` alongside `mcpJson` and mount each at its own path, longest path first. `/mcp` claims everything beneath `/mcp/`, so without that ordering the default endpoint answers `/mcp/weather` and the surface's tools are unreachable.
32
+
33
+ OAuth discovery is split between the endpoints rather than duplicated across them. RFC 9728 folds a resource's path into its well-known route, so each endpoint's own document is already distinct, but the path-less `/.well-known/oauth-protected-resource` predates that and describes whichever resource answers it. Only the default endpoint claims it — otherwise every unit registers the same route and the provider's router decides which resource a client is told about, and in dev a client probing it is described whichever surface sorted first.
34
+
35
+ - 51bd35a: Document five public keys that carried no JSDoc: `wireChannel`'s `onDisconnect`,
36
+ `wireRemoteAddon`'s `serverUrl` and `tags`, and `CoreUserSession`'s `userId` and
37
+ `orgId`. A key printed as a name and a type is a shape; what a caller needs is
38
+ what to put in it, and only the JSDoc where the type is declared carries that
39
+ into the IDE, the console and the shipped surface doc at once.
40
+
41
+ ## 0.12.115
42
+
43
+ ### Patch Changes
44
+
45
+ - 238902c: A channel's `auth` flag now reaches its meta. The inspector already read it to mark the connect and disconnect functions sessionless, then dropped it, so nothing downstream could tell a public channel from a private one without reading the generated source.
46
+
47
+ That matters in front of the channel rather than inside it. A `wireCLI({ auth: false })` program is reachable by anyone holding its address; a router or deploy that cannot see the flag either guesses or demands a token in front of a surface it was never protecting, locking out the clients the program was opened for.
48
+
49
+ The flag is only recorded when the channel actually said. A channel that never mentioned `auth` leaves it absent rather than claiming a default it did not declare, which the runtime continues to read as requiring a session.
50
+
51
+ - 2fd2b22: Telemetry middleware now records `errorStack` alongside `errorMessage`, so an observability backend has the stack of a failed invocation and not only its message.
52
+
1
53
  ## 0.12.114
2
54
 
3
55
  ### Patch Changes
@@ -8,12 +8,14 @@ export const telemetryOuter = pikkuMiddlewareFactory(({ environmentId, orgId } =
8
8
  const start = performance.now();
9
9
  let outcome = 'ok';
10
10
  let errorMessage;
11
+ let errorStack;
11
12
  try {
12
13
  await next();
13
14
  }
14
15
  catch (e) {
15
16
  outcome = 'error';
16
17
  errorMessage = e instanceof Error ? e.message : String(e);
18
+ errorStack = e instanceof Error ? e.stack : undefined;
17
19
  throw e;
18
20
  }
19
21
  finally {
@@ -26,6 +28,7 @@ export const telemetryOuter = pikkuMiddlewareFactory(({ environmentId, orgId } =
26
28
  totalDuration: Math.round(performance.now() - start),
27
29
  outcome,
28
30
  ...(errorMessage ? { errorMessage } : {}),
31
+ ...(errorStack ? { errorStack } : {}),
29
32
  ...(wire.http
30
33
  ? {
31
34
  httpStatus: wire.http.response?.statusCode,
@@ -49,12 +52,14 @@ export const telemetryInner = pikkuMiddlewareFactory(({ environmentId, orgId } =
49
52
  const start = performance.now();
50
53
  let outcome = 'ok';
51
54
  let errorMessage;
55
+ let errorStack;
52
56
  try {
53
57
  await next();
54
58
  }
55
59
  catch (e) {
56
60
  outcome = 'error';
57
61
  errorMessage = e instanceof Error ? e.message : String(e);
62
+ errorStack = e instanceof Error ? e.stack : undefined;
58
63
  throw e;
59
64
  }
60
65
  finally {
@@ -68,6 +73,7 @@ export const telemetryInner = pikkuMiddlewareFactory(({ environmentId, orgId } =
68
73
  outcome,
69
74
  pikkuUserId: wire.pikkuUserId,
70
75
  ...(errorMessage ? { errorMessage } : {}),
76
+ ...(errorStack ? { errorStack } : {}),
71
77
  ...(environmentId ? { environmentId } : {}),
72
78
  ...(orgId ? { orgId } : {}),
73
79
  });
@@ -81,7 +81,9 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
81
81
  postgres?: PostgresConfig;
82
82
  } & Config;
83
83
  export interface CoreUserSession {
84
+ /** Who the session belongs to, as your own system identifies them. Pikku only carries it. */
84
85
  userId?: string;
86
+ /** The tenant the session is acting inside, when the project has more than one. */
85
87
  orgId?: string;
86
88
  /** True when the session belongs to a synthetic scenario actor — lets audits/analytics address synthetic traffic */
87
89
  actor?: boolean;
@@ -16,6 +16,18 @@ export type WireAddonConfig = {
16
16
  * and is typed against the addon's function names.
17
17
  */
18
18
  mcp?: boolean | string[];
19
+ /**
20
+ * Serves this addon's MCP tools on an endpoint of their own rather than
21
+ * folding them into the project's single `/mcp`. `true` mounts them at
22
+ * `/mcp/<name>`; a string is the path, used as given.
23
+ *
24
+ * This is what lets one project expose several connectors: each wired
25
+ * instance becomes its own MCP server, with its own tool list, so a client
26
+ * pointed at one never sees another's tools. Leaving it unset keeps the
27
+ * addon's tools on the shared endpoint, which is where they have always
28
+ * been.
29
+ */
30
+ mcpEndpoint?: boolean | string;
19
31
  /** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
20
32
  tags?: string[];
21
33
  /** Required of every function in the addon, on top of the function's own. */
@@ -12,11 +12,13 @@ export type WireRemoteAddonConfig = {
12
12
  name: string;
13
13
  /** Must be installed as a devDependency — `pikku verify` enforces this. */
14
14
  package: string;
15
+ /** Where the addon is deployed, e.g. `https://registry.example.com`. A function when it varies per environment. */
15
16
  serverUrl: string | ((services: CoreServices) => string | Promise<string>);
16
17
  /** Omit when the addon declares its remote surface public. */
17
18
  auth?: RemoteAddonAuth;
18
19
  /** Map a consumer-facing fn name → the remote fn name, when they differ (rare). */
19
20
  remoteName?: (fn: string) => string;
21
+ /** Applied to every function the addon contributes, so tag middleware and permissions reach them. */
20
22
  tags?: string[];
21
23
  };
22
24
  /**
@@ -37,6 +37,17 @@ export interface ChannelMeta {
37
37
  message: ChannelMessageMeta | null;
38
38
  messageWirings: Record<string, Record<string, ChannelMessageMeta>>;
39
39
  binary?: boolean | null;
40
+ /**
41
+ * Whether a connection must carry a session. Absent means the channel did not
42
+ * say, which the runtime treats as requiring one.
43
+ *
44
+ * Recorded so that something deploying or routing in front of this channel can
45
+ * tell a public surface from a private one without reading the generated
46
+ * source: a `wireCLI({ auth: false })` program is reachable by anyone holding
47
+ * the address, and a proxy that does not know that will either guess or gate
48
+ * traffic it was never protecting.
49
+ */
50
+ auth?: boolean;
40
51
  /** Set when the channel was wired by a gateway rather than declared directly. */
41
52
  gateway?: boolean;
42
53
  summary?: string;
@@ -57,6 +68,7 @@ export type CoreChannel<ChannelData, Channel extends string, ChannelConnect = Co
57
68
  func?: ChannelConnect;
58
69
  middleware?: PikkuMiddleware[];
59
70
  };
71
+ /** Runs once after the socket closes, however it closed. Nothing it returns reaches the client. */
60
72
  onDisconnect?: ChannelDisconnect | {
61
73
  func?: ChannelDisconnect;
62
74
  middleware?: PikkuMiddleware[];
@@ -21,12 +21,24 @@ export type MCPResourceMeta = Record<string, Omit<CoreMCPResource, 'func' | 'mid
21
21
  inputSchema: string | null;
22
22
  outputSchema: string | null;
23
23
  middleware?: MiddlewareMetadata[];
24
+ /**
25
+ * The MCP endpoint this belongs to, when the project serves more than one.
26
+ * Absent means the project's default endpoint. Codegen splits the metadata
27
+ * by this key, so each endpoint's manifest lists only its own.
28
+ */
29
+ surface?: string;
24
30
  }>;
25
31
  export type MCPToolMeta = Record<string, Omit<CoreMCPTool, 'func' | 'middleware'> & {
26
32
  pikkuFuncId: string;
27
33
  inputSchema: string | null;
28
34
  outputSchema: string | null;
29
35
  middleware?: MiddlewareMetadata[];
36
+ /**
37
+ * The MCP endpoint this belongs to, when the project serves more than one.
38
+ * Absent means the project's default endpoint. Codegen splits the metadata
39
+ * by this key, so each endpoint's manifest lists only its own.
40
+ */
41
+ surface?: string;
30
42
  }>;
31
43
  export type MCPPromptMeta = Record<string, Omit<CoreMCPPrompt, 'func' | 'middleware'> & {
32
44
  pikkuFuncId: string;
@@ -38,6 +50,12 @@ export type MCPPromptMeta = Record<string, Omit<CoreMCPPrompt, 'func' | 'middlew
38
50
  required: boolean;
39
51
  }>;
40
52
  middleware?: MiddlewareMetadata[];
53
+ /**
54
+ * The MCP endpoint this belongs to, when the project serves more than one.
55
+ * Absent means the project's default endpoint. Codegen splits the metadata
56
+ * by this key, so each endpoint's manifest lists only its own.
57
+ */
58
+ surface?: string;
41
59
  }>;
42
60
  export type CoreMCPResource<PikkuFunctionConfig = CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>>, PikkuPermission = CorePikkuPermission<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
43
61
  /** How the client addresses this resource. `{name}` marks a parameter, and every parameter must be a key of the function's input schema. */
@@ -6,7 +6,7 @@ import type { ScenarioPersonas } from '../../services/personas-service.js';
6
6
  import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } from './scenario-step.types.js';
7
7
  import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
8
8
  export { addFeature, resolveFeatureScenarios } from './feature.js';
9
- export type { CoreFeature, CoreFeatureScenario, FeatureMeta, FeaturesMeta, PikkuBrowserWire, PikkuScenarioWire, ScenarioBrowserFailure, ScenarioBrowserProvider, ScenarioEnvironment, ScenarioStepKind, ScenarioStepMeta, ScenarioStepOptions, ScenarioStepPhase, ScenarioSurface, TestIdSelector, } from './scenario.types.js';
9
+ export type { CoreFeature, CoreFeatureScenario, FeatureMeta, FeaturesMeta, PikkuBrowserWire, PikkuScenarioWire, ScenarioBrowserFailure, ScenarioBrowserProvider, ScenarioEnvironment, ScenarioStepKind, ScenarioStepMeta, ScenarioStepOptions, ScenarioScreenshotOptions, ScenarioStepPhase, ScenarioSurface, TestIdSelector, } from './scenario.types.js';
10
10
  export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioStepRow, } from './scenario-run.types.js';
11
11
  export { SCENARIO_SURFACES } from './scenario-step.types.js';
12
12
  export { resolveScenarioSurfaces } from './scenario-surface.js';
@@ -92,6 +92,9 @@ export declare class ScenarioActorRequired extends PikkuError {
92
92
  * coverage gap. (A `then` that ran somewhere, just not on the run surface, is
93
93
  * the coverage gap; it is reported rather than thrown. See
94
94
  * {@link ScenarioNoWitness} for one that ran nowhere.)
95
+ *
96
+ * A strict run also throws it for a step that *could* fall back, because there
97
+ * the fallback is the thing being refused.
95
98
  */
96
99
  export declare class ScenarioNoSurfaceBinding extends PikkuError {
97
100
  readonly stepFunc: string;
@@ -114,6 +117,22 @@ export declare class ScenarioNoWitness extends PikkuError {
114
117
  readonly runSurface: ScenarioSurface;
115
118
  constructor(stepFunc: string, declared: ScenarioSurface[], runSurface: ScenarioSurface);
116
119
  }
120
+ /**
121
+ * An assertion ran, but not on the surface the run targets.
122
+ *
123
+ * The sibling of {@link ScenarioNoWitness}: that one checked nothing anywhere,
124
+ * this one checked the system of record while the prose claims the actor saw it
125
+ * on the page. Ordinary runs count it as a coverage gap; a strict run refuses
126
+ * it, because a flow that cannot be observed end to end on the run surface is
127
+ * not a flow that surface can be documented from.
128
+ */
129
+ export declare class ScenarioUnwitnessedAssertion extends PikkuError {
130
+ readonly stepFunc: string;
131
+ readonly declared: ScenarioSurface[];
132
+ readonly runSurface: ScenarioSurface;
133
+ readonly witnessedOn: ScenarioSurface[];
134
+ constructor(stepFunc: string, declared: ScenarioSurface[], runSurface: ScenarioSurface, witnessedOn: ScenarioSurface[]);
135
+ }
117
136
  /**
118
137
  * The scenario capability, layered onto a workflow service rather than being
119
138
  * one.
@@ -145,13 +164,21 @@ export declare class PikkuScenarioService implements WorkflowRunExtension {
145
164
  private scenarioBrowserProvider?;
146
165
  private scenarioEnvironment?;
147
166
  private runSurface;
167
+ private strictSurface;
148
168
  constructor(engine: WorkflowRunEngine);
149
169
  /**
150
170
  * The surface every actor drives the system through for this run, set once by
151
171
  * the runner from `--run`. `default` is the server-side path — the fast suite.
172
+ *
173
+ * `strict` removes both routes off that surface: an action may not fall back
174
+ * to its `default` binding, and a `then` may not be witnessed anywhere else.
175
+ * It is what lets a run stand as evidence of the whole flow on one surface,
176
+ * which is what generating documentation from a run requires. On a `default`
177
+ * run it changes nothing, because nothing there can fall back.
152
178
  */
153
- setRunSurface(surface: ScenarioSurface): void;
179
+ setRunSurface(surface: ScenarioSurface, strict?: boolean): void;
154
180
  getRunSurface(): ScenarioSurface;
181
+ isStrictSurface(): boolean;
155
182
  /**
156
183
  * Registered by `@pikku/playwright` (or any other driver) before a scenario
157
184
  * runs. Absent means browser steps cannot run, which the CLI checks up front
@@ -132,14 +132,21 @@ addError(ScenarioActorRequired, {
132
132
  * coverage gap. (A `then` that ran somewhere, just not on the run surface, is
133
133
  * the coverage gap; it is reported rather than thrown. See
134
134
  * {@link ScenarioNoWitness} for one that ran nowhere.)
135
+ *
136
+ * A strict run also throws it for a step that *could* fall back, because there
137
+ * the fallback is the thing being refused.
135
138
  */
136
139
  export class ScenarioNoSurfaceBinding extends PikkuError {
137
140
  stepFunc;
138
141
  declared;
139
142
  runSurface;
140
143
  constructor(stepFunc, declared, runSurface) {
141
- super(`[scenario] step '${stepFunc}' declares no binding for '${runSurface}' and no 'default' to fall back to ` +
142
- `(declares: ${declared.join(', ') || 'nothing'}).`);
144
+ const declares = `(declares: ${declared.join(', ') || 'nothing'})`;
145
+ super(declared.includes('default')
146
+ ? `[scenario] step '${stepFunc}' has no binding for '${runSurface}' and a strict run will not let it ` +
147
+ `fall back to 'default' ${declares}. Add a '${runSurface}' binding, or drop --strict.`
148
+ : `[scenario] step '${stepFunc}' declares no binding for '${runSurface}' and no 'default' to fall back to ` +
149
+ `${declares}.`);
143
150
  this.stepFunc = stepFunc;
144
151
  this.declared = declared;
145
152
  this.runSurface = runSurface;
@@ -175,6 +182,34 @@ addError(ScenarioNoWitness, {
175
182
  status: 500,
176
183
  message: 'Assertion has no witness for this surface.',
177
184
  });
185
+ /**
186
+ * An assertion ran, but not on the surface the run targets.
187
+ *
188
+ * The sibling of {@link ScenarioNoWitness}: that one checked nothing anywhere,
189
+ * this one checked the system of record while the prose claims the actor saw it
190
+ * on the page. Ordinary runs count it as a coverage gap; a strict run refuses
191
+ * it, because a flow that cannot be observed end to end on the run surface is
192
+ * not a flow that surface can be documented from.
193
+ */
194
+ export class ScenarioUnwitnessedAssertion extends PikkuError {
195
+ stepFunc;
196
+ declared;
197
+ runSurface;
198
+ witnessedOn;
199
+ constructor(stepFunc, declared, runSurface, witnessedOn) {
200
+ super(`[scenario] assertion '${stepFunc}' was checked on ${witnessedOn.join(', ')}, not on '${runSurface}'. ` +
201
+ `It held there — but nothing looked at '${runSurface}', which is what the step's prose claims the actor saw. ` +
202
+ `Add a '${runSurface}' witness (declares: ${declared.join(', ') || 'nothing'}), or drop --strict.`);
203
+ this.stepFunc = stepFunc;
204
+ this.declared = declared;
205
+ this.runSurface = runSurface;
206
+ this.witnessedOn = witnessedOn;
207
+ }
208
+ }
209
+ addError(ScenarioUnwitnessedAssertion, {
210
+ status: 500,
211
+ message: 'Assertion was checked, but not on the run surface.',
212
+ });
178
213
  /**
179
214
  * The scenario capability, layered onto a workflow service rather than being
180
215
  * one.
@@ -211,19 +246,30 @@ export class PikkuScenarioService {
211
246
  scenarioBrowserProvider;
212
247
  scenarioEnvironment;
213
248
  runSurface = 'default';
249
+ strictSurface = false;
214
250
  constructor(engine) {
215
251
  this.engine = engine;
216
252
  }
217
253
  /**
218
254
  * The surface every actor drives the system through for this run, set once by
219
255
  * the runner from `--run`. `default` is the server-side path — the fast suite.
256
+ *
257
+ * `strict` removes both routes off that surface: an action may not fall back
258
+ * to its `default` binding, and a `then` may not be witnessed anywhere else.
259
+ * It is what lets a run stand as evidence of the whole flow on one surface,
260
+ * which is what generating documentation from a run requires. On a `default`
261
+ * run it changes nothing, because nothing there can fall back.
220
262
  */
221
- setRunSurface(surface) {
263
+ setRunSurface(surface, strict = false) {
222
264
  this.runSurface = surface;
265
+ this.strictSurface = strict;
223
266
  }
224
267
  getRunSurface() {
225
268
  return this.runSurface;
226
269
  }
270
+ isStrictSurface() {
271
+ return this.strictSurface;
272
+ }
227
273
  /**
228
274
  * Registered by `@pikku/playwright` (or any other driver) before a scenario
229
275
  * runs. Absent means browser steps cannot run, which the CLI checks up front
@@ -605,7 +651,8 @@ export class PikkuScenarioService {
605
651
  });
606
652
  };
607
653
  if (resolution.kind === 'action') {
608
- if (resolution.fellBack && !declared.includes('default')) {
654
+ if (resolution.fellBack &&
655
+ (this.strictSurface || !declared.includes('default'))) {
609
656
  throw new ScenarioNoSurfaceBinding(resolvedStepFunc, declared, this.runSurface);
610
657
  }
611
658
  return await runOnSurface(resolution.surface);
@@ -613,6 +660,9 @@ export class PikkuScenarioService {
613
660
  if (resolution.surfaces.length === 0) {
614
661
  throw new ScenarioNoWitness(resolvedStepFunc, declared, this.runSurface);
615
662
  }
663
+ if (this.strictSurface && resolution.unwitnessed) {
664
+ throw new ScenarioUnwitnessedAssertion(resolvedStepFunc, declared, this.runSurface, resolution.surfaces);
665
+ }
616
666
  // A `then` runs every witness it has and they must agree. The surface
617
667
  // witness runs first so that when the page is the thing that is wrong,
618
668
  // it is the failure that surfaces.
@@ -31,6 +31,15 @@ export interface ScenarioArtifact {
31
31
  actor?: string;
32
32
  /** The caption the scenario author took a screenshot under. */
33
33
  name?: string;
34
+ /**
35
+ * The same shot across runs, under one key. `path` carries the order the run
36
+ * happened in, so it moves whenever a step is inserted before it — nothing
37
+ * that outlives a run (a caption override, a diff against last week) can key
38
+ * off it.
39
+ */
40
+ id?: string;
41
+ /** Fit to show outside the run: a marketing card, a docs page, a gallery. */
42
+ showcase?: boolean;
34
43
  }
35
44
  /** One step of a run, already joined to the prose that declared it. */
36
45
  export interface ScenarioStepRow {
@@ -166,11 +166,22 @@ export interface TestIdSelector {
166
166
  * interface via `declare module`, so `wire.browser.page` is a fully typed
167
167
  * Playwright `Page` in a project that installs it.
168
168
  */
169
+ /** How one deliberate screenshot is taken, and what it is for. */
170
+ export interface ScenarioScreenshotOptions {
171
+ /**
172
+ * Publish this one outside the run — a marketing card, a docs page, a
173
+ * gallery. Declared at the call site because only the author of the step
174
+ * knows the page is at a moment worth showing a stranger.
175
+ */
176
+ showcase?: boolean;
177
+ /** Photograph the whole scrollable page rather than the viewport. */
178
+ fullPage?: boolean;
179
+ }
169
180
  export interface PikkuBrowserWire {
170
181
  /** The actor whose browser context this is */
171
182
  readonly actor: string;
172
183
  goto(url: string): Promise<void>;
173
- screenshot(name?: string): Promise<Uint8Array>;
184
+ screenshot(name?: string, options?: ScenarioScreenshotOptions): Promise<Uint8Array>;
174
185
  }
175
186
  /**
176
187
  * What one actor's window looked like at the moment a scenario failed.
@@ -1,6 +1,6 @@
1
1
  import type { CorePikkuFunctionConfig, CorePikkuFunctionHook } from '../../function/functions.types.js';
2
2
  export type { ScenarioStepInvocation, ScenarioStepMeta, PikkuScenarioWire, } from './dsl/workflow-dsl.types.js';
3
- export type { ScenarioStepPhase, ScenarioStepKind, ScenarioStepOptions, PikkuScenarioStepWire, ScenarioEnvironment, ScenarioSurface, ScenarioSurfaceResolution, PikkuBrowserWire, TestIdSelector, ScenarioBrowserProvider, ScenarioBrowserFailure, } from './scenario-step.types.js';
3
+ export type { ScenarioStepPhase, ScenarioStepKind, ScenarioStepOptions, PikkuScenarioStepWire, ScenarioEnvironment, ScenarioSurface, ScenarioSurfaceResolution, PikkuBrowserWire, ScenarioScreenshotOptions, TestIdSelector, ScenarioBrowserProvider, ScenarioBrowserFailure, } from './scenario-step.types.js';
4
4
  export type CoreFeatureScenario = CorePikkuFunctionConfig<any, any, any> | {
5
5
  scenario: CorePikkuFunctionConfig<any, any, any>;
6
6
  data: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.114",
3
+ "version": "0.12.117",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -82,6 +82,7 @@
82
82
  "ScenarioHookError",
83
83
  "ScenarioNoSurfaceBinding",
84
84
  "ScenarioNoWitness",
85
+ "ScenarioUnwitnessedAssertion",
85
86
  "ScenarioWitnessDisagreement",
86
87
  "addFeature",
87
88
  "composeStepProse",