@pikku/core 0.12.51 → 0.12.53

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 (54) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/services/http-scenario-actors.d.ts +67 -0
  3. package/dist/services/http-scenario-actors.js +193 -0
  4. package/dist/services/http-user-flow-actors.d.ts +20 -0
  5. package/dist/services/http-user-flow-actors.js +100 -0
  6. package/dist/services/index.d.ts +5 -2
  7. package/dist/services/index.js +4 -1
  8. package/dist/services/istanbul-coverage-service.d.ts +12 -0
  9. package/dist/services/istanbul-coverage-service.js +41 -0
  10. package/dist/services/meta-service.d.ts +4 -4
  11. package/dist/services/meta-service.js +8 -8
  12. package/dist/services/scenario-actors-service.d.ts +21 -0
  13. package/dist/services/scenario-actors-service.js +1 -0
  14. package/dist/services/stub-tracker.d.ts +43 -0
  15. package/dist/services/stub-tracker.js +146 -0
  16. package/dist/services/user-flow-actors-service.d.ts +11 -1
  17. package/dist/services/v8-coverage-service.d.ts +41 -0
  18. package/dist/services/v8-coverage-service.js +63 -0
  19. package/dist/types/core.types.d.ts +13 -4
  20. package/dist/wirings/actor-flow/actor-flow.types.d.ts +68 -0
  21. package/dist/wirings/actor-flow/actor-flow.types.js +1 -0
  22. package/dist/wirings/actor-flow/index.d.ts +11 -0
  23. package/dist/wirings/actor-flow/index.js +1 -0
  24. package/dist/wirings/actor-flow/run-conversation.d.ts +36 -0
  25. package/dist/wirings/actor-flow/run-conversation.js +181 -0
  26. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +22 -6
  27. package/dist/wirings/workflow/pikku-workflow-service.d.ts +4 -2
  28. package/dist/wirings/workflow/pikku-workflow-service.js +92 -2
  29. package/dist/wirings/workflow/workflow.types.d.ts +7 -7
  30. package/package.json +2 -1
  31. package/src/services/http-scenario-actors-converse.test.ts +222 -0
  32. package/src/services/{http-user-flow-actors.test.ts → http-scenario-actors.test.ts} +17 -7
  33. package/src/services/http-scenario-actors.ts +269 -0
  34. package/src/services/index.ts +27 -8
  35. package/src/services/istanbul-coverage-service.test.ts +66 -0
  36. package/src/services/istanbul-coverage-service.ts +65 -0
  37. package/src/services/meta-service.ts +9 -9
  38. package/src/services/scenario-actors-service.ts +27 -0
  39. package/src/services/stub-tracker.test.ts +104 -0
  40. package/src/services/stub-tracker.ts +185 -0
  41. package/src/services/v8-coverage-service.test.ts +69 -0
  42. package/src/services/v8-coverage-service.ts +121 -0
  43. package/src/types/core.types.ts +13 -4
  44. package/src/wirings/actor-flow/actor-flow.types.ts +73 -0
  45. package/src/wirings/actor-flow/index.ts +22 -0
  46. package/src/wirings/actor-flow/run-conversation.test.ts +176 -0
  47. package/src/wirings/actor-flow/run-conversation.ts +285 -0
  48. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -6
  49. package/src/wirings/workflow/pikku-workflow-service.ts +144 -5
  50. package/src/wirings/workflow/{user-flow-step.test.ts → scenario-step.test.ts} +19 -14
  51. package/src/wirings/workflow/workflow.types.ts +8 -6
  52. package/tsconfig.tsbuildinfo +1 -1
  53. package/src/services/http-user-flow-actors.ts +0 -132
  54. package/src/services/user-flow-actors-service.ts +0 -31
@@ -11,6 +11,7 @@ import {
11
11
  pikkuState,
12
12
  } from '../../pikku-state.js'
13
13
  import { getDurationInMilliseconds } from '../../time-utils.js'
14
+ import { createHttpScenarioActors } from '../../services/http-scenario-actors.js'
14
15
 
