@pikku/core 0.12.69 → 0.12.71

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +34 -2
  4. package/dist/function/functions.types.d.ts +27 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/internal.d.ts +1 -1
  7. package/dist/internal.js +1 -1
  8. package/dist/pikku-state.js +1 -0
  9. package/dist/services/http-scenario-actors.d.ts +12 -4
  10. package/dist/services/http-scenario-actors.js +47 -45
  11. package/dist/services/in-memory-queue-service.d.ts +6 -0
  12. package/dist/services/in-memory-queue-service.js +8 -1
  13. package/dist/services/in-memory-workflow-service.d.ts +3 -5
  14. package/dist/services/in-memory-workflow-service.js +10 -19
  15. package/dist/services/index.d.ts +2 -1
  16. package/dist/services/index.js +1 -0
  17. package/dist/services/meta-service.d.ts +5 -1
  18. package/dist/services/meta-service.js +44 -18
  19. package/dist/services/scenario-actors-service.d.ts +108 -2
  20. package/dist/services/scenario-actors-service.js +40 -1
  21. package/dist/services/workflow-service.d.ts +7 -5
  22. package/dist/types/core.types.d.ts +28 -3
  23. package/dist/types/state.types.d.ts +3 -1
  24. package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
  25. package/dist/wirings/actor-flow/index.d.ts +1 -1
  26. package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
  27. package/dist/wirings/actor-flow/run-conversation.js +27 -27
  28. package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
  30. package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
  31. package/dist/wirings/cli/command-parser.js +11 -1
  32. package/dist/wirings/rpc/rpc-runner.js +1 -1
  33. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
  34. package/dist/wirings/workflow/feature.d.ts +28 -0
  35. package/dist/wirings/workflow/feature.js +57 -0
  36. package/dist/wirings/workflow/graph/graph-runner.js +3 -2
  37. package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
  38. package/dist/wirings/workflow/graph/graph-validation.js +0 -142
  39. package/dist/wirings/workflow/graph/index.d.ts +1 -1
  40. package/dist/wirings/workflow/graph/index.js +1 -1
  41. package/dist/wirings/workflow/index.d.ts +13 -3
  42. package/dist/wirings/workflow/index.js +15 -2
  43. package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
  44. package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
  45. package/dist/wirings/workflow/pikku-workflow-service.d.ts +170 -23
  46. package/dist/wirings/workflow/pikku-workflow-service.js +338 -297
  47. package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
  48. package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
  49. package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
  50. package/dist/wirings/workflow/scenario-poll.js +25 -0
  51. package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
  52. package/dist/wirings/workflow/scenario-prose.js +45 -0
  53. package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
  54. package/dist/wirings/workflow/scenario-step-guards.js +29 -0
  55. package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
  56. package/dist/wirings/workflow/scenario-step.types.js +1 -0
  57. package/dist/wirings/workflow/workflow.types.d.ts +82 -8
  58. package/package.json +3 -1
  59. package/src/function/functions.types.ts +32 -0
  60. package/src/index.ts +1 -0
  61. package/src/internal.ts +5 -1
  62. package/src/pikku-state.ts +1 -0
  63. package/src/services/http-scenario-actors.test.ts +85 -1
  64. package/src/services/http-scenario-actors.ts +65 -51
  65. package/src/services/in-memory-queue-service.test.ts +66 -1
  66. package/src/services/in-memory-queue-service.ts +13 -2
  67. package/src/services/in-memory-workflow-service.ts +12 -25
  68. package/src/services/index.ts +5 -0
  69. package/src/services/meta-service.test.ts +79 -0
  70. package/src/services/meta-service.ts +61 -26
  71. package/src/services/scenario-actors-service.ts +157 -2
  72. package/src/services/workflow-service.ts +7 -4
  73. package/src/types/core.types.ts +34 -2
  74. package/src/types/state.types.ts +3 -0
  75. package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
  76. package/src/wirings/actor-flow/index.ts +1 -1
  77. package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
  78. package/src/wirings/actor-flow/run-conversation.ts +36 -41
  79. package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
  80. package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
  81. package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
  82. package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
  83. package/src/wirings/cli/command-parser.test.ts +60 -0
  84. package/src/wirings/cli/command-parser.ts +12 -1
  85. package/src/wirings/rpc/rpc-runner.test.ts +28 -5
  86. package/src/wirings/rpc/rpc-runner.ts +1 -1
  87. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
  88. package/src/wirings/workflow/feature.test.ts +131 -0
  89. package/src/wirings/workflow/feature.ts +78 -0
  90. package/src/wirings/workflow/graph/graph-runner.ts +3 -2
  91. package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
  92. package/src/wirings/workflow/graph/graph-validation.ts +0 -196
  93. package/src/wirings/workflow/graph/index.ts +1 -5
  94. package/src/wirings/workflow/index.ts +73 -6
  95. package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
  96. package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
  97. package/src/wirings/workflow/pikku-workflow-service.ts +572 -419
  98. package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
  99. package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
  100. package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
  101. package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
  102. package/src/wirings/workflow/scenario-poll.test.ts +66 -0
  103. package/src/wirings/workflow/scenario-poll.ts +36 -0
  104. package/src/wirings/workflow/scenario-prose.test.ts +152 -0
  105. package/src/wirings/workflow/scenario-prose.ts +79 -0
  106. package/src/wirings/workflow/scenario-service.test.ts +155 -0
  107. package/src/wirings/workflow/scenario-step-guards.ts +43 -0
  108. package/src/wirings/workflow/scenario-step.test.ts +442 -9
  109. package/src/wirings/workflow/scenario-step.types.ts +157 -0
  110. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  111. package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
  112. package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
  113. package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
  114. package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
  115. package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
  116. package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
  117. package/src/wirings/workflow/workflow.types.ts +99 -5
  118. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,29 @@
