@pikku/core 0.12.52 → 0.12.54
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 +37 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/istanbul-coverage-service.d.ts +12 -0
- package/dist/services/istanbul-coverage-service.js +41 -0
- package/dist/services/scenario-actors-service.d.ts +3 -21
- package/dist/services/stub-tracker.d.ts +43 -0
- package/dist/services/stub-tracker.js +146 -0
- package/dist/services/v8-coverage-service.d.ts +79 -0
- package/dist/services/v8-coverage-service.js +63 -0
- package/dist/types/core.types.d.ts +9 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +16 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +0 -6
- package/dist/wirings/workflow/pikku-workflow-service.js +56 -6
- package/dist/wirings/workflow/workflow.types.d.ts +1 -1
- package/package.json +3 -1
- package/src/services/index.ts +21 -0
- package/src/services/istanbul-coverage-service.test.ts +66 -0
- package/src/services/istanbul-coverage-service.ts +65 -0
- package/src/services/scenario-actors-service.ts +3 -21
- package/src/services/stub-tracker.test.ts +104 -0
- package/src/services/stub-tracker.ts +185 -0
- package/src/services/v8-coverage-service.test.ts +69 -0
- package/src/services/v8-coverage-service.ts +165 -0
- package/src/types/core.types.ts +9 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +29 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +100 -6
- package/src/wirings/workflow/workflow.types.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// node:inspector is imported lazily inside start() so this module stays
|
|
2
|
+
// loadable on runtimes without it (e.g. Cloudflare Workers).
|
|
3
|
+
|
|
4
|
+
export interface CoverageRange {
|
|
5
|
+
startOffset: number
|
|
6
|
+
endOffset: number
|
|
7
|
+
count: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FunctionCoverage {
|
|
11
|
+
functionName: string
|
|
12
|
+
isBlockCoverage: boolean
|
|
13
|
+
ranges: CoverageRange[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ScriptCoverage {
|
|
17
|
+
scriptId: string
|
|
18
|
+
url: string
|
|
19
|
+
functions: FunctionCoverage[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type LineHits = Map<string, Map<number, number>>
|
|
23
|
+
|
|
24
|
+
export type CoverageSnapshot =
|
|
25
|
+
| {
|
|
26
|
+
kind: 'v8-scripts'
|
|
27
|
+
scripts: ScriptCoverage[]
|
|
28
|
+
getScriptSource: (scriptId: string) => Promise<string>
|
|
29
|
+
}
|
|
30
|
+
| { kind: 'line-hits'; lineHits: LineHits }
|
|
31
|
+
|
|
32
|
+
export type CoverageStatus = 'covered' | 'partial' | 'uncovered' | 'unknown'
|
|
33
|
+
|
|
34
|
+
export interface FunctionCoverageEntry {
|
|
35
|
+
name: string
|
|
36
|
+
sourceFile: string
|
|
37
|
+
exposed: boolean
|
|
38
|
+
description: string | null
|
|
39
|
+
coveredLines: number
|
|
40
|
+
totalLines: number
|
|
41
|
+
missedLines: number[]
|
|
42
|
+
ratio: number
|
|
43
|
+
status: CoverageStatus
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FunctionCoverageReport {
|
|
47
|
+
generatedAt: string
|
|
48
|
+
summary: {
|
|
49
|
+
total: number
|
|
50
|
+
covered: number
|
|
51
|
+
partial: number
|
|
52
|
+
uncovered: number
|
|
53
|
+
unknown: number
|
|
54
|
+
overallRatio: number
|
|
55
|
+
}
|
|
56
|
+
functions: FunctionCoverageEntry[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CoverageFunctionMeta {
|
|
60
|
+
name: string
|
|
61
|
+
sourceFile: string
|
|
62
|
+
bodySourceFile?: string
|
|
63
|
+
exportedName?: string
|
|
64
|
+
expose?: boolean
|
|
65
|
+
description?: string | null
|
|
66
|
+
bodyStart?: number
|
|
67
|
+
bodyEnd?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CoverageService {
|
|
71
|
+
start(): Promise<void>
|
|
72
|
+
takeCoverage(): Promise<CoverageSnapshot>
|
|
73
|
+
/** Maps the current snapshot onto function body spans. Provided by the
|
|
74
|
+
* process that booted the coverage backend (the pikku CLI) — absent when
|
|
75
|
+
* the runtime only exposes raw snapshots. */
|
|
76
|
+
takeReport?(
|
|
77
|
+
functionsMeta: Record<string, CoverageFunctionMeta>
|
|
78
|
+
): Promise<FunctionCoverageReport>
|
|
79
|
+
reset(): Promise<void>
|
|
80
|
+
stop(): Promise<void>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type InspectorSession = {
|
|
84
|
+
connect(): void
|
|
85
|
+
disconnect(): void
|
|
86
|
+
post(
|
|
87
|
+
method: string,
|
|
88
|
+
params?: unknown,
|
|
89
|
+
callback?: (err: Error | null, result?: any) => void
|
|
90
|
+
): void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class V8CoverageService implements CoverageService {
|
|
94
|
+
private session: InspectorSession | null = null
|
|
95
|
+
private startPromise: Promise<void> | null = null
|
|
96
|
+
|
|
97
|
+
private post<T>(method: string, params?: unknown): Promise<T> {
|
|
98
|
+
const session = this.session
|
|
99
|
+
if (!session) {
|
|
100
|
+
throw new Error('V8CoverageService not started — call start() first')
|
|
101
|
+
}
|
|
102
|
+
return new Promise<T>((resolve, reject) => {
|
|
103
|
+
session.post(method, params, (err, result) =>
|
|
104
|
+
err ? reject(err) : resolve(result as T)
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
start(): Promise<void> {
|
|
110
|
+
this.startPromise ??= this.doStart()
|
|
111
|
+
return this.startPromise
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async doStart(): Promise<void> {
|
|
115
|
+
const inspector = await import('node:inspector')
|
|
116
|
+
this.session = new inspector.Session() as unknown as InspectorSession
|
|
117
|
+
this.session.connect()
|
|
118
|
+
await this.post('Profiler.enable')
|
|
119
|
+
await this.post('Debugger.enable')
|
|
120
|
+
await this.post('Profiler.startPreciseCoverage', {
|
|
121
|
+
callCount: true,
|
|
122
|
+
detailed: true,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async takeCoverage(): Promise<CoverageSnapshot> {
|
|
127
|
+
const { result } = await this.post<{ result: ScriptCoverage[] }>(
|
|
128
|
+
'Profiler.takePreciseCoverage'
|
|
129
|
+
)
|
|
130
|
+
return {
|
|
131
|
+
kind: 'v8-scripts',
|
|
132
|
+
scripts: result.filter((script) => script.url.startsWith('file://')),
|
|
133
|
+
getScriptSource: (scriptId) => this.getScriptSource(scriptId),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getScriptSource(scriptId: string): Promise<string> {
|
|
138
|
+
const { scriptSource } = await this.post<{ scriptSource: string }>(
|
|
139
|
+
'Debugger.getScriptSource',
|
|
140
|
+
{ scriptId }
|
|
141
|
+
)
|
|
142
|
+
return scriptSource
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async reset(): Promise<void> {
|
|
146
|
+
await this.post('Profiler.stopPreciseCoverage')
|
|
147
|
+
await this.post('Profiler.startPreciseCoverage', {
|
|
148
|
+
callCount: true,
|
|
149
|
+
detailed: true,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async stop(): Promise<void> {
|
|
154
|
+
if (!this.session) return
|
|
155
|
+
try {
|
|
156
|
+
await this.post('Profiler.stopPreciseCoverage')
|
|
157
|
+
await this.post('Profiler.disable')
|
|
158
|
+
await this.post('Debugger.disable')
|
|
159
|
+
} finally {
|
|
160
|
+
this.session.disconnect()
|
|
161
|
+
this.session = null
|
|
162
|
+
this.startPromise = null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/types/core.types.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
|
|
|
38
38
|
import type { CredentialService } from '../services/credential-service.js'
|
|
39
39
|
import type { EmailService } from '../services/email-service.js'
|
|
40
40
|
import type { MetaService } from '../services/meta-service.js'
|
|
41
|
+
import type { CoverageService } from '../services/v8-coverage-service.js'
|
|
41
42
|
import type { SessionStore } from '../services/session-store.js'
|
|
42
43
|
import type {
|
|
43
44
|
AuditDurability,
|
|
@@ -150,6 +151,12 @@ export type FunctionMeta = FunctionRuntimeMeta &
|
|
|
150
151
|
isDirectFunction: boolean
|
|
151
152
|
sourceFile: string
|
|
152
153
|
exportedName: string
|
|
154
|
+
/** File containing the handler body when it differs from sourceFile (imported handlers) */
|
|
155
|
+
bodySourceFile?: string
|
|
156
|
+
/** 1-indexed first line of the handler body (verbose meta; coverage mapping) */
|
|
157
|
+
bodyStart: number
|
|
158
|
+
/** 1-indexed last line of the handler body (verbose meta; coverage mapping) */
|
|
159
|
+
bodyEnd: number
|
|
153
160
|
} & CommonWireMeta
|
|
154
161
|
>
|
|
155
162
|
|
|
@@ -293,6 +300,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
293
300
|
emailService?: EmailService
|
|
294
301
|
/** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
|
|
295
302
|
metaService?: MetaService
|
|
303
|
+
/** V8 precise-coverage collector (`pikku dev --coverage` only) */
|
|
304
|
+
coverageService?: CoverageService
|
|
296
305
|
/** Audit service for durable or staged audit event capture */
|
|
297
306
|
audit?: AuditService
|
|
298
307
|
/**
|
|
@@ -37,6 +37,20 @@ export interface WorkflowExpectEventuallyOptions extends WorkflowStepOptions {
|
|
|
37
37
|
interval?: string | number
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Options for workflow.expectError() */
|
|
41
|
+
export interface WorkflowExpectErrorOptions extends WorkflowStepOptions {
|
|
42
|
+
/** Assert the error message matches (string = substring match). */
|
|
43
|
+
matches?: string | RegExp
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Options for workflow.expectService() */
|
|
47
|
+
export interface WorkflowExpectServiceOptions extends WorkflowStepOptions {
|
|
48
|
+
/** Assert a recorded call's first argument deep-equals this value. */
|
|
49
|
+
calledWith?: unknown
|
|
50
|
+
/** Assert the exact number of matching calls. Default: at least one. */
|
|
51
|
+
times?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* Type signature for workflow.do() RPC form - used by inspector
|
|
42
56
|
*/
|
|
@@ -381,6 +395,21 @@ export interface PikkuWorkflowWire {
|
|
|
381
395
|
options?: WorkflowExpectEventuallyOptions
|
|
382
396
|
) => Promise<TOutput>
|
|
383
397
|
|
|
398
|
+
/** Error-path step (scenarios): succeeds only when the RPC throws; returns the message */
|
|
399
|
+
expectError: <TInput = any>(
|
|
400
|
+
stepName: string,
|
|
401
|
+
rpcName: string,
|
|
402
|
+
data: TInput,
|
|
403
|
+
options?: WorkflowExpectErrorOptions
|
|
404
|
+
) => Promise<string>
|
|
405
|
+
|
|
406
|
+
/** Stub-assertion step (scenarios): asserts `service.method` was called on the target server */
|
|
407
|
+
expectService: (
|
|
408
|
+
stepName: string,
|
|
409
|
+
serviceMethod: string,
|
|
410
|
+
options?: WorkflowExpectServiceOptions
|
|
411
|
+
) => Promise<void>
|
|
412
|
+
|
|
384
413
|
/** Sleep for a duration */
|
|
385
414
|
sleep: WorkflowWireSleep
|
|
386
415
|
|
|
@@ -60,6 +60,8 @@ import type {
|
|
|
60
60
|
WorkflowServiceConfig,
|
|
61
61
|
WorkflowStepOptions,
|
|
62
62
|
WorkflowExpectEventuallyOptions,
|
|
63
|
+
WorkflowExpectErrorOptions,
|
|
64
|
+
WorkflowExpectServiceOptions,
|
|
63
65
|
} from './workflow.types.js'
|
|
64
66
|
import {
|
|
65
67
|
continueGraph,
|
|
@@ -1099,12 +1101,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1099
1101
|
})
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1102
|
-
/**
|
|
1103
|
-
* Start a new workflow run
|
|
1104
|
-
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
1105
|
-
* @param options.inline - If true, execute workflow directly without queue service
|
|
1106
|
-
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
1107
|
-
*/
|
|
1108
1104
|
/**
|
|
1109
1105
|
* Start a new workflow run
|
|
1110
1106
|
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
@@ -2229,6 +2225,104 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2229
2225
|
)
|
|
2230
2226
|
},
|
|
2231
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 = 'pikkuScenarioGetStubCalls'
|
|
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
|
+
|
|
2232
2326
|
// Implement workflow.sleep()
|
|
2233
2327
|
sleep: async (stepName: string, duration: string | number) => {
|
|
2234
2328
|
this.verifyStepName(stepName)
|
|
@@ -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,
|