15
16
  const resolveWorkflowMeta = (
16
17
  name: string
@@ -59,6 +60,8 @@ import type {
59
60
  WorkflowServiceConfig,
60
61
  WorkflowStepOptions,
61
62
  WorkflowExpectEventuallyOptions,
63
+ WorkflowExpectErrorOptions,
64
+ WorkflowExpectServiceOptions,
62
65
  } from './workflow.types.js'
63
66
  import {
64
67
  continueGraph,
@@ -67,7 +70,7 @@ import {
67
70
  runFromMeta,
68
71
  } from './graph/graph-runner.js'
69
72
  import type { WorkflowService } from '../../services/workflow-service.js'
70
- import type { UserFlowActors } from '../../services/user-flow-actors-service.js'
73
+ import type { ScenarioActors } from '../../services/scenario-actors-service.js'
71
74
  import {
72
75
  PikkuError,
73
76
  addError,
@@ -222,7 +225,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
222
225
  private inlineRuns = new Set<string>()
223
226
  // User-flow actors per run: live authenticated clients (cookie jars) are
224
227
  // process-local by nature, so they ride this map, never the persisted wire.
225
- private runActors = new Map<string, UserFlowActors>()
228
+ private runActors = new Map<string, ScenarioActors>()
226
229
 
227
230
  protected get logger() {
228
231
  return getSingletonServices()?.logger
@@ -1065,6 +1068,39 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1065
1068
  return true
1066
1069
  }
1067
1070
 
1071
+ /** Build HTTP scenario actors for a run started without them; undefined when SCENARIO_ACTOR_SECRET or the API URL is missing */
1072
+ private async resolveScenarioActors(): Promise<ScenarioActors | undefined> {
1073
+ const services = getSingletonServices()
1074
+ const variables = services?.variables
1075
+ const metaService = services?.metaService
1076
+ if (!variables || !metaService) {
1077
+ return undefined
1078
+ }
1079
+ const secret = await variables.get('SCENARIO_ACTOR_SECRET')
1080
+ const apiUrl = await variables.get('API_URL')
1081
+ if (!secret || !apiUrl) {
1082
+ services?.logger?.warn(
1083
+ 'A scenario was started without actors but SCENARIO_ACTOR_SECRET / API_URL is not configured — running without actors.'
1084
+ )
1085
+ return undefined
1086
+ }
1087
+ const actorsConfig = await metaService.getScenarioActorsMeta()
1088
+ if (!actorsConfig || Object.keys(actorsConfig).length === 0) {
1089
+ return undefined
1090
+ }
1091
+ const signInPath =
1092
+ (await variables.get('SCENARIO_SIGN_IN_PATH')) ??
1093
+ '/api/auth/sign-in/actor'
1094
+ const rpcPath = (await variables.get('SCENARIO_RPC_PATH')) ?? '/rpc'
1095
+ return createHttpScenarioActors({
1096
+ apiUrl,
1097
+ secret,
1098
+ actors: actorsConfig,
1099
+ signInPath,
1100
+ rpcPath,
1101
+ })
1102
+ }
1103
+
1068
1104
  /**
1069
1105
  * Start a new workflow run
1070
1106
  * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
@@ -1076,7 +1112,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1076
1112
  input: I,
1077
1113
  wire: WorkflowRunWire,
1078
1114
  rpcService: any,
1079
- options?: { inline?: boolean; startNode?: string; actors?: UserFlowActors }
1115
+ options?: { inline?: boolean; startNode?: string; actors?: ScenarioActors }
1080
1116
  ): Promise<{ runId: string }> {
1081
1117
  // Resolve workflow from static meta (root or addon namespace), then dynamic DB
1082
1118
  const resolved = resolveWorkflowMeta(name)
@@ -1140,8 +1176,13 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1140
1176
  }
1141
1177
  )
1142
1178
 
1143
- if (options?.actors) {
1144
- this.runActors.set(runId, options.actors)
1179
+ const actors =
1180
+ options?.actors ??
1181
+ (workflowMeta.source === 'scenario'
1182
+ ? await this.resolveScenarioActors()
1183
+ : undefined)
1184
+ if (actors) {
1185
+ this.runActors.set(runId, actors)
1145
1186
  }
1146
1187
 
1147
1188
  if (shouldInline) {
@@ -2184,6 +2225,104 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2184
2225
  )
2185
2226
  },
2186
2227
 
2228
+ expectError: async (
2229
+ stepName: string,
2230
+ rpcName: string,
2231
+ data: any,
2232
+ options?: WorkflowExpectErrorOptions
2233
+ ) => {
2234
+ this.verifyStepName(stepName)
2235
+ const resolvedRpcName =
2236
+ addonNamespace && !rpcName.includes(':')
2237
+ ? `${addonNamespace}:${rpcName}`
2238
+ : rpcName
2239
+ return await this.inlineStep(
2240
+ runId,
2241
+ stepName,
2242
+ async () => {
2243
+ let result: any
2244
+ try {
2245
+ result = options?.actor
2246
+ ? await options.actor.invoke(resolvedRpcName, data)
2247
+ : await rpcService.rpcWithWire(resolvedRpcName, data, {})
2248
+ } catch (e: any) {
2249
+ const message = e?.message ?? String(e)
2250
+ if (options?.matches) {
2251
+ const matched =
2252
+ typeof options.matches === 'string'
2253
+ ? message.includes(options.matches)
2254
+ : options.matches.test(message)
2255
+ if (!matched) {
2256
+ throw new Error(
2257
+ `[workflow] expectError '${stepName}' ('${resolvedRpcName}') threw, but the message did not match ${options.matches}: ${message}`
2258
+ )
2259
+ }
2260
+ }
2261
+ return message
2262
+ }
2263
+ throw new Error(
2264
+ `[workflow] expectError '${stepName}' ('${resolvedRpcName}') expected an error but the call succeeded: ${JSON.stringify(result)?.slice(0, 300)}`
2265
+ )
2266
+ },
2267
+ options
2268
+ )
2269
+ },
2270
+
2271
+ expectService: async (
2272
+ stepName: string,
2273
+ serviceMethod: string,
2274
+ options?: WorkflowExpectServiceOptions
2275
+ ) => {
2276
+ this.verifyStepName(stepName)
2277
+ const [service, method] = serviceMethod.split('.')
2278
+ if (!service || !method) {
2279
+ throw new Error(
2280
+ `[workflow] expectService '${stepName}' needs 'service.method', got '${serviceMethod}'`
2281
+ )
2282
+ }
2283
+ await this.inlineStep(
2284
+ runId,
2285
+ stepName,
2286
+ async () => {
2287
+ const rpcName = 'console:getStubCalls'
2288
+ const calls: Array<{
2289
+ service: string
2290
+ method: string
2291
+ args: unknown[]
2292
+ }> = options?.actor
2293
+ ? await options.actor.invoke(rpcName, { service })
2294
+ : await rpcService.rpcWithWire(rpcName, { service }, {})
2295
+ const matching = (calls ?? []).filter(
2296
+ (c) =>
2297
+ c.service === service &&
2298
+ c.method === method &&
2299
+ (options?.calledWith === undefined ||
2300
+ JSON.stringify(c.args?.[0]) ===
2301
+ JSON.stringify(options.calledWith))
2302
+ )
2303
+ const expected = options?.times
2304
+ const ok =
2305
+ expected === undefined
2306
+ ? matching.length > 0
2307
+ : matching.length === expected
2308
+ if (!ok) {
2309
+ const seen =
2310
+ (calls ?? [])
2311
+ .map(
2312
+ (c) =>
2313
+ `${c.service}.${c.method}(${JSON.stringify(c.args?.[0])?.slice(0, 120) ?? ''})`
2314
+ )
2315
+ .join('\n ') || '(none)'
2316
+ throw new Error(
2317
+ `[workflow] expectService '${stepName}' expected ${expected ?? 'at least one'} call(s) to '${serviceMethod}'` +
2318
+ `${options?.calledWith !== undefined ? ` with ${JSON.stringify(options.calledWith)}` : ''}, found ${matching.length}. Recorded:\n ${seen}`
2319
+ )
2320
+ }
2321
+ },
2322
+ options
2323
+ )
2324
+ },
2325
+
2187
2326
  // Implement workflow.sleep()
2188
2327
  sleep: async (stepName: string, duration: string | number) => {
2189
2328
  this.verifyStepName(stepName)
@@ -3,14 +3,14 @@ import assert from 'node:assert/strict'
3
3
 
4
4
  import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
5
5
  import { pikkuState, resetPikkuState } from '../../pikku-state.js'
6
- import type { UserFlowActor } from '../../services/user-flow-actors-service.js'
6
+ import type { ScenarioActor } from '../../services/scenario-actors-service.js'
7
7
 
8
8
  const noopLogger = { error() {}, info() {}, warn() {}, debug() {} }
9
9
 
10
10
  const fakeActor = (
11
11
  name: string,
12
12
  handler: (rpcName: string, data: unknown) => Promise<unknown>
13
- ): UserFlowActor & { calls: Array<{ rpcName: string; data: unknown }> } => {
13
+ ): ScenarioActor & { calls: Array<{ rpcName: string; data: unknown }> } => {
14
14
  const calls: Array<{ rpcName: string; data: unknown }> = []
15
15
  return {
16
16
  name,
@@ -31,14 +31,14 @@ const setup = async (
31
31
  logger: noopLogger,
32
32
  ...services,
33
33
  } as any)
34
- const runId = await ws.createRun('userFlowTest', {}, true, 'hash', {
34
+ const runId = await ws.createRun('scenarioTest', {}, true, 'hash', {
35
35
  type: 'test',
36
36
  } as any)
37
37
  ws.registerInlineRun(runId)
38
38
  return runId
39
39
  }
40
40
 
41
- describe('user-flow actor steps (workflow.do with `actor`)', () => {
41
+ describe('scenario actor steps (workflow.do with `actor`)', () => {
42
42
  beforeEach(() => resetPikkuState())
43
43
 
44
44
  test('routes through the actor over the real transport, never internal rpc', async () => {
@@ -54,7 +54,7 @@ describe('user-flow actor steps (workflow.do with `actor`)', () => {
54
54
  },
55
55
  }
56
56
 
57
- const wire = ws.createWorkflowWire('userFlowTest', runId, rpc)
57
+ const wire = ws.createWorkflowWire('scenarioTest', runId, rpc)
58
58
  const result = await wire.do(
59
59
  'customer creates todo',
60
60
  'createTodo',
@@ -74,7 +74,7 @@ describe('user-flow actor steps (workflow.do with `actor`)', () => {
74
74
  let invocations = 0
75
75
  const yasser = fakeActor('yasser', async () => ({ n: ++invocations }))
76
76
  const runId = await setup(ws)
77
- const wire = ws.createWorkflowWire('userFlowTest', runId, {})
77
+ const wire = ws.createWorkflowWire('scenarioTest', runId, {})
78
78
 
79
79
  const first = await wire.do('step', 'someRpc', {}, { actor: yasser })
80
80
  assert.deepEqual(first, { n: 1 })
@@ -82,8 +82,13 @@ describe('user-flow actor steps (workflow.do with `actor`)', () => {
82
82
  // Simulate replay: reset per-run ordinals so the same logical step name
83
83
  // resolves to the same durable step key.
84
84
  ;(ws as any).resetStepOrdinals(runId)
85
- const replayWire = ws.createWorkflowWire('userFlowTest', runId, {})
86
- const replayed = await replayWire.do('step', 'someRpc', {}, { actor: yasser })
85
+ const replayWire = ws.createWorkflowWire('scenarioTest', runId, {})
86
+ const replayed = await replayWire.do(
87
+ 'step',
88
+ 'someRpc',
89
+ {},
90
+ { actor: yasser }
91
+ )
87
92
 
88
93
  assert.deepEqual(replayed, { n: 1 }, 'replay must return the cached result')
89
94
  assert.equal(invocations, 1, 'the actor must not be re-invoked on replay')
@@ -106,7 +111,7 @@ describe('user-flow actor steps (workflow.do with `actor`)', () => {
106
111
  workflowQueued: true,
107
112
  } as any
108
113
 
109
- const wire = ws.createWorkflowWire('userFlowTest', runId, {})
114
+ const wire = ws.createWorkflowWire('scenarioTest', runId, {})
110
115
  await wire.do('step', 'queuedRpc', {}, { actor: customer })
111
116
 
112
117
  assert.equal(queued, 0, 'actor steps are outbound HTTP — never queued')
@@ -116,10 +121,10 @@ describe('user-flow actor steps (workflow.do with `actor`)', () => {
116
121
  test('actor step failure surfaces the actor error and fails after retries', async () => {
117
122
  const ws = new InMemoryWorkflowService()
118
123
  const broken = fakeActor('broken', async () => {
119
- throw new Error("[user-flow] 'createTodo' as 'broken' returned 403: nope")
124
+ throw new Error("[scenario] 'createTodo' as 'broken' returned 403: nope")
120
125
  })
121
126
  const runId = await setup(ws)
122
- const wire = ws.createWorkflowWire('userFlowTest', runId, {})
127
+ const wire = ws.createWorkflowWire('scenarioTest', runId, {})
123
128
 
124
129
  await assert.rejects(
125
130
  wire.do('step', 'createTodo', {}, { actor: broken, retries: 2 }),
@@ -139,7 +144,7 @@ describe('workflow.expectEventually', () => {
139
144
  notifications: ++polls >= 3 ? ['ping'] : [],
140
145
  }))
141
146
  const runId = await setup(ws)
142
- const wire = ws.createWorkflowWire('userFlowTest', runId, {})
147
+ const wire = ws.createWorkflowWire('scenarioTest', runId, {})
143
148
 
144
149
  const result = await wire.expectEventually(
145
150
  'sarah sees the notification',
@@ -157,7 +162,7 @@ describe('workflow.expectEventually', () => {
157
162
  const ws = new InMemoryWorkflowService()
158
163
  const sarah = fakeActor('sarah', async () => ({ notifications: [] }))
159
164
  const runId = await setup(ws)
160
- const wire = ws.createWorkflowWire('userFlowTest', runId, {})
165
+ const wire = ws.createWorkflowWire('scenarioTest', runId, {})
161
166
 
162
167
  await assert.rejects(
163
168
  wire.expectEventually(
@@ -178,7 +183,7 @@ describe('workflow.expectEventually', () => {
178
183
  const rpc = {
179
184
  rpcWithWire: async () => ({ ready: ++polls >= 2 }),
180
185
  }
181
- const wire = ws.createWorkflowWire('userFlowTest', runId, rpc)
186
+ const wire = ws.createWorkflowWire('scenarioTest', runId, rpc)
182
187
 
183
188
  const result = await wire.expectEventually(
184
189
  'job finishes',
@@ -8,6 +8,8 @@ export type { WorkflowService } from '../../services/workflow-service.js'
8
8
  export type {
9
9
  WorkflowStepOptions,
10
10
  WorkflowExpectEventuallyOptions,
11
+ WorkflowExpectErrorOptions,
12
+ WorkflowExpectServiceOptions,
11
13
  WorkflowWireDoRPC,
12
14
  WorkflowWireDoInline,
13
15
  WorkflowWireSleep,
@@ -325,9 +327,9 @@ export type WorkflowsMeta = Record<
325
327
  context?: WorkflowContext
326
328
  dsl?: boolean
327
329
  expose?: boolean
328
- /** True for pikkuUserFlow workflows (complex + actor steps). */
329
- userFlow?: boolean
330
- /** Actor names a user flow declares (personas it runs steps as). */
330
+ /** True for pikkuScenario workflows (complex + actor steps). */
331
+ scenario?: boolean
332
+ /** Actor names a scenario declares (personas it runs steps as). */
331
333
  actors?: string[]
332
334
  }
333
335
  >
@@ -342,13 +344,13 @@ export interface WorkflowRuntimeMeta {
342
344
  name: string
343
345
  /** Pikku function name (for execution) */
344
346
  pikkuFuncId: string
345
- /** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph', 'user-flow' (complex + actor steps) */
346
- source: 'dsl' | 'complex' | 'graph' | 'dynamic-workflow' | 'user-flow'
347
+ /** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph', 'scenario' (complex + actor steps) */
348
+ source: 'dsl' | 'complex' | 'graph' | 'dynamic-workflow' | 'scenario'
347
349
  /** Optional description */
348
350
  description?: string
349
351
  /** Tags for organization */
350
352
  tags?: string[]
351
- /** Actor names a user flow declares (personas it runs steps as). */
353
+ /** Actor names a scenario declares (personas it runs steps as). */
352
354
  actors?: string[]
353
355
  /** Serialized nodes */
354
356
  nodes?: Record<string, any>