1
+ /** A session held the way a browser holds one, for code driving a real target. */
2
+ export interface ScenarioCookieJar {
3
+ /** `fetch`, but it sends what the target has set and keeps what it sets. */
4
+ fetch: typeof fetch;
5
+ /** Forgets the session — what an actor does before signing in again. */
6
+ clear(): void;
7
+ /**
8
+ * Whether the target has set anything yet. This is a fact about the jar, not
9
+ * about the session: a target that sets a CSRF or locale cookie before anyone
10
+ * signs in fills the jar without establishing one. Whoever needs to know
11
+ * whether a sign-in happened has to track the sign-in.
12
+ */
13
+ readonly empty: boolean;
14
+ }
15
+ /**
16
+ * A `fetch` that remembers cookies.
17
+ *
18
+ * A browser persists the session cookie on its own; anything driving a target
19
+ * from this process has to. Every response is read, not just the sign-in, so a
20
+ * cookie the target rotates mid-session is followed rather than dropped.
21
+ *
22
+ * The jar is a closure local, so two jars never share a session — which is what
23
+ * lets one scenario sign in as several people without one of them inheriting
24
+ * the other's session.
25
+ *
26
+ * It also stamps `Origin`, because Better Auth rejects a state-changing POST
27
+ * whose Origin does not match its baseURL.
28
+ */
29
+ export declare const createCookieJar: (apiUrl: string) => ScenarioCookieJar;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A `fetch` that remembers cookies.
3
+ *
4
+ * A browser persists the session cookie on its own; anything driving a target
5
+ * from this process has to. Every response is read, not just the sign-in, so a
6
+ * cookie the target rotates mid-session is followed rather than dropped.
7
+ *
8
+ * The jar is a closure local, so two jars never share a session — which is what
9
+ * lets one scenario sign in as several people without one of them inheriting
10
+ * the other's session.
11
+ *
12
+ * It also stamps `Origin`, because Better Auth rejects a state-changing POST
13
+ * whose Origin does not match its baseURL.
14
+ */
15
+ export const createCookieJar = (apiUrl) => {
16
+ const jar = new Map();
17
+ const origin = new URL(apiUrl).origin;
18
+ return {
19
+ fetch: async (input, init) => {
20
+ const headers = new Headers(init?.headers);
21
+ headers.set('origin', origin);
22
+ const held = [...jar].map(([name, value]) => `${name}=${value}`);
23
+ const caller = headers.get('cookie');
24
+ if (held.length > 0 || caller) {
25
+ headers.set('cookie', [caller, ...held].filter(Boolean).join('; '));
26
+ }
27
+ const response = await fetch(input, { ...init, headers });
28
+ for (const raw of response.headers.getSetCookie()) {
29
+ const [pair] = raw.split(';');
30
+ const separator = pair.indexOf('=');
31
+ if (separator > 0) {
32
+ const name = pair.slice(0, separator);
33
+ const value = pair.slice(separator + 1);
34
+ // An empty value is how a target deletes a cookie — drop the name
35
+ // rather than holding a cookie whose value says it is gone.
36
+ if (value) {
37
+ jar.set(name, value);
38
+ }
39
+ else {
40
+ jar.delete(name);
41
+ }
42
+ }
43
+ }
44
+ return response;
45
+ },
46
+ clear: () => jar.clear(),
47
+ get empty() {
48
+ return jar.size === 0;
49
+ },
50
+ };
51
+ };
@@ -0,0 +1,20 @@
1
+ /** How long to keep trying, and how long to wait between tries. */
2
+ export interface PollOptions {
3
+ /** Total time to keep attempting before giving up. Default 15s. */
4
+ timeoutMs?: number;
5
+ /** Gap between attempts. Default 250ms. */
6
+ intervalMs?: number;
7
+ }
8
+ /**
9
+ * Attempt something until it answers, or the deadline passes.
10
+ *
11
+ * `undefined` means "not yet" and nothing else — `false`, `0` and `''` are all
12
+ * answers, because a probe asking whether something happened reports `false`
13
+ * when it did not. Answering `undefined` at the deadline rather than throwing
14
+ * leaves the error to the caller, who is the only one who knows what was being
15
+ * waited for.
16
+ *
17
+ * A step that polls its target — a delivery reaching a terminal status, a run
18
+ * finishing — reaches for this rather than writing the deadline loop again.
19
+ */
20
+ export declare const pollUntil: <T>(attempt: () => Promise<T | undefined> | T | undefined, { timeoutMs, intervalMs }?: PollOptions) => Promise<T | undefined>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Attempt something until it answers, or the deadline passes.
3
+ *
4
+ * `undefined` means "not yet" and nothing else — `false`, `0` and `''` are all
5
+ * answers, because a probe asking whether something happened reports `false`
6
+ * when it did not. Answering `undefined` at the deadline rather than throwing
7
+ * leaves the error to the caller, who is the only one who knows what was being
8
+ * waited for.
9
+ *
10
+ * A step that polls its target — a delivery reaching a terminal status, a run
11
+ * finishing — reaches for this rather than writing the deadline loop again.
12
+ */
13
+ export const pollUntil = async (attempt, { timeoutMs = 15_000, intervalMs = 250 } = {}) => {
14
+ const deadline = Date.now() + timeoutMs;
15
+ for (;;) {
16
+ const answer = await attempt();
17
+ if (answer !== undefined) {
18
+ return answer;
19
+ }
20
+ if (Date.now() >= deadline) {
21
+ return undefined;
22
+ }
23
+ await new Promise((done) => setTimeout(done, intervalMs));
24
+ }
25
+ };
@@ -0,0 +1,38 @@
1
+ import type { ScenarioStepPhase } from './scenario-step.types.js';
2
+ /**
3
+ * Renders the English sentence a reporter shows for a scenario step.
4
+ *
5
+ * This is the inversion of cucumber: rather than parsing English into a call,
6
+ * we render English out of a typed call, so the readable report survives
7
+ * without a regex registry paying for it.
8
+ *
9
+ * Lives in core so the CLI reporter and the console render identically.
10
+ */
11
+ /**
12
+ * Fill a step's `template` from the input that step was actually called with,
13
+ * so the reported sentence names the values under test — "sees @pikku/addon-todos"
14
+ * rather than "sees an addon in the gallery" repeated three times.
15
+ *
16
+ * A placeholder with no recorded value renders as nothing and the surrounding
17
+ * whitespace collapses, so an optional input that was omitted reads as a shorter
18
+ * sentence rather than a literal `{state}` leaking into the report.
19
+ */
20
+ export declare const renderStepTemplate: (template: string, input: unknown) => string;
21
+ export declare const composeStepProse: ({ phase, description, template, input, actor, keywordWidth, }: {
22
+ phase: ScenarioStepPhase;
23
+ description: string;
24
+ /**
25
+ * The prose this step renders, with `{placeholders}` filled from `input`.
26
+ * Unlike `description`, which documents what the step does, this is what a
27
+ * reader of the report sees. Falls back to `description` when absent.
28
+ */
29
+ template?: string;
30
+ /** The input this step was called with, as recorded on the run. */
31
+ input?: unknown;
32
+ actor?: string;
33
+ /**
34
+ * Pad the keyword to this width so a ladder of steps lines its sentences up
35
+ * under each other. Omit for inline prose.
36
+ */
37
+ keywordWidth?: number;
38
+ }) => string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Renders the English sentence a reporter shows for a scenario step.
3
+ *
4
+ * This is the inversion of cucumber: rather than parsing English into a call,
5
+ * we render English out of a typed call, so the readable report survives
6
+ * without a regex registry paying for it.
7
+ *
8
+ * Lives in core so the CLI reporter and the console render identically.
9
+ */
10
+ /**
11
+ * Fill a step's `template` from the input that step was actually called with,
12
+ * so the reported sentence names the values under test — "sees @pikku/addon-todos"
13
+ * rather than "sees an addon in the gallery" repeated three times.
14
+ *
15
+ * A placeholder with no recorded value renders as nothing and the surrounding
16
+ * whitespace collapses, so an optional input that was omitted reads as a shorter
17
+ * sentence rather than a literal `{state}` leaking into the report.
18
+ */
19
+ export const renderStepTemplate = (template, input) => {
20
+ const values = input && typeof input === 'object' ? input : {};
21
+ return template
22
+ .replace(/\{(\w+)\}/g, (_match, key) => formatValue(values[key]))
23
+ .replace(/\s+/g, ' ')
24
+ .trim();
25
+ };
26
+ const formatValue = (value) => {
27
+ if (value === undefined || value === null) {
28
+ return '';
29
+ }
30
+ if (typeof value === 'object') {
31
+ return JSON.stringify(value);
32
+ }
33
+ return String(value);
34
+ };
35
+ export const composeStepProse = ({ phase, description, template, input, actor, keywordWidth, }) => {
36
+ const keyword = phase === 'step' ? '' : capitalise(phase);
37
+ const subject = actor ? `the ${actor}` : '';
38
+ const rendered = template ? renderStepTemplate(template, input) : description;
39
+ const sentence = [subject, rendered].filter(Boolean).join(' ');
40
+ if (keywordWidth === undefined) {
41
+ return [keyword, sentence].filter(Boolean).join(' ');
42
+ }
43
+ return `${keyword.padEnd(keywordWidth)} ${sentence}`;
44
+ };
45
+ const capitalise = (value) => value.charAt(0).toUpperCase() + value.slice(1);
@@ -0,0 +1,16 @@
1
+ import type { PikkuScenarioStepWire, ScenarioEnvironment } from './scenario-step.types.js';
2
+ /**
3
+ * The actor this step was called with, or a loud error naming the step.
4
+ *
5
+ * A step that talks to the target app needs an identity, but `actor` is
6
+ * optional on the wire because a pure assertion step needs none. This is the
7
+ * one place that narrowing happens, so every step file stops writing its own
8
+ * `actorOf(...)` guard.
9
+ */
10
+ export declare const requireActor: <TActor>(scenarioStep: PikkuScenarioStepWire<TActor> | undefined) => TActor;
11
+ /**
12
+ * The environment this run targets, or a loud error naming the step. A run
13
+ * started outside `pikku scenario run` only carries one when the server has
14
+ * `API_URL` configured.
15
+ */
16
+ export declare const requireScenarioEnv: (scenarioStep: PikkuScenarioStepWire<unknown> | undefined) => ScenarioEnvironment;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The actor this step was called with, or a loud error naming the step.
3
+ *
4
+ * A step that talks to the target app needs an identity, but `actor` is
5
+ * optional on the wire because a pure assertion step needs none. This is the
6
+ * one place that narrowing happens, so every step file stops writing its own
7
+ * `actorOf(...)` guard.
8
+ */
9
+ export const requireActor = (scenarioStep) => {
10
+ const actor = scenarioStep?.actor;
11
+ if (!actor) {
12
+ throw new Error(`[scenario] step '${scenarioStep?.name ?? 'unknown'}' was called without an actor. ` +
13
+ `Pass { actor: actors.<name> } so it runs as that persona.`);
14
+ }
15
+ return actor;
16
+ };
17
+ /**
18
+ * The environment this run targets, or a loud error naming the step. A run
19
+ * started outside `pikku scenario run` only carries one when the server has
20
+ * `API_URL` configured.
21
+ */
22
+ export const requireScenarioEnv = (scenarioStep) => {
23
+ const env = scenarioStep?.env;
24
+ if (!env) {
25
+ throw new Error(`[scenario] step '${scenarioStep?.name ?? 'unknown'}' needs the target environment, but this run carries none. ` +
26
+ `Run it through 'pikku scenario run <environment>' with scenarios.environments.<environment> declared in pikku.config.json.`);
27
+ }
28
+ return env;
29
+ };
@@ -0,0 +1,148 @@
1
+ import type { ScenarioActor } from '../../services/scenario-actors-service.js';
2
+ /**
3
+ * Scenario steps: named, typed units of scenario behaviour.
4
+ *
5
+ * A step's body is an ordinary pikku function, so it may drive a browser, call
6
+ * an RPC as its actor, or run a workflow. `given`/`when`/`then` are sugar over
7
+ * `step` — they only change the prose the reporter renders.
8
+ */
9
+ /**
10
+ * Which Gherkin-style keyword the reporter prefixes this step with. `step`
11
+ * renders no prefix at all.
12
+ */
13
+ export type ScenarioStepPhase = 'step' | 'given' | 'when' | 'then';
14
+ /**
15
+ * Options accepted by `scenario.step/given/when/then`.
16
+ *
17
+ * Note the retry default differs from an ordinary workflow step: retrying a
18
+ * failed assertion is the wrong behaviour for a test primitive, so steps
19
+ * default to no retries.
20
+ */
21
+ export interface ScenarioStepOptions {
22
+ /** The actor this step runs as. Required for steps declaring `browser: true`. */
23
+ actor?: unknown;
24
+ /** Overrides the step's own `description` for this call site only. */
25
+ description?: string;
26
+ /** Defaults to 0 for steps — a failed assertion must not be retried. */
27
+ retries?: number;
28
+ retryDelay?: number | string;
29
+ }
30
+ /**
31
+ * The environment a scenario run targets, as declared in pikku.config.json
32
+ * under `scenarios.environments`.
33
+ */
34
+ export interface ScenarioEnvironment {
35
+ /** Base API URL of the target app, INCLUDING the HTTP prefix. */
36
+ apiUrl: string;
37
+ /** Base URL of the app's UI, for environments with browser steps. */
38
+ appUrl?: string;
39
+ }
40
+ /**
41
+ * The `scenarioStep` wire, present on every scenario step invocation.
42
+ *
43
+ * `TActor` is the project's own actor type, so a step reaches only the RPCs its
44
+ * actors can actually call. It defaults to the open `ScenarioActor` for a
45
+ * project that declares no registry.
46
+ */
47
+ export interface PikkuScenarioStepWire<TActor = ScenarioActor> {
48
+ /** Registered step name (also its pikkuFuncId) */
49
+ name: string;
50
+ /** Durable key within the run; may carry an `#ordinal` suffix when repeated */
51
+ stepName: string;
52
+ runId: string;
53
+ phase: ScenarioStepPhase;
54
+ /**
55
+ * The actor this step runs as, when one was given. Call RPCs through it
56
+ * (`actor.invoke(...)`) so they run against the target environment as that
57
+ * persona.
58
+ */
59
+ actor?: TActor;
60
+ /**
61
+ * The environment this run targets. A step runs in the CLI process, where
62
+ * there is no `variables` service — this is how a raw-HTTP step learns the
63
+ * target's URL without reaching for `process.env`.
64
+ */
65
+ env?: ScenarioEnvironment;
66
+ }
67
+ /**
68
+ * How a browser step names an element.
69
+ *
70
+ * A `data-testid` on its own is rarely enough to name exactly one: `where`
71
+ * matches the element's own data attributes (so a step asserts a status
72
+ * without reading translated copy back to the app), `prefix` matches a family
73
+ * of ids, `containing` picks the match holding a piece of text, and `within`
74
+ * scopes the lookup to one row or section.
75
+ *
76
+ * Declared here so a step's input type is structural; the driver
77
+ * (`@pikku/playwright`) is what resolves it against a real page.
78
+ */
79
+ export interface TestIdSelector {
80
+ testId: string;
81
+ /** Match every test id beginning with `testId`, e.g. every `flow-card-*`. */
82
+ prefix?: boolean;
83
+ /** Data attributes the element must also carry, e.g. `{ 'data-open': 'true' }`. */
84
+ where?: Record<string, string>;
85
+ /** Narrow to the one match holding this text. */
86
+ containing?: string;
87
+ /** Scope the lookup to one enclosing element, e.g. the row for one user. */
88
+ within?: TestIdSelector;
89
+ }
90
+ /**
91
+ * Structural browser handle, present only when the runner provisioned a
92
+ * browser for this step (`browser: true` on the step config).
93
+ *
94
+ * `@pikku/core` deliberately never imports playwright — it must stay
95
+ * dependency-free for edge runtimes. `@pikku/playwright` augments this
96
+ * interface via `declare module`, so `wire.browser.page` is a fully typed
97
+ * Playwright `Page` in a project that installs it.
98
+ */
99
+ export interface PikkuBrowserWire {
100
+ /** The actor whose browser context this is */
101
+ readonly actor: string;
102
+ goto(url: string): Promise<void>;
103
+ screenshot(name?: string): Promise<Uint8Array>;
104
+ }
105
+ /**
106
+ * What one actor's window looked like at the moment a scenario failed.
107
+ *
108
+ * A browser step fails with a selector timeout that says nothing about *why*
109
+ * the page never rendered. The answer is almost always in the page's own
110
+ * errors, which the driver has been collecting all along.
111
+ */
112
+ export interface ScenarioBrowserFailure {
113
+ /** The actor whose window this is. */
114
+ actor: string;
115
+ /** Where the window was pointed, when the driver can still report it. */
116
+ url?: string;
117
+ /** Path the screenshot was written to; absent when none could be taken. */
118
+ screenshot?: string;
119
+ consoleErrors: string[];
120
+ pageErrors: string[];
121
+ failedRequests: string[];
122
+ apiErrors: string[];
123
+ }
124
+ /**
125
+ * Supplied by `@pikku/playwright` (or any other driver) and consumed by the
126
+ * scenario runner. Declared here so the CLI depends only on core.
127
+ *
128
+ * `reset` and `captureFailure` are optional so a driver written against an
129
+ * earlier version keeps compiling; the runner treats a driver without them as
130
+ * one that simply offers no isolation and no diagnostics.
131
+ */
132
+ export interface ScenarioBrowserProvider {
133
+ /** Resolve — creating on first use — the browser session for an actor. */
134
+ sessionFor(actorName: string): Promise<PikkuBrowserWire>;
135
+ /**
136
+ * Discard every actor's per-scenario state — cookies, storage, open pages —
137
+ * while keeping the browser itself. Called between scenarios, so one
138
+ * scenario cannot leave the next signed in as somebody else.
139
+ */
140
+ reset?(): Promise<void>;
141
+ /**
142
+ * Snapshot every open window for a failed scenario. `label` identifies the
143
+ * scenario in artifact filenames. Never throws: a failure to capture must
144
+ * not replace the failure being captured.
145
+ */
146
+ captureFailure?(label: string): Promise<ScenarioBrowserFailure[]>;
147
+ close(): Promise<void>;
148
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,9 @@
1
1
  import type { SerializedError, CommonWireMeta } from '../../types/core.types.js';
2
- import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
2
+ import type { CorePikkuFunctionConfig, CorePikkuFunctionHook } from '../../function/functions.types.js';
3
3
  import type { GroupConcurrencyConfig } from '../queue/queue.types.js';
4
4
  export type { WorkflowService } from '../../services/workflow-service.js';
5
- export type { WorkflowStepOptions, WorkflowExpectEventuallyOptions, WorkflowExpectErrorOptions, WorkflowExpectServiceOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, WorkflowWireApproval, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, PikkuScenarioWire, } from './dsl/workflow-dsl.types.js';
5
+ export type { WorkflowStepOptions, WorkflowExpectEventuallyOptions, WorkflowExpectErrorOptions, WorkflowExpectServiceOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, WorkflowWireApproval, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, ScenarioStepInvocation, ScenarioStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, PikkuScenarioWire, } from './dsl/workflow-dsl.types.js';
6
+ export type { ScenarioStepPhase, ScenarioStepOptions, PikkuScenarioStepWire, PikkuBrowserWire, ScenarioBrowserProvider, } from './scenario-step.types.js';
6
7
  import type { WorkflowStepMeta } from './dsl/workflow-dsl.types.js';
7
8
  export interface WorkflowRunWire {
8
9
  type: string;
@@ -181,11 +182,6 @@ export interface WorkflowRunService {
181
182
  graph: any;
182
183
  source: string;
183
184
  } | null>;
184
- getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
185
- workflowName: string;
186
- graphHash: string;
187
- graph: any;
188
- }>>;
189
185
  deleteRun(id: string): Promise<boolean>;
190
186
  }
