@pikku/core 0.12.117 → 0.12.118
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 +41 -0
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +20 -1
- package/dist/wirings/workflow/pikku-scenario-service.js +41 -0
- package/dist/wirings/workflow/scenario-run.types.d.ts +56 -2
- package/dist/wirings/workflow/scenario-step.types.d.ts +9 -0
- package/dist/wirings/workflow/scenario.types.d.ts +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## 0.12.118
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- b4a895e: A scenario step row now carries where it fell inside each actor's recording, and a scenario result now says which feature it came from by id as well as by title.
|
|
6
|
+
|
|
7
|
+
The video offset is recorded rather than estimated. Documentation built out of a run has to turn a step sentence into an exact moment — a chapter marker, or a still pulled with `ffmpeg -ss` — and the only number available until now was the scenario clock summed out of the ladder. That clock is not the video's: recording starts when the actor's browser context opens, which is somewhere after step one, and every RPC step before or between the browser ones burns scenario time while the file sits still. The two drift apart by however much of the scenario happened off camera, and a still forty seconds out is a picture of the wrong screen with nothing to say it is wrong.
|
|
8
|
+
|
|
9
|
+
So `ScenarioStepRow.video` is stamped at the moment the step runs, from the driver's own clock: `ScenarioBrowserProvider.videoStartedAt(actor)` reports when that actor's context was opened with `recordVideo`, and the runner subtracts. It is a list of `{ actor, offsetMs }` rather than one number, because a video belongs to an actor and not to the scenario — one step touching two windows falls at a different moment in each, and an offset that does not name its file cannot be seeked to. A run without video, a step with no actor, and a step that never ran all carry nothing, which is what keeps the console's existing estimate as the fallback for runs recorded before this.
|
|
10
|
+
|
|
11
|
+
`ScenarioResult.featureId` is the other half of the same problem. `feature` is a title written for people to read and rewritten whenever the wording improves, so nothing downstream could key off it; the id `addFeature` registered the feature under does not move. The runner threads it from the plan, which read it off the registry, instead of deriving it from the label. `scenarioName` already carried the registration id and keeps it.
|
|
12
|
+
|
|
13
|
+
- b4a895e: `pikku scenario guide` writes the user guide a scenario suite already contains. A feature reads as a page and a scenario as a section, and a run leaves screenshots behind with the captions their author took them under — the command joins that to the editorial prose a project checks in under `docs/` and writes markdown. It renders no HTML, ships no components, resolves no asset URLs and calls no model: an image is an ordinary relative ``, and whoever consumes the markdown rewrites the paths.
|
|
14
|
+
|
|
15
|
+
A page cites a feature by leaving the marker pair where the block belongs:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
---
|
|
19
|
+
title: Deployments
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
A deployment is one tracked shipment of your app.
|
|
23
|
+
|
|
24
|
+
<!-- pikku:guide feature=deploymentsFeature -->
|
|
25
|
+
<!-- /pikku:guide -->
|
|
26
|
+
|
|
27
|
+
## Does my app go down during a deploy?
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That one line does both halves of the job. It says _where_ the block goes, which a frontmatter list cannot express, and it is what the coverage gate counts to decide _whether_ a feature is documented at all. The mapping stays many-to-many and falls out of the union of every marker in the tree. A rebuild rewrites exactly the regions between the markers, so every sentence a human wrote around them survives.
|
|
31
|
+
|
|
32
|
+
**The steps are evidence, not content.** A generated block is the scenario's title, the description its author wrote, and the shots it filed — never a numbered Given/When/Then ladder, which is a test report and not something anybody arrives at a documentation page wanting. The sentences are still what the guide is kept honest against: `docs/.guide.lock` records a hash of each feature's step sentences and artifact ids, deliberately not of the image bytes. Restyling a UI changes every screenshot and no sentence; inserting or renaming a step changes what the prose was describing, and the page is reported stale. The lock is generated and checked in, so no hash is ever typed or merged by hand — and a tree whose lock is untracked reports every page as current forever.
|
|
33
|
+
|
|
34
|
+
Every registered feature has to be cited by some page, and a feature that is pure plumbing says so rather than being written about — `pikkuFeature({ document: false })`, threaded through the inspector and `FeatureMeta`. An uncited feature fails the command by name; `--allow-undocumented` downgrades that one failure to a report. A page citing a feature id that is not registered stays an error either way.
|
|
35
|
+
|
|
36
|
+
A guide is only written out of a run that can stand behind it. A run that failed or was killed halfway is refused, because a page is a claim that the product does what it says. So is a narrowed one: `pikku scenario run --flows`/`--features`/`--tags` leaves out scenarios the suite has, and a guide built from it would describe those flows as though they do not exist. `ScenarioRunRecord.selection` records the filters a run was selected with, since nothing in the results afterwards can tell a suite of forty from forty that were asked for.
|
|
37
|
+
|
|
38
|
+
Results are joined to features by `featureId`, falling back to the display name only for records written before that field existed — a title is rewritten freely and two features may share one.
|
|
39
|
+
|
|
40
|
+
Emission is deterministic — identical inputs give byte-identical output, and no timestamp goes in that did not come from the run record. Frontmatter the compiler does not own (`slug`, `draft`, `sidebar_position`, anything else a docs site reads) passes through untouched, and a source written with CRLF line endings is read as having frontmatter.
|
|
41
|
+
|
|
1
42
|
## 0.12.117
|
|
2
43
|
|
|
3
44
|
### Patch Changes
|
|
@@ -3,11 +3,12 @@ import { InMemoryWorkflowService } from '../../services/in-memory-workflow-servi
|
|
|
3
3
|
import type { RunLifecycleContext, WorkflowRunEngine, WorkflowRunExtension } from './workflow-run-engine.types.js';
|
|
4
4
|
import type { PikkuRawWire } from '../../types/core.types.js';
|
|
5
5
|
import type { ScenarioPersonas } from '../../services/personas-service.js';
|
|
6
|
+
import type { ScenarioStepVideoOffset } from './scenario-run.types.js';
|
|
6
7
|
import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } from './scenario-step.types.js';
|
|
7
8
|
import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
|
|
8
9
|
export { addFeature, resolveFeatureScenarios } from './feature.js';
|
|
9
10
|
export type { CoreFeature, CoreFeatureScenario, FeatureMeta, FeaturesMeta, PikkuBrowserWire, PikkuScenarioWire, ScenarioBrowserFailure, ScenarioBrowserProvider, ScenarioEnvironment, ScenarioStepKind, ScenarioStepMeta, ScenarioStepOptions, ScenarioScreenshotOptions, ScenarioStepPhase, ScenarioSurface, TestIdSelector, } from './scenario.types.js';
|
|
10
|
-
export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioStepRow, } from './scenario-run.types.js';
|
|
11
|
+
export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunSelection, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioStepRow, ScenarioStepVideoOffset, } from './scenario-run.types.js';
|
|
11
12
|
export { SCENARIO_SURFACES } from './scenario-step.types.js';
|
|
12
13
|
export { resolveScenarioSurfaces } from './scenario-surface.js';
|
|
13
14
|
export { pollUntil, type PollOptions } from './scenario-poll.js';
|
|
@@ -161,6 +162,7 @@ export declare class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
161
162
|
private readonly engine;
|
|
162
163
|
private runActors;
|
|
163
164
|
private runContexts;
|
|
165
|
+
private runVideoOffsets;
|
|
164
166
|
private scenarioBrowserProvider?;
|
|
165
167
|
private scenarioEnvironment?;
|
|
166
168
|
private runSurface;
|
|
@@ -193,6 +195,23 @@ export declare class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
193
195
|
*/
|
|
194
196
|
setScenarioEnvironment(env: ScenarioEnvironment | undefined): void;
|
|
195
197
|
getScenarioEnvironment(): ScenarioEnvironment | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* Where each of a run's browser steps fell in its actor's video, keyed by the
|
|
200
|
+
* durable step name, handed over and forgotten in one call.
|
|
201
|
+
*
|
|
202
|
+
* Taken rather than read because the runner joins these onto the step rows
|
|
203
|
+
* after the run has finished — which is past the point anything else would
|
|
204
|
+
* clear them, and the only moment they are still wanted.
|
|
205
|
+
*/
|
|
206
|
+
takeStepVideoOffsets(runId: string): Map<string, ScenarioStepVideoOffset[]>;
|
|
207
|
+
/**
|
|
208
|
+
* Stamp where a step began inside one actor's recording.
|
|
209
|
+
*
|
|
210
|
+
* First write per actor wins: a `then` step runs once per witness, and the
|
|
211
|
+
* moment the reader wants is when the sentence started, not when its last
|
|
212
|
+
* witness got around to the browser.
|
|
213
|
+
*/
|
|
214
|
+
private recordVideoOffset;
|
|
196
215
|
attachRunContext(runId: string, workflowMeta: any, options?: {
|
|
197
216
|
actors?: ScenarioPersonas;
|
|
198
217
|
}): Promise<void>;
|
|
@@ -243,6 +243,10 @@ export class PikkuScenarioService {
|
|
|
243
243
|
// so the body and its hooks share one object rather than reading it back off
|
|
244
244
|
// the persisted wire.
|
|
245
245
|
runContexts = new Map();
|
|
246
|
+
// Where each browser step landed in its actor's recording, per run. Held
|
|
247
|
+
// apart from the run context because the runner reads it once the run has
|
|
248
|
+
// ended, which is exactly when `detachRunContext` has cleared that.
|
|
249
|
+
runVideoOffsets = new Map();
|
|
246
250
|
scenarioBrowserProvider;
|
|
247
251
|
scenarioEnvironment;
|
|
248
252
|
runSurface = 'default';
|
|
@@ -292,6 +296,39 @@ export class PikkuScenarioService {
|
|
|
292
296
|
getScenarioEnvironment() {
|
|
293
297
|
return this.scenarioEnvironment;
|
|
294
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Where each of a run's browser steps fell in its actor's video, keyed by the
|
|
301
|
+
* durable step name, handed over and forgotten in one call.
|
|
302
|
+
*
|
|
303
|
+
* Taken rather than read because the runner joins these onto the step rows
|
|
304
|
+
* after the run has finished — which is past the point anything else would
|
|
305
|
+
* clear them, and the only moment they are still wanted.
|
|
306
|
+
*/
|
|
307
|
+
takeStepVideoOffsets(runId) {
|
|
308
|
+
const offsets = this.runVideoOffsets.get(runId);
|
|
309
|
+
this.runVideoOffsets.delete(runId);
|
|
310
|
+
return offsets ?? new Map();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Stamp where a step began inside one actor's recording.
|
|
314
|
+
*
|
|
315
|
+
* First write per actor wins: a `then` step runs once per witness, and the
|
|
316
|
+
* moment the reader wants is when the sentence started, not when its last
|
|
317
|
+
* witness got around to the browser.
|
|
318
|
+
*/
|
|
319
|
+
recordVideoOffset(runId, stepName, actor, offsetMs) {
|
|
320
|
+
let byStep = this.runVideoOffsets.get(runId);
|
|
321
|
+
if (!byStep) {
|
|
322
|
+
byStep = new Map();
|
|
323
|
+
this.runVideoOffsets.set(runId, byStep);
|
|
324
|
+
}
|
|
325
|
+
const offsets = byStep.get(stepName) ?? [];
|
|
326
|
+
if (offsets.some((offset) => offset.actor === actor)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
offsets.push({ actor, offsetMs });
|
|
330
|
+
byStep.set(stepName, offsets);
|
|
331
|
+
}
|
|
295
332
|
async attachRunContext(runId, workflowMeta, options) {
|
|
296
333
|
const actors = options?.actors ??
|
|
297
334
|
(workflowMeta.source === 'scenario'
|
|
@@ -641,6 +678,10 @@ export class PikkuScenarioService {
|
|
|
641
678
|
// The dispatch guard above already refused an actor-less call, and a
|
|
642
679
|
// browser binding always requires one.
|
|
643
680
|
wire.browser = await this.scenarioBrowserProvider.sessionFor(actor.name);
|
|
681
|
+
const videoStartedAt = this.scenarioBrowserProvider.videoStartedAt?.(actor.name);
|
|
682
|
+
if (videoStartedAt !== undefined) {
|
|
683
|
+
this.recordVideoOffset(runId, stepName, actor.name, Math.max(0, Date.now() - videoStartedAt));
|
|
684
|
+
}
|
|
644
685
|
}
|
|
645
686
|
return await runPikkuFunc('workflow', workflowName, resolvedStepFunc, {
|
|
646
687
|
singletonServices: getSingletonServices(),
|
|
@@ -41,6 +41,21 @@ export interface ScenarioArtifact {
|
|
|
41
41
|
/** Fit to show outside the run: a marketing card, a docs page, a gallery. */
|
|
42
42
|
showcase?: boolean;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Where a step falls inside one actor's recording.
|
|
46
|
+
*
|
|
47
|
+
* Measured from the moment that actor's browser context opened — which is when
|
|
48
|
+
* Playwright starts the file — rather than from the start of the scenario, so
|
|
49
|
+
* it addresses the video's own clock. The two differ by however long the
|
|
50
|
+
* scenario spent before that window existed, plus every non-browser step since,
|
|
51
|
+
* which is why the offset is recorded at the moment the step runs instead of
|
|
52
|
+
* being summed back out of the ladder afterwards.
|
|
53
|
+
*/
|
|
54
|
+
export interface ScenarioStepVideoOffset {
|
|
55
|
+
/** Whose recording this offset is into: one actor, one video file. */
|
|
56
|
+
actor: string;
|
|
57
|
+
offsetMs: number;
|
|
58
|
+
}
|
|
44
59
|
/** One step of a run, already joined to the prose that declared it. */
|
|
45
60
|
export interface ScenarioStepRow {
|
|
46
61
|
sentence: string;
|
|
@@ -54,6 +69,16 @@ export interface ScenarioStepRow {
|
|
|
54
69
|
status: string;
|
|
55
70
|
durationMs?: number;
|
|
56
71
|
error?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Where this step lands in each actor's video, for the actors whose window
|
|
74
|
+
* was being recorded when it ran. Absent for a step with no actor, a run
|
|
75
|
+
* without video, and a step that never ran at all.
|
|
76
|
+
*
|
|
77
|
+
* A list rather than one number because a video belongs to an actor, not to
|
|
78
|
+
* the scenario: a step touching two windows falls at a different moment in
|
|
79
|
+
* each, and an offset that does not name its file cannot be seeked to.
|
|
80
|
+
*/
|
|
81
|
+
video?: ScenarioStepVideoOffset[];
|
|
57
82
|
}
|
|
58
83
|
/** Everything known about why one scenario failed. */
|
|
59
84
|
export interface ScenarioFailureDetail {
|
|
@@ -76,10 +101,22 @@ export interface ScenarioResult {
|
|
|
76
101
|
error?: string;
|
|
77
102
|
steps?: ScenarioStepRow[];
|
|
78
103
|
failure?: ScenarioFailureDetail;
|
|
79
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The registered scenario this ran, by the id it is registered under — the
|
|
106
|
+
* same id `FeatureMetaEntry.scenario` references. The label alone does not
|
|
107
|
+
* give it, and unlike the label it is not rewritten when the prose is.
|
|
108
|
+
*/
|
|
80
109
|
scenarioName?: string;
|
|
81
|
-
/** The feature that grouped it,
|
|
110
|
+
/** The feature that grouped it — its display name, which is freely renamed. */
|
|
82
111
|
feature?: string;
|
|
112
|
+
/**
|
|
113
|
+
* The registered feature that grouped it, by id.
|
|
114
|
+
*
|
|
115
|
+
* `feature` is a title someone writes for people to read, so nothing that
|
|
116
|
+
* outlives a run can key off it. This is what `addFeature` registered the
|
|
117
|
+
* feature under, and it is what survives the title being rewritten.
|
|
118
|
+
*/
|
|
119
|
+
featureId?: string;
|
|
83
120
|
tags?: string[];
|
|
84
121
|
/** Images and footage this scenario produced, filed under the run. */
|
|
85
122
|
artifacts?: ScenarioArtifact[];
|
|
@@ -103,6 +140,21 @@ export interface ScenarioRunReport {
|
|
|
103
140
|
hookFailures: string[];
|
|
104
141
|
}
|
|
105
142
|
export type ScenarioRunStatus = 'running' | 'passed' | 'failed';
|
|
143
|
+
/**
|
|
144
|
+
* The filters a run was selected with, recorded when it was narrowed at all.
|
|
145
|
+
*
|
|
146
|
+
* A narrowed run is a partial record of the suite: scenarios a feature owns can
|
|
147
|
+
* be missing from it, and whole features can be absent, with nothing in the
|
|
148
|
+
* results to say so. Anything that reads a run as evidence of what the suite
|
|
149
|
+
* does — rather than of what happened that afternoon — has to be able to tell
|
|
150
|
+
* the two apart, and it cannot be inferred from the results afterwards.
|
|
151
|
+
*/
|
|
152
|
+
export interface ScenarioRunSelection {
|
|
153
|
+
flows?: string[];
|
|
154
|
+
features?: string[];
|
|
155
|
+
tags?: string[];
|
|
156
|
+
excludeTags?: string[];
|
|
157
|
+
}
|
|
106
158
|
/**
|
|
107
159
|
* A whole run, as it is stored and read back.
|
|
108
160
|
*
|
|
@@ -116,6 +168,8 @@ export interface ScenarioRunRecord extends ScenarioRunReport {
|
|
|
116
168
|
status: ScenarioRunStatus;
|
|
117
169
|
/** The surface the run targeted: `default`, `browser`, … */
|
|
118
170
|
surface: string;
|
|
171
|
+
/** Absent on a run of the whole suite; see {@link ScenarioRunSelection}. */
|
|
172
|
+
selection?: ScenarioRunSelection;
|
|
119
173
|
startedAt: string;
|
|
120
174
|
finishedAt?: string;
|
|
121
175
|
}
|
|
@@ -235,6 +235,15 @@ export interface ScenarioBrowserProvider {
|
|
|
235
235
|
* scenario's reset, long after the outcome that decides whether to keep them.
|
|
236
236
|
*/
|
|
237
237
|
endScenario?(outcome: 'passed' | 'failed'): void;
|
|
238
|
+
/**
|
|
239
|
+
* When this actor's recording started, as epoch milliseconds.
|
|
240
|
+
*
|
|
241
|
+
* The seam that keeps the video clock out of `@pikku/core`: a driver knows
|
|
242
|
+
* when it opened the context it passed `recordVideo` to, and the runner turns
|
|
243
|
+
* that into a per-step offset. Absent for an actor with no window open, and
|
|
244
|
+
* for a run recording nothing — both of which leave the step's offset off.
|
|
245
|
+
*/
|
|
246
|
+
videoStartedAt?(actorName: string): number | undefined;
|
|
238
247
|
/**
|
|
239
248
|
* Snapshot every open window for a failed scenario. `label` identifies the
|
|
240
249
|
* scenario in artifact filenames. Never throws: a failure to capture must
|
|
@@ -9,6 +9,14 @@ export type CoreFeature = {
|
|
|
9
9
|
name: string;
|
|
10
10
|
description?: string;
|
|
11
11
|
tags?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Whether this feature is guide material. Defaults to true: a feature is a
|
|
14
|
+
* page of the user guide unless it says otherwise, so a feature nobody has
|
|
15
|
+
* written about is a gap `pikku scenario guide` reports rather than a page
|
|
16
|
+
* silently missing. Pure plumbing — a wire, a validation layer, a bearer
|
|
17
|
+
* auth handshake — sets it false and stops being a coverage problem.
|
|
18
|
+
*/
|
|
19
|
+
document?: boolean;
|
|
12
20
|
scenarios: readonly CoreFeatureScenario[];
|
|
13
21
|
before?: CorePikkuFunctionHook;
|
|
14
22
|
after?: CorePikkuFunctionHook;
|
|
@@ -22,6 +30,8 @@ export type FeatureMeta = {
|
|
|
22
30
|
name: string;
|
|
23
31
|
description?: string;
|
|
24
32
|
tags: string[];
|
|
33
|
+
/** Present only when the feature opted out; absent means documented. */
|
|
34
|
+
document?: boolean;
|
|
25
35
|
entries: FeatureMetaEntry[];
|
|
26
36
|
unresolvedEntries: number;
|
|
27
37
|
hasBefore: boolean;
|