@pikku/core 0.12.74 → 0.12.78
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 +397 -0
- package/dist/column-form.d.ts +32 -0
- package/dist/column-form.js +42 -0
- package/dist/crypto-utils.d.ts +15 -4
- package/dist/crypto-utils.js +18 -2
- package/dist/data-classification.d.ts +44 -0
- package/dist/function/functions.types.d.ts +23 -10
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/middleware/auth-bearer.js +2 -1
- package/dist/middleware/remote-auth.js +1 -1
- package/dist/remote.js +1 -1
- package/dist/secret-value.d.ts +56 -0
- package/dist/secret-value.js +46 -0
- package/dist/services/audit-service.d.ts +74 -4
- package/dist/services/audit-service.js +7 -5
- package/dist/services/credential-wire-service.d.ts +5 -0
- package/dist/services/credential-wire-service.js +9 -1
- package/dist/services/email-service.d.ts +2 -1
- package/dist/services/in-memory-workflow-service.d.ts +1 -0
- package/dist/services/in-memory-workflow-service.js +20 -0
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.js +1 -1
- package/dist/services/local-content-request-handler.d.ts +29 -0
- package/dist/services/local-content-request-handler.js +176 -0
- package/dist/services/local-secrets.d.ts +4 -3
- package/dist/services/local-secrets.js +7 -3
- package/dist/services/logger.d.ts +22 -5
- package/dist/services/queue-webhook-service.js +1 -1
- package/dist/services/scoped-secret-service.d.ts +4 -3
- package/dist/services/secret-service.d.ts +8 -3
- package/dist/services/typed-secret-service.d.ts +5 -4
- package/dist/services/webhook-service.d.ts +2 -1
- package/dist/testing/service-tests.js +6 -6
- package/dist/types/core.types.d.ts +25 -4
- package/dist/wirings/ai-agent/ai-agent-agui.js +13 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +7 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +14 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +27 -1
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +40 -0
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +20 -0
- package/dist/wirings/ai-agent/voice-input.js +44 -9
- package/dist/wirings/ai-agent/voice-output.d.ts +15 -0
- package/dist/wirings/ai-agent/voice-output.js +10 -1
- package/dist/wirings/cli/channel/cli-raw-client-runner.d.ts +21 -3
- package/dist/wirings/cli/channel/cli-raw-client-runner.js +13 -5
- package/dist/wirings/cli/channel/index.d.ts +1 -0
- package/dist/wirings/persona/define-personas.d.ts +4 -0
- package/dist/wirings/persona/define-personas.js +4 -0
- package/dist/wirings/persona/persona.types.d.ts +11 -0
- package/dist/wirings/queue/queue-identity.js +2 -1
- package/dist/wirings/queue/queue.types.d.ts +2 -1
- package/dist/wirings/queue/signed-queue-service.d.ts +2 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +2 -1
- package/dist/wirings/rpc/remote-addon-auth.js +6 -2
- package/dist/wirings/virtual-user/index.d.ts +3 -0
- package/dist/wirings/virtual-user/index.js +2 -0
- package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +54 -0
- package/dist/wirings/virtual-user/prepare-virtual-user-run.js +49 -0
- package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +90 -0
- package/dist/wirings/virtual-user/virtual-user-run-store.js +1 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +14 -10
- package/dist/wirings/workflow/pikku-scenario-service.js +1 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +35 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +51 -0
- package/dist/wirings/workflow/scenario-prose.js +1 -1
- package/dist/wirings/workflow/scenario-step.types.d.ts +13 -7
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md +48 -0
- package/knowledge/decisions/internals/core-column-form-is-an-axis-of-its-own.md +84 -0
- package/knowledge/decisions/internals/core-data-classification-brand-is-an-optional-property.md +9 -2
- package/knowledge/decisions/internals/index.md +4 -0
- package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +53 -0
- package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +48 -0
- package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +43 -0
- package/package.json +3 -2
- package/src/column-form.test.ts +97 -0
- package/src/column-form.ts +58 -0
- package/src/crypto-utils.ts +25 -6
- package/src/data-classification.ts +44 -0
- package/src/function/functions.types.ts +47 -10
- package/src/function/index.ts +1 -0
- package/src/index.ts +24 -2
- package/src/middleware/auth-bearer.test.ts +3 -2
- package/src/middleware/auth-bearer.ts +2 -1
- package/src/middleware/remote-auth.test.ts +2 -1
- package/src/middleware/remote-auth.ts +1 -1
- package/src/remote.test.ts +2 -1
- package/src/remote.ts +1 -1
- package/src/secret-value.test.ts +204 -0
- package/src/secret-value.ts +111 -0
- package/src/services/audit-service.ts +87 -9
- package/src/services/credential-wire-service.ts +9 -1
- package/src/services/email-service.ts +3 -1
- package/src/services/in-memory-workflow-service.ts +29 -0
- package/src/services/index.ts +3 -3
- package/src/services/local-content-request-handler.test.ts +202 -0
- package/src/services/local-content-request-handler.ts +267 -0
- package/src/services/local-secrets.test.ts +20 -5
- package/src/services/local-secrets.ts +15 -7
- package/src/services/logger.ts +27 -7
- package/src/services/queue-webhook-service.test.ts +2 -1
- package/src/services/queue-webhook-service.ts +1 -1
- package/src/services/scoped-secret-service.ts +4 -3
- package/src/services/secret-service.ts +8 -3
- package/src/services/typed-secret-service.ts +11 -7
- package/src/services/webhook-service.ts +4 -1
- package/src/testing/service-tests.ts +6 -6
- package/src/types/core.types.ts +25 -4
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +16 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +14 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +7 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +18 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +32 -1
- package/src/wirings/ai-agent/ai-agent.types.ts +45 -1
- package/src/wirings/ai-agent/index.ts +2 -0
- package/src/wirings/ai-agent/voice-input.test.ts +65 -0
- package/src/wirings/ai-agent/voice-input.ts +48 -9
- package/src/wirings/ai-agent/voice-output.test.ts +91 -1
- package/src/wirings/ai-agent/voice-output.ts +28 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +39 -9
- package/src/wirings/cli/channel/index.ts +4 -0
- package/src/wirings/persona/define-personas.ts +4 -0
- package/src/wirings/persona/persona.types.ts +11 -0
- package/src/wirings/queue/queue-identity.test.ts +2 -1
- package/src/wirings/queue/queue-identity.ts +4 -1
- package/src/wirings/queue/queue.types.ts +6 -1
- package/src/wirings/queue/signed-queue-service.ts +2 -1
- package/src/wirings/rpc/remote-addon-auth.ts +8 -3
- package/src/wirings/rpc/rpc-runner.test.ts +6 -4
- package/src/wirings/virtual-user/index.ts +12 -0
- package/src/wirings/virtual-user/prepare-virtual-user-run.test.ts +115 -0
- package/src/wirings/virtual-user/prepare-virtual-user-run.ts +95 -0
- package/src/wirings/virtual-user/virtual-user-run-store.ts +98 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +14 -16
- package/src/wirings/workflow/pikku-scenario-service.ts +1 -10
- package/src/wirings/workflow/pikku-workflow-service.ts +67 -0
- package/src/wirings/workflow/scenario-prose.test.ts +5 -7
- package/src/wirings/workflow/scenario-prose.ts +1 -1
- package/src/wirings/workflow/scenario-service.test.ts +0 -1
- package/src/wirings/workflow/scenario-step.test.ts +4 -5
- package/src/wirings/workflow/scenario-step.types.ts +13 -7
- package/src/wirings/workflow/scenario-surface.test.ts +6 -5
- package/src/wirings/workflow/workflow-stalled-recovery.test.ts +106 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +12 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { VirtualUserDisposition, VirtualUserFinding, VirtualUserTally } from './virtual-user.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One recorded run: who ran, what they were told, and what came back.
|
|
4
|
+
*
|
|
5
|
+
* A run is dispatched and answered for later, so the record is created before
|
|
6
|
+
* the work starts and is the thing the returned `runId` addresses.
|
|
7
|
+
*
|
|
8
|
+
* This record is a run's ONLY trace. A virtual user is not a workflow — it
|
|
9
|
+
* explores, so no two attempts take the same steps and there is nothing to
|
|
10
|
+
* replay — and it is not queued either, because the record already carries the
|
|
11
|
+
* progress a queue would only be holding on the way here.
|
|
12
|
+
*
|
|
13
|
+
* The cost of that is the one thing to know when reading `status`: a restart
|
|
14
|
+
* mid-run leaves a record at `running` with nothing left to finish it. A run
|
|
15
|
+
* older than its budget window and still `running` is dead, not working.
|
|
16
|
+
*/
|
|
17
|
+
export interface VirtualUserRunRecord {
|
|
18
|
+
runId: string;
|
|
19
|
+
persona: string;
|
|
20
|
+
disposition: VirtualUserDisposition;
|
|
21
|
+
/** What makes a run replayable at all — a finding without it is an anecdote. */
|
|
22
|
+
seed: number;
|
|
23
|
+
/**
|
|
24
|
+
* `running` until the engine returns. Not derived from `finishedAt` being
|
|
25
|
+
* unset: a crashed run has no finish time either, and the two are not the
|
|
26
|
+
* same result.
|
|
27
|
+
*/
|
|
28
|
+
status: 'running' | 'completed' | 'failed';
|
|
29
|
+
/** The caller's situational goals, run alongside the derived intents. */
|
|
30
|
+
goals: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Ids and slugs the user carried in, and whatever it learned on the way out.
|
|
33
|
+
* Kept because a finding only reproduces alongside the notes that produced it.
|
|
34
|
+
*/
|
|
35
|
+
memory: Record<string, string>;
|
|
36
|
+
findings: VirtualUserFinding[];
|
|
37
|
+
tally: VirtualUserTally | null;
|
|
38
|
+
/** Which budget or stopping rule ended the run. */
|
|
39
|
+
stoppedBy: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Why the run itself failed, as opposed to what it found. A run that could
|
|
42
|
+
* not start has no findings and is not a clean empty result.
|
|
43
|
+
*/
|
|
44
|
+
error: string | null;
|
|
45
|
+
/** The session that started it, where the host tracks one. */
|
|
46
|
+
startedBy: string | null;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
finishedAt: Date | null;
|
|
49
|
+
}
|
|
50
|
+
/** What a run is created with — everything else is filled in by the outcome. */
|
|
51
|
+
export interface VirtualUserRunStart {
|
|
52
|
+
persona: string;
|
|
53
|
+
disposition: VirtualUserDisposition;
|
|
54
|
+
seed: number;
|
|
55
|
+
goals?: readonly string[];
|
|
56
|
+
memory?: Record<string, string>;
|
|
57
|
+
startedBy?: string | null;
|
|
58
|
+
}
|
|
59
|
+
/** The outcome of a run that reached the end of its budget without throwing. */
|
|
60
|
+
export interface VirtualUserRunOutcome {
|
|
61
|
+
findings: readonly VirtualUserFinding[];
|
|
62
|
+
tally: VirtualUserTally;
|
|
63
|
+
memory: Record<string, string>;
|
|
64
|
+
stoppedBy: string | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Where runs are kept. Declared here rather than in a database package so the
|
|
68
|
+
* scaffolded RPCs depend on the shape and not on kysely — `@pikku/kysely` ships
|
|
69
|
+
* one implementation, and an app with its own store satisfies this instead.
|
|
70
|
+
*
|
|
71
|
+
* SECURITY: findings from an `adversarial` run are working exploits carrying
|
|
72
|
+
* live ids. An implementation is a privileged store; the scaffold gates every
|
|
73
|
+
* read behind a scope for that reason, and a host exposing these records more
|
|
74
|
+
* widely is publishing its own exploits.
|
|
75
|
+
*/
|
|
76
|
+
export interface VirtualUserRunStore {
|
|
77
|
+
/** Records a run as `running` and returns its id. */
|
|
78
|
+
start(run: VirtualUserRunStart): Promise<string>;
|
|
79
|
+
/** Marks a run `completed` and stores what it found. */
|
|
80
|
+
complete(runId: string, outcome: VirtualUserRunOutcome): Promise<void>;
|
|
81
|
+
/** Marks a run `failed`. The run itself broke; it has no findings. */
|
|
82
|
+
fail(runId: string, error: string): Promise<void>;
|
|
83
|
+
get(runId: string): Promise<VirtualUserRunRecord | null>;
|
|
84
|
+
/** Newest first. `persona` narrows to one persona's history. */
|
|
85
|
+
list(options?: {
|
|
86
|
+
persona?: string;
|
|
87
|
+
limit?: number;
|
|
88
|
+
offset?: number;
|
|
89
|
+
}): Promise<VirtualUserRunRecord[]>;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -62,7 +62,7 @@ export type WorkflowWireDoRPC = <TOutput = any, TInput = any>(stepName: string,
|
|
|
62
62
|
*/
|
|
63
63
|
export type WorkflowWireDoInline = <T>(stepName: string, fn: () => Promise<T> | T, options?: WorkflowStepOptions) => Promise<T>;
|
|
64
64
|
/**
|
|
65
|
-
* Type signature for scenario.
|
|
65
|
+
* Type signature for scenario.given/when/then - used by inspector.
|
|
66
66
|
*
|
|
67
67
|
* Deliberately mirrors WorkflowWireDoRPC's shape: the target is a string, not
|
|
68
68
|
* an imported symbol, so the extractor reads it as a literal.
|
|
@@ -181,7 +181,7 @@ export interface RpcStepMeta {
|
|
|
181
181
|
expectEventually?: boolean;
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
|
-
* Scenario step metadata — a call to `scenario.
|
|
184
|
+
* Scenario step metadata — a call to `scenario.given/when/then`.
|
|
185
185
|
*
|
|
186
186
|
* Distinct from RpcStepMeta on purpose: a step runs locally through
|
|
187
187
|
* runPikkuFunc and must never be treated as dispatchable on the queue/replay
|
|
@@ -194,7 +194,7 @@ export interface ScenarioStepMeta {
|
|
|
194
194
|
stepName: string;
|
|
195
195
|
/** Registered name of the step function being run */
|
|
196
196
|
stepFunc: string;
|
|
197
|
-
/** Which keyword the reporter prefixes —
|
|
197
|
+
/** Which keyword the reporter prefixes — Given, When or Then */
|
|
198
198
|
phase: ScenarioStepPhase;
|
|
199
199
|
/** Output variable name (if assigned) */
|
|
200
200
|
outputVar?: string;
|
|
@@ -484,18 +484,22 @@ export interface PikkuScenarioWire extends PikkuWorkflowWire {
|
|
|
484
484
|
/** Stub-assertion step: asserts `service.method` was called on the target server */
|
|
485
485
|
expectService: (stepName: string, serviceMethod: string, options?: WorkflowExpectServiceOptions) => Promise<void>;
|
|
486
486
|
/**
|
|
487
|
-
* Run a registered scenario step
|
|
488
|
-
*
|
|
489
|
-
*
|
|
487
|
+
* Run a registered scenario step, as the setup the scenario starts from.
|
|
488
|
+
*
|
|
489
|
+
* Shaped exactly like `do`'s RPC form — `(stepName, target, data, options)` —
|
|
490
|
+
* so the inspector reads the target as a string literal rather than resolving
|
|
491
|
+
* an imported symbol.
|
|
490
492
|
*
|
|
491
493
|
* The generated `TypedScenario` narrows these over `FlattenedScenarioStepMap`.
|
|
492
494
|
*/
|
|
493
|
-
step(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
|
|
494
|
-
/** `step` with a "Given" prefix in the rendered prose */
|
|
495
495
|
given(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
|
|
496
|
-
/** `
|
|
496
|
+
/** `when`: the same call as `given`, rendered as the action under test */
|
|
497
497
|
when(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
|
|
498
|
-
/**
|
|
498
|
+
/**
|
|
499
|
+
* `then`: a claim about what the action left behind. Unlike `given`/`when`
|
|
500
|
+
* this is not prose alone — the step's bindings become witnesses, so every
|
|
501
|
+
* declared surface is observed and the observations must agree.
|
|
502
|
+
*/
|
|
499
503
|
then(stepName: string, stepFunc: string, data?: any, options?: ScenarioStepOptions): Promise<any>;
|
|
500
504
|
runScheduledTask: (name: string) => Promise<unknown>;
|
|
501
505
|
}
|
|
@@ -452,12 +452,11 @@ export class PikkuScenarioService {
|
|
|
452
452
|
}, options);
|
|
453
453
|
},
|
|
454
454
|
// Scenario steps: a named `pikkuScenarioStep` run as one durable step.
|
|
455
|
-
// `given`/`when
|
|
455
|
+
// `given`/`when` are sugar for each other, differing only in the
|
|
456
456
|
// prose a reporter renders. `then` is not: the phase is what decides
|
|
457
457
|
// whether the step's bindings are alternatives or witnesses, so the same
|
|
458
458
|
// step function called two ways runs differently. See
|
|
459
459
|
// {@link resolveScenarioSurfaces}.
|
|
460
|
-
step: (stepName, stepFunc, data, options) => this.scenarioStep('step', scenarioStepContext(), stepName, stepFunc, data, options),
|
|
461
460
|
given: (stepName, stepFunc, data, options) => this.scenarioStep('given', scenarioStepContext(), stepName, stepFunc, data, options),
|
|
462
461
|
when: (stepName, stepFunc, data, options) => this.scenarioStep('when', scenarioStepContext(), stepName, stepFunc, data, options),
|
|
463
462
|
then: (stepName, stepFunc, data, options) => this.scenarioStep('then', scenarioStepContext(), stepName, stepFunc, data, options),
|
|
@@ -168,6 +168,41 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
168
168
|
source: string;
|
|
169
169
|
} | null>;
|
|
170
170
|
resumeWorkflow(runId: string, workflowName?: string): Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* Ids of runs that are stalled: still `running`, with no step in a state that
|
|
173
|
+
* something is expected to complete (`running`, `scheduled`, `suspended`),
|
|
174
|
+
* and no step activity since `before`.
|
|
175
|
+
*
|
|
176
|
+
* Returns nothing by default so a store that cannot express the query keeps
|
|
177
|
+
* working unchanged; a store that overrides it gains crash recovery through
|
|
178
|
+
* `recoverStalledRuns`.
|
|
179
|
+
*/
|
|
180
|
+
protected findStalledRunIds(_before: Date, _limit: number): Promise<string[]>;
|
|
181
|
+
/**
|
|
182
|
+
* Re-drive runs whose next move was lost, and report which were resumed.
|
|
183
|
+
*
|
|
184
|
+
* Arming a step is two writes to two systems — the step row, then the queue
|
|
185
|
+
* or scheduler job — so a process that dies between them leaves a run that is
|
|
186
|
+
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
187
|
+
* that will never complete and never error, so it neither finishes nor fails.
|
|
188
|
+
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
189
|
+
* step's insert and its timer, parking the run permanently.)
|
|
190
|
+
*
|
|
191
|
+
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
|
|
192
|
+
* step state, and every settled step is memoized, so resuming a run that was
|
|
193
|
+
* not actually stuck costs an orchestration pass and changes nothing. That
|
|
194
|
+
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
195
|
+
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
196
|
+
*
|
|
197
|
+
* This is not self-starting. Call it from a scheduled task at whatever
|
|
198
|
+
* interval suits the workload.
|
|
199
|
+
*/
|
|
200
|
+
recoverStalledRuns(options?: {
|
|
201
|
+
stalledAfterMs?: number;
|
|
202
|
+
limit?: number;
|
|
203
|
+
}): Promise<{
|
|
204
|
+
resumed: string[];
|
|
205
|
+
}>;
|
|
171
206
|
protected resolveStepJobOptions(stepOptions?: WorkflowStepOptions): JobOptions;
|
|
172
207
|
queueStepWorker(runId: string, stepName: string, rpcName: string, data: any, stepOptions?: WorkflowStepOptions, fromStepName?: string): Promise<void>;
|
|
173
208
|
executeWorkflowSleepCompleted(runId: string, stepId: string): Promise<void>;
|
|
@@ -142,6 +142,10 @@ const WORKFLOW_TERMINAL_STATES = new Set([
|
|
|
142
142
|
'failed',
|
|
143
143
|
'cancelled',
|
|
144
144
|
]);
|
|
145
|
+
/** Idle window before a `running` run with nothing in flight is treated as stalled. */
|
|
146
|
+
const DEFAULT_STALLED_RUN_MS = 5 * 60_000;
|
|
147
|
+
/** Runs re-driven per `recoverStalledRuns` call, so one sweep is bounded. */
|
|
148
|
+
const DEFAULT_STALLED_RUN_LIMIT = 100;
|
|
145
149
|
const WORKFLOW_POLL_MIN_MS = 10;
|
|
146
150
|
const WORKFLOW_POLL_FACTOR = 1.6;
|
|
147
151
|
const WORKFLOW_CHILD_POLL_MAX_MS = 500;
|
|
@@ -435,6 +439,53 @@ export class PikkuWorkflowService {
|
|
|
435
439
|
group: this.getJobGroup(workflowName),
|
|
436
440
|
});
|
|
437
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Ids of runs that are stalled: still `running`, with no step in a state that
|
|
444
|
+
* something is expected to complete (`running`, `scheduled`, `suspended`),
|
|
445
|
+
* and no step activity since `before`.
|
|
446
|
+
*
|
|
447
|
+
* Returns nothing by default so a store that cannot express the query keeps
|
|
448
|
+
* working unchanged; a store that overrides it gains crash recovery through
|
|
449
|
+
* `recoverStalledRuns`.
|
|
450
|
+
*/
|
|
451
|
+
async findStalledRunIds(_before, _limit) {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Re-drive runs whose next move was lost, and report which were resumed.
|
|
456
|
+
*
|
|
457
|
+
* Arming a step is two writes to two systems — the step row, then the queue
|
|
458
|
+
* or scheduler job — so a process that dies between them leaves a run that is
|
|
459
|
+
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
460
|
+
* that will never complete and never error, so it neither finishes nor fails.
|
|
461
|
+
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
462
|
+
* step's insert and its timer, parking the run permanently.)
|
|
463
|
+
*
|
|
464
|
+
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
|
|
465
|
+
* step state, and every settled step is memoized, so resuming a run that was
|
|
466
|
+
* not actually stuck costs an orchestration pass and changes nothing. That
|
|
467
|
+
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
468
|
+
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
469
|
+
*
|
|
470
|
+
* This is not self-starting. Call it from a scheduled task at whatever
|
|
471
|
+
* interval suits the workload.
|
|
472
|
+
*/
|
|
473
|
+
async recoverStalledRuns(options) {
|
|
474
|
+
const before = new Date(Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS));
|
|
475
|
+
const runIds = await this.findStalledRunIds(before, options?.limit ?? DEFAULT_STALLED_RUN_LIMIT);
|
|
476
|
+
const resumed = [];
|
|
477
|
+
for (const runId of runIds) {
|
|
478
|
+
try {
|
|
479
|
+
await this.resumeWorkflow(runId);
|
|
480
|
+
resumed.push(runId);
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
// One unresumable run must not stop the sweep from recovering the rest.
|
|
484
|
+
getSingletonServices()?.logger?.error(`Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return { resumed };
|
|
488
|
+
}
|
|
438
489
|
resolveStepJobOptions(stepOptions) {
|
|
439
490
|
const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES;
|
|
440
491
|
const retryDelay = stepOptions?.retryDelay;
|
|
@@ -15,7 +15,7 @@ const formatValue = (value) => {
|
|
|
15
15
|
return String(value);
|
|
16
16
|
};
|
|
17
17
|
export const composeStepProse = ({ phase, description, template, input, actor, keywordWidth, }) => {
|
|
18
|
-
const keyword =
|
|
18
|
+
const keyword = capitalise(phase);
|
|
19
19
|
const subject = actor ? `the ${actor}` : '';
|
|
20
20
|
const rendered = template ? renderStepTemplate(template, input) : description;
|
|
21
21
|
const sentence = [subject, rendered].filter(Boolean).join(' ');
|
|
@@ -3,14 +3,20 @@ import type { ScenarioPersona } from '../../services/personas-service.js';
|
|
|
3
3
|
* Scenario steps: named, typed units of scenario behaviour.
|
|
4
4
|
*
|
|
5
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
|
|
7
|
-
*
|
|
6
|
+
* an RPC as its actor, or run a workflow. `given` and `when` differ only in the
|
|
7
|
+
* prose the reporter renders; `then` also changes what runs, because its
|
|
8
|
+
* bindings are witnesses — every declared surface is observed and the
|
|
9
|
+
* observations must agree. See {@link ScenarioSurfaceResolution}.
|
|
8
10
|
*/
|
|
9
11
|
/**
|
|
10
|
-
* Which Gherkin-style keyword the reporter prefixes this step with.
|
|
11
|
-
*
|
|
12
|
+
* Which Gherkin-style keyword the reporter prefixes this step with.
|
|
13
|
+
*
|
|
14
|
+
* Every step takes one. A scenario is read by people deciding whether it
|
|
15
|
+
* describes the behaviour they wanted, and a step that says only what it does
|
|
16
|
+
* without saying whether it is setup, action or claim is the one nobody can
|
|
17
|
+
* check — which is also why PKU680 can tell a scenario that never asserts.
|
|
12
18
|
*/
|
|
13
|
-
export type ScenarioStepPhase = '
|
|
19
|
+
export type ScenarioStepPhase = 'given' | 'when' | 'then';
|
|
14
20
|
/**
|
|
15
21
|
* Who acts in a step.
|
|
16
22
|
*
|
|
@@ -41,7 +47,7 @@ export declare const SCENARIO_SURFACES: readonly ScenarioSurface[];
|
|
|
41
47
|
/**
|
|
42
48
|
* How a step's declared surfaces resolve for one run.
|
|
43
49
|
*
|
|
44
|
-
* `given`/`when
|
|
50
|
+
* `given`/`when` bindings are **alternatives** — clicking Buy and calling
|
|
45
51
|
* `createOrder` are two ways to cause one effect, so exactly one runs.
|
|
46
52
|
*
|
|
47
53
|
* `then` bindings are **witnesses** — "the order row says paid" and "the
|
|
@@ -73,7 +79,7 @@ export type ScenarioSurfaceResolution = {
|
|
|
73
79
|
unwitnessed: boolean;
|
|
74
80
|
};
|
|
75
81
|
/**
|
|
76
|
-
* Options accepted by `scenario.
|
|
82
|
+
* Options accepted by `scenario.given/when/then`.
|
|
77
83
|
*
|
|
78
84
|
* Note the retry default differs from an ordinary workflow step: retrying a
|
|
79
85
|
* failed assertion is the wrong behaviour for a test primitive, so steps
|
|
@@ -188,6 +188,13 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
|
|
|
188
188
|
dsl?: boolean;
|
|
189
189
|
expose?: boolean;
|
|
190
190
|
scenario?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* The flow asserts through an expectation helper — `expectService`,
|
|
193
|
+
* `expectError`, `expectEventually` — rather than a `then` step. Those are
|
|
194
|
+
* inline steps and carry no phase, so without this PKU680 reads a scenario
|
|
195
|
+
* whose only witness is a recorded service call as asserting nothing.
|
|
196
|
+
*/
|
|
197
|
+
asserts?: boolean;
|
|
191
198
|
skip?: string;
|
|
192
199
|
actors?: string[];
|
|
193
200
|
}>;
|
package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user run is not a workflow and not a queued job
|
|
4
|
+
description: runVirtualUser writes its record, dispatches the run without awaiting it, and returns the id — because an exploratory run has nothing to replay and the record already carries what a queue would be holding
|
|
5
|
+
tags: virtual-user, storage
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user run is not a workflow and not a queued job
|
|
9
|
+
|
|
10
|
+
`runVirtualUser` — the RPC `scaffold.virtualUser` generates — does three things
|
|
11
|
+
in order: writes a `VirtualUserRunStore` record, dispatches
|
|
12
|
+
`executeVirtualUserRun` **without awaiting it**, and returns the `runId`. There
|
|
13
|
+
is no workflow, no queue, and no worker.
|
|
14
|
+
|
|
15
|
+
Both of the alternatives are the obvious ones, and both are wrong for this.
|
|
16
|
+
|
|
17
|
+
**A workflow** is a replayable step graph: its value is that a run can be
|
|
18
|
+
resumed at the step it died on, and that the same input reaches the same step.
|
|
19
|
+
A virtual user is the opposite by construction — it is an LLM deciding what to
|
|
20
|
+
try next, so no two attempts take the same steps, and there is no step to resume
|
|
21
|
+
*to*. Recording a run as a workflow puts entries in the workflow store that can
|
|
22
|
+
never be replayed, and gives every operator reading that store a row that lies
|
|
23
|
+
about what it is. The seed makes a run *reproducible* — run it again and it
|
|
24
|
+
explores the same way — which is a different property from resumable, and one
|
|
25
|
+
the record already carries.
|
|
26
|
+
|
|
27
|
+
**A queue** buys durability across a restart and a retry on failure. It costs a
|
|
28
|
+
broker dependency in every application that turns the scaffold on, plus a worker
|
|
29
|
+
whose progress cannot be read anyway: the run's state lives in the store, not in
|
|
30
|
+
the queue entry. The queue would be holding a copy of what the record already
|
|
31
|
+
has, on the way to the same place.
|
|
32
|
+
|
|
33
|
+
So the record is the run's only trace, and that is what `VirtualUserRunStore`
|
|
34
|
+
exists for. It is also why `fail()` is a method rather than an absence: a run
|
|
35
|
+
that crashed and a run that found nothing are different answers, and a record
|
|
36
|
+
left at `running` is neither.
|
|
37
|
+
|
|
38
|
+
The cost is real and is stated on the type: **a restart mid-run strands a record
|
|
39
|
+
at `running` with nothing left to finish it.** A run older than its budget
|
|
40
|
+
window and still `running` is dead, not working — that is a read-side rule, and
|
|
41
|
+
it is cheaper than the two dependencies avoided. Nothing retries; a stranded run
|
|
42
|
+
is started again, with its seed if the caller wants the same exploration.
|
|
43
|
+
|
|
44
|
+
**What this rules out:** dispatching the run through `startWorkflow`; a
|
|
45
|
+
scaffolded queue worker; awaiting the engine inside the request (a run takes
|
|
46
|
+
minutes and survives neither a rollout nor a proxy timeout); and inferring
|
|
47
|
+
`status` from `finishedAt` being unset, which cannot separate a crash from a run
|
|
48
|
+
still going.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A column's at-rest form is an axis of its own
|
|
4
|
+
description: How a value is stored is independent of how sensitive it is, so form carries a required nominal brand on writes while classification stays optional on reads
|
|
5
|
+
tags: core
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A column's at-rest form is an axis of its own
|
|
9
|
+
|
|
10
|
+
`ColumnForm` in `packages/core/src/data-classification.ts` is a second,
|
|
11
|
+
independent annotation on a column: `plain | hashed | wrapped | sealed`. It
|
|
12
|
+
answers "how are these bytes held?", where `Classification` answers "may this
|
|
13
|
+
value leave the process?".
|
|
14
|
+
|
|
15
|
+
The two axes were one field before this, with `security: 'encrypted'` sitting
|
|
16
|
+
alongside `secret` as though they were alternatives. They are not, and the
|
|
17
|
+
conflation made the field unanswerable: a token hash and a live bearer token are
|
|
18
|
+
both `secret`, one must never be encrypted — the digest *is* the lookup key —
|
|
19
|
+
and the other must always be. Nothing in a single enum could tell them apart, so
|
|
20
|
+
nothing could check either.
|
|
21
|
+
|
|
22
|
+
## Why `wrapped` and `sealed` rather than `encrypted`
|
|
23
|
+
|
|
24
|
+
Sealed values *are* encrypted, so an `encrypted` member sitting beside `sealed`
|
|
25
|
+
would be a supertype posing as a sibling, and every new column would be an
|
|
26
|
+
even-odds guess. What actually separates them is who can read the value back:
|
|
27
|
+
`wrapped` is symmetric and the application holds the key; `sealed` is asymmetric
|
|
28
|
+
and the application holds only the public half. Writing one where the other
|
|
29
|
+
belongs produces a row nobody can ever open, which is why the type system is
|
|
30
|
+
made to know the difference.
|
|
31
|
+
|
|
32
|
+
## Why these brands are required when `Secret<T>` is optional
|
|
33
|
+
|
|
34
|
+
`WrappedValue`, `SealedValue` and `HashedValue` are `string & { readonly [sym]:
|
|
35
|
+
true }` with a `unique symbol` — nominal, and **required**, which
|
|
36
|
+
[the classification-brand decision](core-data-classification-brand-is-an-optional-property.md)
|
|
37
|
+
explicitly rules out for `Private`/`Pii`/`Secret`. That decision still stands and
|
|
38
|
+
this does not weaken it. It applies to a different side of a different set of
|
|
39
|
+
columns:
|
|
40
|
+
|
|
41
|
+
- `Secret<T>` brands **every** classified column's SELECT type. A required brand
|
|
42
|
+
there would break `where('email', '=', someString)` in every downstream
|
|
43
|
+
project.
|
|
44
|
+
- A form brands **only** the INSERT/UPDATE type, and only on the columns that
|
|
45
|
+
opt in by declaring a form. There is nothing to break, because a column
|
|
46
|
+
without a form generates exactly what it generated before.
|
|
47
|
+
|
|
48
|
+
The brands compose rather than compete: a wrapped secret column selects as
|
|
49
|
+
`Secret<WrappedValue>`, so the inspector's PKU910 check still finds
|
|
50
|
+
`__classification__`, while a row read back is already a `WrappedValue` and
|
|
51
|
+
flows into a rewrap or re-seal without a cast.
|
|
52
|
+
|
|
53
|
+
Each brand widens to `string`, so query operands, serialization and template
|
|
54
|
+
literals are unaffected. The constraint is on **construction**: the only way to
|
|
55
|
+
produce one is `envelopeEncrypt`/`envelopeRewrap`/`wrapDEK` (wrapped) or
|
|
56
|
+
`hashToken` (hashed), or the deliberately-named `unsafeAs*` assertions in
|
|
57
|
+
`column-form.ts` for the three cases a bare string legitimately arrives —
|
|
58
|
+
backfill migrations, test fixtures, and values sealed by another service.
|
|
59
|
+
|
|
60
|
+
## What is deliberately NOT enforced
|
|
61
|
+
|
|
62
|
+
`envelopeDecrypt` and `unwrapDEK` take plain `string`, not the brand. Requiring
|
|
63
|
+
it would buy nothing — feeding in the wrong string already fails at the AEAD tag
|
|
64
|
+
— while forcing a cast into every path that reads ciphertext out of a row or off
|
|
65
|
+
the wire, which is where casts are least reviewable.
|
|
66
|
+
|
|
67
|
+
The brand proves *provenance*, not correctness. It cannot know a value was
|
|
68
|
+
wrapped under the right key, and making it know would mean phantom-typing key
|
|
69
|
+
ids per scope, which the multi-recipient path would fight constantly.
|
|
70
|
+
|
|
71
|
+
## The plain-secret diagnostic
|
|
72
|
+
|
|
73
|
+
A `secret` column with no declared form raises **PKU483** as a warning, not an
|
|
74
|
+
error. Every project predating the axis has such columns, and failing their next
|
|
75
|
+
`db migrate` would be a breaking change for a diagnosis they have not had a
|
|
76
|
+
chance to act on. `pikku db --fail-on-warn` is how a project opts into the
|
|
77
|
+
ratchet. An explicit `form: 'plain'` silences it — that is the acknowledgement
|
|
78
|
+
that reading the row is *meant* to yield a usable credential.
|
|
79
|
+
|
|
80
|
+
**What this rules out:** collapsing `wrapped` and `sealed` back into one
|
|
81
|
+
`encrypted`; making the form brands optional (they would enforce nothing);
|
|
82
|
+
re-declaring them in generated schema files, since a local `unique symbol` is a
|
|
83
|
+
distinct nominal type and core's own ciphertext would not be assignable to the
|
|
84
|
+
column it belongs in — `db-codegen` imports them from `@pikku/core` instead.
|
package/knowledge/decisions/internals/core-data-classification-brand-is-an-optional-property.md
CHANGED
|
@@ -22,10 +22,17 @@ goes into one.
|
|
|
22
22
|
|
|
23
23
|
The consumer is `@pikku/inspector`, whose `findPiiPaths` reads the level union
|
|
24
24
|
directly and whose PKU910 output check detects the brand on a function's return
|
|
25
|
-
type. The brands are populated from
|
|
26
|
-
|
|
25
|
+
type. The brands are populated from the hand-authored `db/annotations.ts`
|
|
26
|
+
(`DbClassificationMap`) via `pikku db migrate`, which regenerates
|
|
27
27
|
`outDir/db/schema.d.ts` and `outDir/db/classification.gen.ts`.
|
|
28
28
|
|
|
29
|
+
A column's **at-rest form** is a separate axis making the opposite trade — see
|
|
30
|
+
[form is an axis of its own](core-column-form-is-an-axis-of-its-own.md). Its
|
|
31
|
+
brands are nominal and required, which is safe precisely because they land on
|
|
32
|
+
the INSERT/UPDATE side of the columns that opt in, rather than on the SELECT
|
|
33
|
+
side of every classified column. That is not an exception to the rule below; it
|
|
34
|
+
is a different rule about a different side.
|
|
35
|
+
|
|
29
36
|
**What this rules out:** making `__classification__` required to get stronger
|
|
30
37
|
guarantees, or replacing the optional property with a unique symbol / nominal
|
|
31
38
|
brand that behaves like a required one. Either change compiles here and then
|
|
@@ -38,6 +38,7 @@ caller is entitled to assume.
|
|
|
38
38
|
- [CLI parse errors are routed by message prefix](cli-parse-errors-are-routed-by-message-prefix.md) — The CLI runners decide between printing help and printing errors by string-matching the prefixes the parser writes, so those message strings are an interface
|
|
39
39
|
- [CLI stdout is reserved for machine-readable output](cli-stdout-is-reserved-for-machine-readable-output.md) — The default renderer emits single-line NDJSON, diagnostics go to stderr, and --json only hijacks rendering for commands that declared a renderer
|
|
40
40
|
- [CLI unknown long options warn instead of failing](cli-unknown-long-options-warn-instead-of-failing.md) — Unrecognised --long options are accepted, warned about and dropped so older binaries tolerate newer invocations, while unknown short flags stay hard errors
|
|
41
|
+
- [A column's at-rest form is an axis of its own](core-column-form-is-an-axis-of-its-own.md) — How a value is stored is independent of how sensitive it is, so form carries a required nominal brand on writes while classification stays optional on reads
|
|
41
42
|
- [The data-classification brand is an optional property](core-data-classification-brand-is-an-optional-property.md) — Making __classification__ required would break ordinary Kysely operands, so the brand only constrains values flowing out
|
|
42
43
|
- [The function runner restores the wire fields it overwrites](core-function-runner-restores-the-wire-fields-it-overwrites.md) — One wire object is reused across nested calls, so functionId, audit, addonNamespace and rpc are saved and put back in a finally
|
|
43
44
|
- [Hot reload merges generated meta and never replaces it](core-hot-reload-merges-generated-meta-never-replaces-it.md) — Reloading codegen output must preserve runtime-registered meta, which no generated JSON contains
|
|
@@ -77,8 +78,11 @@ caller is entitled to assume.
|
|
|
77
78
|
- [The in-memory workflow service is inline-only and single-process](the-in-memory-workflow-service-is-inline-only-and-single-process.md) — InMemoryWorkflowService wires no queues and implements withRunLock/withStepLock as pass-throughs, because inline execution has no second holder to exclude
|
|
78
79
|
- [The KEK salt is scoped to the key version, not the secret](the-kek-salt-is-scoped-to-the-key-version.md) — One stored salt per key version means N secrets cost one derivation, which is the point of envelope encryption
|
|
79
80
|
- [The schema service is never stubbed, or tests validate nothing](the-schema-service-is-never-stubbed.md) — createStubProxy returns undefined for the schema property so the real schema service is built — a stubbed one turns validation into a silent no-op
|
|
81
|
+
- [One project-shape check, called by both validators](one-project-shape-check-two-validators.md) — workspace validate and fabric validate were separate walks over the same project that duplicated sixteen findings verbatim; the shared half now lives in shared-checks.ts
|
|
82
|
+
- [Scenarios, steps and personas live in files named for them](scenarios-live-in-files-named-for-them.md) — pikkuScenario/pikkuScenarioStep must be in *.scenario.ts/*.steps.ts and definePersonas in *.virtual-user.ts, an error because the mixing is only cheap to undo while it is one file
|
|
80
83
|
- [Trigger declaration is split from trigger source](trigger-declaration-is-split-from-trigger-source.md) — Triggers are declared everywhere but subscribed only in the trigger worker, so app processes never open the underlying subscription
|
|
81
84
|
- [TypedSecretService caches for the process lifetime](typed-secret-service-caches-for-the-process-lifetime.md) — Resolved secrets are cached with no TTL, so a secret rotated out of band is not picked up until restart — tracked as pikkujs/pikku#964
|
|
85
|
+
- [Both validators check personas through one shared module, and a persona counts from either the meta or the source](validate-checks-personas-through-a-shared-module.md) — workspace validate and fabric validate are separate implementations, so the persona/actor/knowledge checks live in persona-checks.ts and are called by both
|
|
82
86
|
- [Webhook delivery history records every attempt, best effort](webhook-delivery-history-records-every-attempt-best-effort.md) — The webhook worker persists each attempt before it throws, and a failure to persist is logged rather than allowed to mask the delivery result
|
|
83
87
|
- [Webhook service collaborators are constructor args, not locator lookups](webhook-service-collaborators-are-constructor-args-not-locator-lookups.md) — QueueWebhookService takes its queue as a constructor parameter so a project wiring webhooks without a queue fails to compile instead of at first send
|
|
84
88
|
- [Whether a run is inline is read from the run record](whether-a-run-is-inline-is-read-from-the-run-record.md) — The runContexts map is a read-through cache over WorkflowRun.inline and a lifetime for replay ordinals, never the answer to what a run is
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: One project-shape check, called by both validators
|
|
4
|
+
description: workspace validate and fabric validate were separate walks over the same project that duplicated sixteen findings verbatim; the shared half now lives in shared-checks.ts and fabric validate is that plus the deploy-shaped checks
|
|
5
|
+
tags: cli, validate, fabric
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# One project-shape check, called by both validators
|
|
9
|
+
|
|
10
|
+
`pikku workspace validate` and `pikku fabric validate` read like one command
|
|
11
|
+
with a flag. They were two implementations — 495 lines and 2026 — that walked
|
|
12
|
+
the same project and emitted sixteen identical findings. `functions-dir-missing`
|
|
13
|
+
existed character for character in both files. All sixteen already agreed on
|
|
14
|
+
severity, which is the only reason the duplication was invisible.
|
|
15
|
+
|
|
16
|
+
What the duplication cost was not the lines. It was that each validator carried
|
|
17
|
+
checks the other lacked for no reason anyone could name, and nobody noticed
|
|
18
|
+
because nobody read them side by side:
|
|
19
|
+
|
|
20
|
+
- fabric validate never checked that `packages/functions` declares zod v4, or
|
|
21
|
+
that it has a `package.json` at all — while it did check that six other
|
|
22
|
+
dependencies are present.
|
|
23
|
+
- workspace validate never checked the four scaffold flags beyond `console`,
|
|
24
|
+
though the console it requires is useless without `rpc`.
|
|
25
|
+
- fabric validate swallowed a JSON parse error and reported
|
|
26
|
+
`pikku-config-missing` for a file sitting right there.
|
|
27
|
+
|
|
28
|
+
Two of workspace validate's own checks were not merely absent from fabric — they
|
|
29
|
+
had never run anywhere:
|
|
30
|
+
|
|
31
|
+
- the auth checks were gated on a middleware instance with `definitionId ===
|
|
32
|
+
'betterAuthSession'`, but the CLI wires `betterAuthStatelessSession` whenever
|
|
33
|
+
`session.cookieCache` is on, which is the configuration Fabric asks for. So
|
|
34
|
+
they skipped precisely the apps most likely to have auth.
|
|
35
|
+
- they looked for migrations in `packages/functions/db/`, and for tables named
|
|
36
|
+
`app_user` and `auth_verification_token`. `pikku db migrate` reads migrations
|
|
37
|
+
from `<root>/db/<engine>/`, and those two table names appear nowhere else in
|
|
38
|
+
pikku or in any template. Had the gate ever opened, the check would have been
|
|
39
|
+
wrong for every project it fired on.
|
|
40
|
+
|
|
41
|
+
`shared-checks.ts` is now the half that is true of any pikku project regardless
|
|
42
|
+
of where it deploys. Workspace validate is exactly that module; fabric validate
|
|
43
|
+
is that module plus the deploy-shaped checks — themes, frontends, the
|
|
44
|
+
Cloudflare adapter, the `.gitignore` contract, the frontend type-check.
|
|
45
|
+
|
|
46
|
+
Fabric's fix hints won where the two differed: they are multi-line and show the
|
|
47
|
+
config block to paste, and there was no reason workspace users were getting the
|
|
48
|
+
one-line version.
|
|
49
|
+
|
|
50
|
+
**What this rules out:** adding a check to one validator and not the other;
|
|
51
|
+
`pikku fabric validate` reporting a corrupt config as a missing one; asserting
|
|
52
|
+
auth table names no scaffold generates; and reading migrations from anywhere
|
|
53
|
+
other than where `pikku db migrate` reads them.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Scenarios, steps and personas live in files named for them, and validate errors when they do not
|
|
4
|
+
description: pikkuScenario/pikkuFeature/pikkuScenarioStep must be in *.scenario.ts, *.scenarios.ts or *.steps.ts, and definePersonas/runVirtualUser in *.virtual-user.ts or *.vu.ts — an error, because the mixing is only cheap to undo while it is one file
|
|
5
|
+
tags: cli, validate, scenarios, personas
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Scenarios, steps and personas live in files named for them, and validate errors when they do not
|
|
9
|
+
|
|
10
|
+
A `pikkuScenario` declared beside the functions it exercises reads as more of
|
|
11
|
+
the same file. The wiring, the function, and the test of the function arrive in
|
|
12
|
+
one scroll, and the scenario is the part that gets lost — you cannot tell from
|
|
13
|
+
a directory listing which files ship and which files test.
|
|
14
|
+
|
|
15
|
+
So `validate` requires the declaration to be in a file named for what it is:
|
|
16
|
+
|
|
17
|
+
| declaration | file |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `pikkuScenario`, `pikkuFeature` | `*.scenario.ts`, `*.scenarios.ts` |
|
|
20
|
+
| `pikkuScenarioStep` and its platform/addon variants | `*.steps.ts` (or the above) |
|
|
21
|
+
| `definePersonas`, `runVirtualUser` | `*.virtual-user.ts`, `*.vu.ts` |
|
|
22
|
+
|
|
23
|
+
Three suffixes for scenarios rather than one, because the split that matters is
|
|
24
|
+
scenarios apart from application code, not a particular spelling — `.steps.ts`
|
|
25
|
+
holds the steps, `.scenarios.ts` holds the scenarios that call them, and a
|
|
26
|
+
project that already made that split does not have to rename anything.
|
|
27
|
+
|
|
28
|
+
**Error, not warning.** The mixing is cheap to undo while it is one file and
|
|
29
|
+
expensive once it is thirty, and a warning is exactly the signal a project
|
|
30
|
+
learns to scroll past. This is the one place where the scenario checks are
|
|
31
|
+
strict: everything else `validate` says about scenarios (no personas declared,
|
|
32
|
+
no actor sign-in, no environments) is a warning, because those describe an
|
|
33
|
+
under-tested project rather than an unreadable one.
|
|
34
|
+
|
|
35
|
+
**Personas are the virtual-user file** because there is no `defineVirtualUsers`
|
|
36
|
+
to look for. That name was retired in favour of `definePersonas`: a virtual user
|
|
37
|
+
is derived rather than authored — the function meta becomes its catalogue, the
|
|
38
|
+
scenario meta becomes its intents, the declared personas become its identities.
|
|
39
|
+
The persona list is the only part a project writes, so it is the part that has
|
|
40
|
+
to be findable by filename.
|
|
41
|
+
|
|
42
|
+
Matching is on the call (`\bpikkuScenario\s*\(`), not the import, so an aliased
|
|
43
|
+
import cannot slip past and a mention in a comment or a string does not trip it.
|
|
44
|
+
Generated files are exempt — codegen puts things where it likes.
|
|
45
|
+
|
|
46
|
+
**What this rules out:** a scenario in a function file; a step in a wiring file;
|
|
47
|
+
personas in a general-purpose `personas.ts`; and reporting any of these as a
|
|
48
|
+
warning that a project can carry indefinitely.
|