191
187
  /**
@@ -235,6 +231,77 @@ export type CoreWorkflow<PikkuFunctionConfig extends CorePikkuFunctionConfig<any
235
231
  /** Tags for organization and filtering */
236
232
  tags?: string[];
237
233
  };
234
+ /**
235
+ * A scenario as a feature references it: either the scenario itself, or the
236
+ * scenario paired with the input to run it with. The paired form is gherkin's
237
+ * `Examples:` — the same scenario run once per row, written as an ordinary
238
+ * loop rather than a table.
239
+ */
240
+ export type CoreFeatureScenario = CorePikkuFunctionConfig<any, any, any> | {
241
+ scenario: CorePikkuFunctionConfig<any, any, any>;
242
+ data: unknown;
243
+ };
244
+ /**
245
+ * A feature: an ordered group of scenarios, mirroring gherkin's Feature ↔
246
+ * Scenario structure. Scenarios are referenced by imported identifier, so a
247
+ * renamed or deleted scenario is a compile error rather than a silent skip.
248
+ *
249
+ * Hooks run **once around the whole group** (`before → a → b → c → after`),
250
+ * not per scenario — per-scenario setup is the scenario's own `before`. That
251
+ * is the one thing a feature deliberately cannot express: gherkin's
252
+ * `Background:` runs per scenario.
253
+ */
254
+ export type CoreFeature = {
255
+ /** Human-readable name. The export identifier is the id. */
256
+ name: string;
257
+ description?: string;
258
+ tags?: string[];
259
+ /** Readonly because `pikkuFeature`'s `const` generic infers a readonly tuple. */
260
+ scenarios: readonly CoreFeatureScenario[];
261
+ before?: CorePikkuFunctionHook;
262
+ after?: CorePikkuFunctionHook;
263
+ };
264
+ /** One entry of a feature's scenario list, as extracted from the source. */
265
+ export type FeatureMetaEntry = {
266
+ /** The scenario's declared export name — its key in `WorkflowsMeta`. */
267
+ scenario: string;
268
+ /** The input this entry runs the scenario with — gherkin's `Examples:`. */
269
+ data?: unknown;
270
+ };
271
+ /**
272
+ * A feature as the console reads it: the document structure around a group of
273
+ * scenarios. Generated to `scenarios/features.gen.json` and read off disk, so
274
+ * nothing app-facing has to import the scenario bootstrap to describe one.
275
+ */
276
+ export type FeatureMeta = {
277
+ /** The export identifier. */
278
+ id: string;
279
+ name: string;
280
+ description?: string;
281
+ tags: string[];
282
+ /** In declared order — a feature's reading order is its declaration order. */
283
+ entries: FeatureMetaEntry[];
284
+ /**
285
+ * Entries that could not be read statically (a spread, a `.map()`). Their
286
+ * membership is only known once the scenario bootstrap has been evaluated,
287
+ * so a non-zero count means this listing is partial.
288
+ */
289
+ unresolvedEntries: number;
290
+ /** Hooks are runtime-only; only their presence is knowable from meta. */
291
+ hasBefore: boolean;
292
+ hasAfter: boolean;
293
+ };
294
+ export type FeaturesMeta = Record<string, FeatureMeta>;
295
+ /** One planned scenario run, resolved from a feature's scenario list. */
296
+ export type FeaturePlanEntry = {
297
+ featureId: string;
298
+ featureName: string;
299
+ scenarioName: string;
300
+ /** The input this entry runs the scenario with, if the feature supplied one. */
301
+ data?: unknown;
302
+ /** The scenario's own tags unioned with the containing feature's. */
303
+ tags: string[];
304
+ };
238
305
  /**
239
306
  * Workflow client interface
240
307
  */
@@ -274,6 +341,13 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
274
341
  expose?: boolean;
275
342
  /** True for pikkuScenario workflows (complex + actor steps). */
276
343
  scenario?: boolean;
344
+ /**
345
+ * Why a scenario is held out of a default run. Stating the reason in code
346
+ * keeps the quarantine next to the scenario it applies to, rather than in
347
+ * a CI invocation nobody reads. Naming the scenario with `--flows` runs it
348
+ * regardless.
349
+ */
350
+ skip?: string;
277
351
  /** Actor names a scenario declares (personas it runs steps as). */
278
352
  actors?: string[];
279
353
  }>;
@@ -288,7 +362,7 @@ export interface WorkflowRuntimeMeta {
288
362
  /** Pikku function name (for execution) */
289
363
  pikkuFuncId: string;
290
364
  /** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph', 'scenario' (complex + actor steps) */
291
- source: 'dsl' | 'complex' | 'graph' | 'dynamic-workflow' | 'scenario';
365
+ source: 'dsl' | 'complex' | 'graph' | 'scenario';
292
366
  /** Optional description */
293
367
  description?: string;
294
368
  /** Tags for organization */
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.69",
3
+ "version": "0.12.71",
4
+ "description": "The Pikku runtime — functions, wirings, services, middleware and types",
4
5
  "author": "yasser.fadl@gmail.com",
5
6
  "license": "MIT",
6
7
  "module": "dist/index.js",
@@ -22,6 +23,7 @@
22
23
  "./function": "./dist/function/index.js",
23
24
  "./channel": "./dist/wirings/channel/index.js",
24
25
  "./workflow": "./dist/wirings/workflow/index.js",
26
+ "./scenario": "./dist/wirings/workflow/pikku-scenario-service.js",
25
27
  "./workflow/timeline": "./dist/wirings/workflow/run-timeline.js",
26
28
  "./workflow/types": "./dist/wirings/workflow/workflow.types.js",
27
29
  "./actor-flow": "./dist/wirings/actor-flow/index.js",
@@ -248,6 +248,17 @@ export type CorePermissionGroup<PikkuPermission = CorePikkuPermission<any>> =
248
248
  | Record<string, PikkuPermission | PikkuPermission[]>
249
249
  | undefined
250
250
 
251
+ /**
252
+ * A lifecycle hook: the same call signature as the function it hangs off, but
253
+ * its return value is discarded. A hook is setup/teardown, not a step — it has
254
+ * no id, no meta and no schema, so it is never recorded and never replayed.
255
+ */
256
+ export type CorePikkuFunctionHook<Services = any, Data = any, Wire = any> = (
257
+ services: Services,
258
+ data: Data,
259
+ wire: Wire
260
+ ) => Promise<void> | void
261
+
251
262
  export type CorePikkuFunctionConfig<
252
263
  PikkuFunction extends
253
264
  | CorePikkuFunction<any, any, any, any, any>
@@ -282,6 +293,8 @@ export type CorePikkuFunctionConfig<
282
293
  workflowRetries?: number
283
294
  /** Timeout for this function when used as a workflow step (e.g. '30s', '5m'). */
284
295
  workflowTimeout?: string
296
+ /** Scenario steps only: this step drives a browser, so the runner must provision one before calling it. */
297
+ browser?: boolean
285
298
  audit?:
286
299
  | boolean
287
300
  | {
@@ -289,6 +302,25 @@ export type CorePikkuFunctionConfig<
289
302
  }
290
303
  approvalDescription?: any
291
304
  func: PikkuFunction
305
+ /**
306
+ * Scenarios only: runs before the scenario body, with the scenario's own
307
+ * signature. Throwing skips the body and fails the run, but `after` still
308
+ * runs.
309
+ */
310
+ before?: CorePikkuFunctionHook
311
+ /**
312
+ * Scenarios only: always runs after the scenario body, in a `finally`.
313
+ * Throwing fails a run that would otherwise have passed; on an
314
+ * already-failed run it attaches as the `cause` and never replaces the
315
+ * original error.
316
+ */
317
+ after?: CorePikkuFunctionHook
318
+ /**
319
+ * Scenarios only: why this scenario is held out of a default run. It is
320
+ * reported as skipped rather than quietly omitted, and naming it directly
321
+ * with `--flows` runs it anyway.
322
+ */
323
+ skip?: string
292
324
  auth?: boolean
293
325
  /**
294
326
  * Scopes the session must hold to run this function. All of them are
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ export type {
55
55
  CorePikkuAuthConfig,
56
56
  CorePikkuFunction,
57
57
  CorePikkuFunctionConfig,
58
+ CorePikkuFunctionHook,
58
59
  CorePikkuPermission,
59
60
  CorePikkuPermissionConfig,
60
61
  CorePikkuPermissionFactory,
package/src/internal.ts CHANGED
@@ -1,4 +1,8 @@
1
- export { pikkuState, resetPikkuState } from './pikku-state.js'
1
+ export {
2
+ pikkuState,
3
+ resetPikkuState,
4
+ getAllPackageStates,
5
+ } from './pikku-state.js'
2
6
  export { httpRouter } from './wirings/http/routers/http-router.js'
3
7
  export type {
4
8
  CreateSingletonServices,
@@ -104,6 +104,7 @@ const createEmptyPackageState = (): PikkuPackageState => ({
104
104
  },
105
105
  workflows: {
106
106
  registrations: new Map(),
107
+ features: new Map(),
107
108
  meta: {},
108
109
  },
109
110
  trigger: {