@pikku/core 0.12.150 → 0.12.151

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 (139) hide show
  1. package/dist/function/function-runner.js +23 -0
  2. package/dist/function/trace-queue-service.d.ts +26 -0
  3. package/dist/function/trace-queue-service.js +48 -0
  4. package/dist/mesh.d.ts +2 -2
  5. package/dist/mesh.js +3 -3
  6. package/dist/middleware/index.d.ts +1 -1
  7. package/dist/middleware/index.js +1 -1
  8. package/dist/middleware/mesh-auth.d.ts +3 -2
  9. package/dist/middleware/mesh-auth.js +7 -2
  10. package/dist/middleware/remote-jobs-secret.d.ts +1 -1
  11. package/dist/middleware/require-origin.d.ts +19 -0
  12. package/dist/middleware/require-origin.js +36 -2
  13. package/dist/services/deployment-service.d.ts +1 -1
  14. package/dist/services/incoming-webhook-service.d.ts +4 -2
  15. package/dist/services/incoming-webhook-service.js +4 -3
  16. package/dist/services/queue-webhook-service.js +7 -1
  17. package/dist/services/remote-jobs.d.ts +6 -1
  18. package/dist/services/scheduler-service.d.ts +3 -1
  19. package/dist/services/webhook-service.d.ts +2 -0
  20. package/dist/services/workflow-service.d.ts +4 -1
  21. package/dist/testing/index.d.ts +4 -0
  22. package/dist/testing/index.js +2 -0
  23. package/dist/testing/queue-identity-transport-tests.d.ts +18 -0
  24. package/dist/testing/queue-identity-transport-tests.js +162 -0
  25. package/dist/testing/queue-trace-conformance.d.ts +26 -0
  26. package/dist/testing/queue-trace-conformance.js +259 -0
  27. package/dist/testing/service-tests/channel-store-tests.js +11 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.d.ts +18 -1
  29. package/dist/testing/service-tests/queued-workflow-harness.js +19 -3
  30. package/dist/testing/service-tests/workflow-service-tests.js +12 -0
  31. package/dist/testing/trace-matrix/assertions.d.ts +3 -0
  32. package/dist/testing/trace-matrix/assertions.js +190 -0
  33. package/dist/testing/trace-matrix/chain-harness.d.ts +119 -0
  34. package/dist/testing/trace-matrix/chain-harness.js +777 -0
  35. package/dist/testing/trace-matrix/dimensions.d.ts +55 -0
  36. package/dist/testing/trace-matrix/dimensions.js +305 -0
  37. package/dist/testing/trace-matrix/pairwise.d.ts +20 -0
  38. package/dist/testing/trace-matrix/pairwise.js +75 -0
  39. package/dist/testing/trace-matrix/report.d.ts +1 -0
  40. package/dist/testing/trace-matrix/report.js +16 -0
  41. package/dist/testing/trace-matrix/transports.d.ts +19 -0
  42. package/dist/testing/trace-matrix/transports.js +27 -0
  43. package/dist/testing/trace-matrix/workflow-cases.d.ts +24 -0
  44. package/dist/testing/trace-matrix/workflow-cases.js +147 -0
  45. package/dist/trace.d.ts +66 -0
  46. package/dist/trace.js +97 -0
  47. package/dist/types/core.types.d.ts +6 -2
  48. package/dist/wirings/addon/addon-reach.d.ts +3 -0
  49. package/dist/wirings/addon/addon-reach.js +9 -0
  50. package/dist/wirings/addon/define-plugin.d.ts +6 -0
  51. package/dist/wirings/addon/wire-addon.js +5 -0
  52. package/dist/wirings/addon/wire-remote-addon.d.ts +11 -0
  53. package/dist/wirings/agent/agent-finalize.d.ts +2 -0
  54. package/dist/wirings/agent/agent-finalize.js +1 -1
  55. package/dist/wirings/agent/agent-prepare.d.ts +3 -0
  56. package/dist/wirings/agent/agent-prepare.js +24 -8
  57. package/dist/wirings/agent/agent-rpc.js +1 -0
  58. package/dist/wirings/agent/agent-runner.js +3 -0
  59. package/dist/wirings/agent/agent-stream.js +4 -0
  60. package/dist/wirings/agent/voice-input.d.ts +1 -1
  61. package/dist/wirings/agent/voice-output.d.ts +1 -1
  62. package/dist/wirings/agent-scorer/agent-scorer-live.d.ts +4 -2
  63. package/dist/wirings/agent-scorer/agent-scorer-live.js +2 -2
  64. package/dist/wirings/channel/channel-common.d.ts +4 -1
  65. package/dist/wirings/channel/channel-common.js +3 -1
  66. package/dist/wirings/channel/channel-handler.js +2 -0
  67. package/dist/wirings/channel/channel-host-rpc.d.ts +1 -1
  68. package/dist/wirings/channel/channel-host-rpc.js +2 -2
  69. package/dist/wirings/channel/channel-rpc-responder.d.ts +7 -1
  70. package/dist/wirings/channel/channel-rpc-responder.js +2 -2
  71. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  72. package/dist/wirings/channel/channel-rpc-service.js +2 -2
  73. package/dist/wirings/channel/channel-rpc.types.d.ts +5 -2
  74. package/dist/wirings/channel/channel-rpc.types.js +1 -1
  75. package/dist/wirings/channel/channel-store.d.ts +4 -2
  76. package/dist/wirings/channel/channel-store.js +2 -2
  77. package/dist/wirings/channel/channel.types.d.ts +3 -0
  78. package/dist/wirings/channel/local/local-channel-runner.js +16 -5
  79. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +3 -1
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +27 -4
  82. package/dist/wirings/gateway/gateway-runner.js +3 -1
  83. package/dist/wirings/http/http-runner.d.ts +4 -4
  84. package/dist/wirings/http/http-runner.js +14 -18
  85. package/dist/wirings/http/http.types.d.ts +3 -3
  86. package/dist/wirings/mcp/mcp-runner.js +15 -5
  87. package/dist/wirings/queue/in-memory-queue-service.d.ts +1 -1
  88. package/dist/wirings/queue/in-memory-queue-service.js +8 -2
  89. package/dist/wirings/queue/index.d.ts +1 -0
  90. package/dist/wirings/queue/index.js +1 -0
  91. package/dist/wirings/queue/queue-runner.d.ts +3 -4
  92. package/dist/wirings/queue/queue-runner.js +8 -7
  93. package/dist/wirings/queue/queue-trace.d.ts +18 -0
  94. package/dist/wirings/queue/queue-trace.js +42 -0
  95. package/dist/wirings/queue/queue.types.d.ts +7 -0
  96. package/dist/wirings/queue/remote-queue-job.d.ts +1 -1
  97. package/dist/wirings/queue/remote-queue-job.js +5 -3
  98. package/dist/wirings/rpc/rpc-runner.d.ts +7 -1
  99. package/dist/wirings/rpc/rpc-runner.js +43 -21
  100. package/dist/wirings/rpc/rpc-types.d.ts +2 -0
  101. package/dist/wirings/scheduler/remote-scheduled-job.d.ts +1 -1
  102. package/dist/wirings/scheduler/remote-scheduled-job.js +2 -2
  103. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -4
  104. package/dist/wirings/scheduler/scheduler-runner.js +7 -6
  105. package/dist/wirings/trigger/pikku-trigger-service.js +7 -4
  106. package/dist/wirings/trigger/webhook-source-runner.d.ts +7 -1
  107. package/dist/wirings/trigger/webhook-source-runner.js +11 -3
  108. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  109. package/dist/wirings/workflow/graph/graph-runner.js +49 -30
  110. package/dist/wirings/workflow/index.d.ts +3 -1
  111. package/dist/wirings/workflow/index.js +1 -0
  112. package/dist/wirings/workflow/pikku-scenario-service.js +2 -0
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +37 -40
  114. package/dist/wirings/workflow/pikku-workflow-service.js +122 -133
  115. package/dist/wirings/workflow/workflow-child-step.d.ts +2 -2
  116. package/dist/wirings/workflow/workflow-child-step.js +11 -4
  117. package/dist/wirings/workflow/workflow-compensation.d.ts +6 -0
  118. package/dist/wirings/workflow/workflow-compensation.js +15 -1
  119. package/dist/wirings/workflow/workflow-constants.d.ts +13 -0
  120. package/dist/wirings/workflow/workflow-constants.js +13 -0
  121. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +2 -0
  122. package/dist/wirings/workflow/workflow-dsl-pass.js +27 -11
  123. package/dist/wirings/workflow/workflow-recovery.d.ts +131 -3
  124. package/dist/wirings/workflow/workflow-recovery.js +233 -7
  125. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +1 -0
  126. package/dist/wirings/workflow/workflow-run-mirror.d.ts +4 -0
  127. package/dist/wirings/workflow/workflow-run-mirror.js +16 -0
  128. package/dist/wirings/workflow/workflow-run-status.d.ts +2 -0
  129. package/dist/wirings/workflow/workflow-run-status.js +13 -0
  130. package/dist/wirings/workflow/workflow-run-trace.d.ts +22 -0
  131. package/dist/wirings/workflow/workflow-run-trace.js +24 -0
  132. package/dist/wirings/workflow/workflow-step-claim.d.ts +18 -0
  133. package/dist/wirings/workflow/workflow-step-claim.js +47 -0
  134. package/dist/wirings/workflow/workflow-step-retry.d.ts +6 -2
  135. package/dist/wirings/workflow/workflow-step-retry.js +8 -3
  136. package/dist/wirings/workflow/workflow-wire.d.ts +12 -0
  137. package/dist/wirings/workflow/workflow-wire.js +33 -0
  138. package/dist/wirings/workflow/workflow.types.d.ts +16 -0
  139. package/package.json +2 -1
@@ -0,0 +1,55 @@
1
+ import { type Combo } from './pairwise.js';
2
+ export declare const STARTS: readonly ['http', 'channel', 'mcp', 'cli', 'trigger', 'gateway', 'queue-job', 'schedule', 'agent-tool'];
3
+ export declare const HOPS: readonly ['rpc', 'mesh', 'remote-addon', 'enqueue', 'schedule', 'start-workflow', 'child-workflow'];
4
+ export declare const ADAPTERS: readonly ['in-memory', 'bullmq', 'pg-boss', 'nats'];
5
+ export declare const WORKFLOWS: readonly ['dsl-queued', 'dsl-inline', 'graph-queued', 'graph-inline'];
6
+ export declare const STEPS: readonly ['rpc', 'sleep', 'suspend', 'approval', 'fan-out', 'retry', 'compensation'];
7
+ export declare const INBOUNDS: readonly ['none', 'valid', 'invalid', 'forged'];
8
+ export declare const OUTCOMES: readonly ['success', 'throw-pikku-error', 'throw-plain-error', 'schema-invalid', 'auth-mesh-jwt', 'auth-remote-addon', 'permission-denied', 'remote-4xx', 'remote-5xx', 'remote-network', 'remote-timeout', 'job-exhausted', 'job-discarded', 'job-poison', 'abandoned', 'step-compensated', 'workflow-cancelled', 'approval-rejected', 'fan-out-sibling-failure'];
9
+ export declare const TIMINGS: readonly ['immediate', 'delayed-job', 'sleep-wakeup', 'slow-overlap', 'retry-backoff', 'lease-expiry-redelivery', 'duplicate-delivery', 'concurrent-starts', 'enqueue-before-fail', 'job-after-parent'];
10
+ export type Start = (typeof STARTS)[number];
11
+ export type Hop = (typeof HOPS)[number];
12
+ export type Adapter = (typeof ADAPTERS)[number];
13
+ export type Workflow = (typeof WORKFLOWS)[number];
14
+ export type Step = (typeof STEPS)[number];
15
+ export type Inbound = (typeof INBOUNDS)[number];
16
+ export type Outcome = (typeof OUTCOMES)[number];
17
+ export type Timing = (typeof TIMINGS)[number];
18
+ /**
19
+ * The combinations that cannot happen, each with why. Every matrix goes through
20
+ * this list and reports what it dropped, so nothing is filtered silently. Rules
21
+ * see partial combinations and must stay undecided (false) on a missing value.
22
+ */
23
+ export declare const EXCLUSIONS: ReadonlyArray<{
24
+ id: string;
25
+ why: string;
26
+ applies: (combo: Combo) => boolean;
27
+ }>;
28
+ /** The first rule that excludes this combination, if any. */
29
+ export declare const exclusionOf: (combo: Combo) => string | undefined;
30
+ export type MatrixReport = {
31
+ name: string;
32
+ generated: number;
33
+ included: number;
34
+ excluded: Record<string, number>;
35
+ };
36
+ /** Start × hop × outcome. */
37
+ export declare const chainMatrix: () => {
38
+ combos: Combo[];
39
+ report: MatrixReport;
40
+ };
41
+ /** Adapter × workflow × step × outcome. */
42
+ export declare const workflowMatrix: () => {
43
+ combos: Combo[];
44
+ report: MatrixReport;
45
+ };
46
+ /**
47
+ * Every value pair across all eight dimensions. `workflow` and `step` gain the
48
+ * value `none` for a chain with no workflow in it.
49
+ */
50
+ export declare const pairwiseMatrix: () => {
51
+ combos: Combo[];
52
+ report: MatrixReport;
53
+ impossible: string[];
54
+ rulesUsed: Set<string>;
55
+ };
@@ -0,0 +1,305 @@
1
+ import { crossProduct, pairwiseCombos } from './pairwise.js';
2
+ export const STARTS = [
3
+ 'http',
4
+ 'channel',
5
+ 'mcp',
6
+ 'cli',
7
+ 'trigger',
8
+ 'gateway',
9
+ 'queue-job',
10
+ 'schedule',
11
+ 'agent-tool',
12
+ ];
13
+ export const HOPS = [
14
+ 'rpc',
15
+ 'mesh',
16
+ 'remote-addon',
17
+ 'enqueue',
18
+ 'schedule',
19
+ 'start-workflow',
20
+ 'child-workflow',
21
+ ];
22
+ export const ADAPTERS = ['in-memory', 'bullmq', 'pg-boss', 'nats'];
23
+ export const WORKFLOWS = [
24
+ 'dsl-queued',
25
+ 'dsl-inline',
26
+ 'graph-queued',
27
+ 'graph-inline',
28
+ ];
29
+ export const STEPS = [
30
+ 'rpc',
31
+ 'sleep',
32
+ 'suspend',
33
+ 'approval',
34
+ 'fan-out',
35
+ 'retry',
36
+ 'compensation',
37
+ ];
38
+ export const INBOUNDS = ['none', 'valid', 'invalid', 'forged'];
39
+ export const OUTCOMES = [
40
+ 'success',
41
+ 'throw-pikku-error',
42
+ 'throw-plain-error',
43
+ 'schema-invalid',
44
+ 'auth-mesh-jwt',
45
+ 'auth-remote-addon',
46
+ 'permission-denied',
47
+ 'remote-4xx',
48
+ 'remote-5xx',
49
+ 'remote-network',
50
+ 'remote-timeout',
51
+ 'job-exhausted',
52
+ 'job-discarded',
53
+ 'job-poison',
54
+ 'abandoned',
55
+ 'step-compensated',
56
+ 'workflow-cancelled',
57
+ 'approval-rejected',
58
+ 'fan-out-sibling-failure',
59
+ ];
60
+ export const TIMINGS = [
61
+ 'immediate',
62
+ 'delayed-job',
63
+ 'sleep-wakeup',
64
+ 'slow-overlap',
65
+ 'retry-backoff',
66
+ 'lease-expiry-redelivery',
67
+ 'duplicate-delivery',
68
+ 'concurrent-starts',
69
+ 'enqueue-before-fail',
70
+ 'job-after-parent',
71
+ ];
72
+ const WORKFLOW_OUTCOMES = [
73
+ 'step-compensated',
74
+ 'workflow-cancelled',
75
+ 'approval-rejected',
76
+ 'fan-out-sibling-failure',
77
+ ];
78
+ const REMOTE_OUTCOMES = [
79
+ 'auth-remote-addon',
80
+ 'remote-4xx',
81
+ 'remote-5xx',
82
+ 'remote-network',
83
+ 'remote-timeout',
84
+ ];
85
+ const JOB_OUTCOMES = [
86
+ 'job-exhausted',
87
+ 'job-discarded',
88
+ 'job-poison',
89
+ ];
90
+ /** Timings that are about how a queue delivers a job. */
91
+ const DELIVERY_TIMINGS = [
92
+ 'delayed-job',
93
+ 'lease-expiry-redelivery',
94
+ 'duplicate-delivery',
95
+ 'job-after-parent',
96
+ ];
97
+ const isWorkflowHop = (hop) => hop === 'start-workflow' || hop === 'child-workflow';
98
+ const isInline = (workflow) => workflow?.endsWith('-inline') ?? false;
99
+ const isGraph = (workflow) => workflow?.startsWith('graph') ?? false;
100
+ const noStep = (step) => step === undefined || step === 'none';
101
+ const known = (...values) => values.every((value) => value !== undefined);
102
+ /**
103
+ * Whether a partial combination could only be run by a queue, given the chain
104
+ * has no queue job in it. Undecided until both the start and the hop are known.
105
+ */
106
+ const noJobInChain = ({ start, hop }) => known(start, hop) && start !== 'queue-job' && hop !== 'enqueue';
107
+ /**
108
+ * The combinations that cannot happen, each with why. Every matrix goes through
109
+ * this list and reports what it dropped, so nothing is filtered silently. Rules
110
+ * see partial combinations and must stay undecided (false) on a missing value.
111
+ */
112
+ export const EXCLUSIONS = [
113
+ {
114
+ id: 'no-inbound-carrier',
115
+ why: 'CLI argv, a trigger source event and a gateway platform message carry no traceparent: each is the root of a trace',
116
+ applies: ({ start, inbound }) => known(start, inbound) &&
117
+ ['cli', 'trigger', 'gateway'].includes(start) &&
118
+ inbound !== 'none',
119
+ },
120
+ {
121
+ id: 'agent-caller-trace-is-not-parsed',
122
+ why: 'An agent tool takes the TraceContext object of its own caller, in-process: no string to be malformed or forged',
123
+ applies: ({ start, inbound }) => start === 'agent-tool' && (inbound === 'invalid' || inbound === 'forged'),
124
+ },
125
+ {
126
+ id: 'job-outcome-needs-a-job',
127
+ why: 'Exhausted retries, discard and poison are about a queue job, and the chain has none',
128
+ applies: (combo) => JOB_OUTCOMES.includes(combo.outcome) && noJobInChain(combo),
129
+ },
130
+ {
131
+ id: 'remote-outcome-needs-a-remote-addon',
132
+ why: 'Remote addon failures only exist on a hop to a remote addon',
133
+ applies: ({ hop, outcome, step }) =>
134
+ // A workflow step's RPC can itself be a remote addon call.
135
+ noStep(step) &&
136
+ known(hop) &&
137
+ REMOTE_OUTCOMES.includes(outcome) &&
138
+ hop !== 'remote-addon',
139
+ },
140
+ {
141
+ id: 'mesh-auth-needs-mesh',
142
+ why: 'pikkuMeshAuthMiddleware guards a mesh hop or an HTTP route; nothing else presents a mesh JWT',
143
+ applies: ({ start, hop, outcome, step }) => noStep(step) &&
144
+ known(start, hop) &&
145
+ outcome === 'auth-mesh-jwt' &&
146
+ hop !== 'mesh' &&
147
+ start !== 'http',
148
+ },
149
+ {
150
+ id: 'remote-host-validates-for-itself',
151
+ why: 'Schema and permission checks of a remote addon run on its host; this side sees a remote-4xx, its own outcome',
152
+ applies: ({ hop, outcome }) => hop === 'remote-addon' &&
153
+ (outcome === 'schema-invalid' || outcome === 'permission-denied'),
154
+ },
155
+ {
156
+ id: 'workflow-outcome-needs-a-workflow',
157
+ why: 'Compensation, cancellation, approval and fan-out only exist inside a workflow run',
158
+ applies: ({ hop, outcome }) => known(hop) && WORKFLOW_OUTCOMES.includes(outcome) && !isWorkflowHop(hop),
159
+ },
160
+ {
161
+ id: 'workflow-dimensions-need-a-workflow-hop',
162
+ why: 'Pairwise only: a chain with no workflow in it has no workflow type or step, which is the value `none`',
163
+ applies: ({ hop, workflow, step }) => known(hop) &&
164
+ ((workflow !== undefined &&
165
+ (workflow !== 'none') !== isWorkflowHop(hop)) ||
166
+ (step !== undefined && (step !== 'none') !== isWorkflowHop(hop))),
167
+ },
168
+ {
169
+ id: 'adapter-needs-a-queue',
170
+ why: 'Pairwise only: a chain with no job through a queue exercises no adapter, so only the in-memory value applies',
171
+ applies: ({ adapter, start, hop, workflow }) => known(adapter, start, hop, workflow) &&
172
+ adapter !== 'in-memory' &&
173
+ start !== 'queue-job' &&
174
+ hop !== 'enqueue' &&
175
+ !workflow.endsWith('-queued'),
176
+ },
177
+ {
178
+ id: 'graph-has-no-such-step',
179
+ why: 'A graph is RPC nodes: sleep, suspend and approval are DSL primitives',
180
+ applies: ({ workflow, step }) => isGraph(workflow) && ['sleep', 'suspend', 'approval'].includes(step),
181
+ },
182
+ {
183
+ id: 'inline-graph-has-no-retry-loop',
184
+ why: 'An inline graph node does not retry',
185
+ applies: ({ workflow, step, timing }) => workflow === 'graph-inline' &&
186
+ (step === 'retry' || timing === 'retry-backoff'),
187
+ },
188
+ {
189
+ id: 'inline-run-has-no-job',
190
+ why: 'An inline workflow does not touch the queue, so no job can exhaust, be discarded or be poison',
191
+ applies: ({ workflow, outcome }) => isInline(workflow) && JOB_OUTCOMES.includes(outcome),
192
+ },
193
+ {
194
+ id: 'approval-outcome-needs-approval-step',
195
+ why: 'Only an approval step can be rejected',
196
+ applies: ({ step, outcome }) => known(step) &&
197
+ step !== 'none' &&
198
+ outcome === 'approval-rejected' &&
199
+ step !== 'approval',
200
+ },
201
+ {
202
+ id: 'fan-out-outcome-needs-fan-out-step',
203
+ why: 'Only a fan-out has concurrent siblings',
204
+ applies: ({ step, outcome }) => known(step) &&
205
+ step !== 'none' &&
206
+ outcome === 'fan-out-sibling-failure' &&
207
+ step !== 'fan-out',
208
+ },
209
+ {
210
+ id: 'delivery-timing-needs-a-job',
211
+ why: 'Delays, redelivery and duplicates are how a queue delivers a job, and the chain has none',
212
+ applies: (combo) => DELIVERY_TIMINGS.includes(combo.timing) && noJobInChain(combo),
213
+ },
214
+ {
215
+ id: 'enqueue-timing-needs-enqueue',
216
+ why: 'Only a chain that enqueues can enqueue just before failing, or leave a job to outlive it',
217
+ applies: ({ hop, timing }) => known(hop) &&
218
+ (timing === 'enqueue-before-fail' || timing === 'job-after-parent') &&
219
+ hop !== 'enqueue',
220
+ },
221
+ {
222
+ id: 'sleep-timing-needs-sleep-step',
223
+ why: 'Only a sleep step wakes up',
224
+ applies: ({ hop, step, timing }) => timing === 'sleep-wakeup' &&
225
+ ((known(hop) && !isWorkflowHop(hop)) ||
226
+ (known(step) && step !== 'sleep')),
227
+ },
228
+ {
229
+ id: 'retry-timing-needs-a-retry',
230
+ why: 'Backoff needs a retried job or a retried step, and only an outcome that fails once can show it',
231
+ applies: ({ start, hop, step, outcome, timing }) => timing === 'retry-backoff' &&
232
+ ((known(start, hop) &&
233
+ start !== 'queue-job' &&
234
+ hop !== 'enqueue' &&
235
+ !isWorkflowHop(hop)) ||
236
+ (known(outcome) &&
237
+ outcome !== 'success' &&
238
+ outcome !== 'job-exhausted') ||
239
+ (known(hop, step) && isWorkflowHop(hop) && step !== 'retry')),
240
+ },
241
+ ];
242
+ /** The first rule that excludes this combination, if any. */
243
+ export const exclusionOf = (combo) => EXCLUSIONS.find((rule) => rule.applies(combo))?.id;
244
+ const tally = (name, combos) => {
245
+ const included = [];
246
+ const excluded = {};
247
+ for (const combo of combos) {
248
+ const rule = exclusionOf(combo);
249
+ if (rule)
250
+ excluded[rule] = (excluded[rule] ?? 0) + 1;
251
+ else
252
+ included.push(combo);
253
+ }
254
+ const report = {
255
+ name,
256
+ generated: combos.length,
257
+ included: included.length,
258
+ excluded,
259
+ };
260
+ return { combos: included, report };
261
+ };
262
+ /** Start × hop × outcome. */
263
+ export const chainMatrix = () => tally('start × hop × outcome', crossProduct({ start: STARTS, hop: HOPS, outcome: OUTCOMES }));
264
+ /** Adapter × workflow × step × outcome. */
265
+ export const workflowMatrix = () => tally('adapter × workflow × step × outcome', crossProduct({
266
+ adapter: ADAPTERS,
267
+ workflow: WORKFLOWS,
268
+ step: STEPS,
269
+ outcome: OUTCOMES,
270
+ }));
271
+ /**
272
+ * Every value pair across all eight dimensions. `workflow` and `step` gain the
273
+ * value `none` for a chain with no workflow in it.
274
+ */
275
+ export const pairwiseMatrix = () => {
276
+ const rulesUsed = new Set();
277
+ const { combos, impossible, pairs } = pairwiseCombos({
278
+ start: STARTS,
279
+ hop: HOPS,
280
+ inbound: INBOUNDS,
281
+ outcome: OUTCOMES,
282
+ timing: TIMINGS,
283
+ workflow: ['none', ...WORKFLOWS],
284
+ step: ['none', ...STEPS],
285
+ adapter: ADAPTERS,
286
+ }, (partial) => {
287
+ const rule = exclusionOf(partial);
288
+ if (rule)
289
+ rulesUsed.add(rule);
290
+ return rule === undefined;
291
+ });
292
+ const excluded = {};
293
+ for (const key of impossible) {
294
+ const pair = Object.fromEntries(key.split('&').map((part) => part.split('=')));
295
+ const rule = exclusionOf(pair) ?? 'no-combination-holds-this-pair';
296
+ excluded[rule] = (excluded[rule] ?? 0) + 1;
297
+ }
298
+ const report = {
299
+ name: 'pairwise (value pairs)',
300
+ generated: pairs,
301
+ included: pairs - impossible.length,
302
+ excluded,
303
+ };
304
+ return { combos, report, impossible, rulesUsed };
305
+ };
@@ -0,0 +1,20 @@
1
+ /** One value per dimension, possibly only some of them while being built. */
2
+ export type Combo = Record<string, string>;
3
+ export type Dimensions = Record<string, readonly string[]>;
4
+ /** Every combination of the named dimensions. */
5
+ export declare const crossProduct: (dimensions: Dimensions) => Combo[];
6
+ /**
7
+ * Finds whole combinations covering every pair of values across `dimensions`,
8
+ * deterministically. A partial combination `allowed` refuses is never extended,
9
+ * and a pair no allowed combination can hold is returned in `impossible` rather
10
+ * than dropped, so a caller can account for it.
11
+ *
12
+ * Greedy: each round takes the first uncovered pair and fills the remaining
13
+ * dimensions with whichever value covers the most new pairs, backtracking when
14
+ * `allowed` closes a branch.
15
+ */
16
+ export declare const pairwiseCombos: (dimensions: Dimensions, allowed?: (partial: Combo) => boolean) => {
17
+ combos: Combo[];
18
+ impossible: string[];
19
+ pairs: number;
20
+ };
@@ -0,0 +1,75 @@
1
+ /** Every combination of the named dimensions. */
2
+ export const crossProduct = (dimensions) => Object.entries(dimensions).reduce((combos, [name, values]) => combos.flatMap((combo) => values.map((value) => ({ ...combo, [name]: value }))), [{}]);
3
+ const pairKey = (a, x, b, y) => a < b ? `${a}=${x}&${b}=${y}` : `${b}=${y}&${a}=${x}`;
4
+ /**
5
+ * Finds whole combinations covering every pair of values across `dimensions`,
6
+ * deterministically. A partial combination `allowed` refuses is never extended,
7
+ * and a pair no allowed combination can hold is returned in `impossible` rather
8
+ * than dropped, so a caller can account for it.
9
+ *
10
+ * Greedy: each round takes the first uncovered pair and fills the remaining
11
+ * dimensions with whichever value covers the most new pairs, backtracking when
12
+ * `allowed` closes a branch.
13
+ */
14
+ export const pairwiseCombos = (dimensions, allowed = () => true) => {
15
+ const names = Object.keys(dimensions);
16
+ const uncovered = new Map();
17
+ for (let i = 0; i < names.length; i++) {
18
+ for (let j = i + 1; j < names.length; j++) {
19
+ const [a, b] = [names[i], names[j]];
20
+ for (const x of dimensions[a]) {
21
+ for (const y of dimensions[b]) {
22
+ uncovered.set(pairKey(a, x, b, y), { [a]: x, [b]: y });
23
+ }
24
+ }
25
+ }
26
+ }
27
+ const pairs = uncovered.size;
28
+ const covers = (combo, name, value) => {
29
+ let count = 0;
30
+ for (const other of Object.keys(combo)) {
31
+ if (other !== name &&
32
+ uncovered.has(pairKey(name, value, other, combo[other]))) {
33
+ count++;
34
+ }
35
+ }
36
+ return count;
37
+ };
38
+ const complete = (combo, remaining) => {
39
+ if (!allowed(combo))
40
+ return undefined;
41
+ const [name, ...rest] = remaining;
42
+ if (!name)
43
+ return combo;
44
+ const ranked = dimensions[name].map((value, order) => ({
45
+ value,
46
+ order,
47
+ gain: covers(combo, name, value),
48
+ })).sort((x, y) => y.gain - x.gain || x.order - y.order);
49
+ for (const { value } of ranked) {
50
+ const found = complete({ ...combo, [name]: value }, rest);
51
+ if (found)
52
+ return found;
53
+ }
54
+ return undefined;
55
+ };
56
+ const combos = [];
57
+ const impossible = [];
58
+ while (uncovered.size > 0) {
59
+ const [key, seed] = uncovered.entries().next().value;
60
+ const combo = complete(seed, names.filter((name) => !(name in seed)));
61
+ if (!combo) {
62
+ uncovered.delete(key);
63
+ impossible.push(key);
64
+ continue;
65
+ }
66
+ combos.push(combo);
67
+ for (let i = 0; i < names.length; i++) {
68
+ for (let j = i + 1; j < names.length; j++) {
69
+ const [a, b] = [names[i], names[j]];
70
+ uncovered.delete(pairKey(a, combo[a], b, combo[b]));
71
+ }
72
+ }
73
+ }
74
+ return { combos, impossible, pairs };
75
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Prints what each matrix generated and dropped, by reason:
3
+ * `bun src/testing/trace-matrix/report.ts`
4
+ */
5
+ import { chainMatrix, EXCLUSIONS, pairwiseMatrix, workflowMatrix, } from './dimensions.js';
6
+ for (const { report, combos } of [
7
+ chainMatrix(),
8
+ workflowMatrix(),
9
+ pairwiseMatrix(),
10
+ ]) {
11
+ console.log(`${report.name}: ${combos.length} tests from ${report.generated}, ${report.included} kept`);
12
+ for (const [id, count] of Object.entries(report.excluded)) {
13
+ const why = EXCLUSIONS.find((rule) => rule.id === id)?.why;
14
+ console.log(` ${count} ${id}: ${why ?? 'see the rules this pair crosses'}`);
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ import type { JobOptions } from '../../wirings/queue/queue.types.js';
2
+ import type { Adapter } from './dimensions.js';
3
+ export type EncodedJob = {
4
+ body: string;
5
+ headers: Record<string, string>;
6
+ };
7
+ /**
8
+ * How one adapter puts a job on the wire, so the core run covers each wire
9
+ * format with no broker. SIMULATED: the real adapters run the same permutations
10
+ * in `queue-trace-conformance.ts`, behind `scripts/test-trace-adapters.sh`.
11
+ */
12
+ export type JobTransport = {
13
+ encode: (data: unknown, options?: JobOptions) => EncodedJob;
14
+ decode: (job: EncodedJob) => {
15
+ data: any;
16
+ traceparent?: string;
17
+ };
18
+ };
19
+ export declare const TRANSPORTS: Record<Adapter, JobTransport>;
@@ -0,0 +1,27 @@
1
+ import { unwrapJobTrace, wrapJobTrace, } from '../../wirings/queue/queue-trace.js';
2
+ const json = (value) => value === undefined ? 'null' : JSON.stringify(value);
3
+ const envelope = {
4
+ encode: (data, options) => ({
5
+ body: json(wrapJobTrace(data, options?.traceparent)),
6
+ headers: {},
7
+ }),
8
+ decode: ({ body }) => unwrapJobTrace(JSON.parse(body)),
9
+ };
10
+ const headers = {
11
+ encode: (data, options) => ({
12
+ body: json(data),
13
+ headers: options?.traceparent
14
+ ? { traceparent: options.traceparent }
15
+ : {},
16
+ }),
17
+ decode: ({ body, headers }) => ({
18
+ data: JSON.parse(body),
19
+ traceparent: headers.traceparent,
20
+ }),
21
+ };
22
+ export const TRANSPORTS = {
23
+ 'in-memory': headers,
24
+ bullmq: envelope,
25
+ 'pg-boss': envelope,
26
+ nats: headers,
27
+ };
@@ -0,0 +1,24 @@
1
+ import { InMemoryWorkflowService } from '../../wirings/workflow/in-memory-workflow-service.js';
2
+ import type { QueuedWorkflowHarness } from '../service-tests/queued-workflow-harness.js';
3
+ import type { Outcome, Step, Workflow } from './dimensions.js';
4
+ /** Sleeps go through the scheduler, as in a deployment, so the wake-up is driven by hand. */
5
+ export declare class ScheduledSleeps extends InMemoryWorkflowService {
6
+ protected scheduleSleep(...args: [string, string, number | string]): Promise<boolean>;
7
+ }
8
+ /** What a step's RPC throws for an outcome: the class a real call would raise. */
9
+ export declare const outcomeError: (outcome: Outcome) => Error | undefined;
10
+ export type RunCase = {
11
+ /** Names every function and workflow of this run apart from a concurrent one. */
12
+ id: string;
13
+ workflow: Workflow;
14
+ step: Step;
15
+ outcome: Outcome;
16
+ };
17
+ /**
18
+ * Registers a workflow named `flow-<id>` that reaches `step`, then has its last
19
+ * RPC meet `outcome`. Returns what brings the run to its end.
20
+ */
21
+ export declare const defineRun: (h: QueuedWorkflowHarness, run: RunCase, child?: boolean) => {
22
+ /** Wakes whatever the run is waiting on, then lets it finish. */
23
+ settle: (runId: string, wakeSleeps: () => Promise<void>) => Promise<void>;
24
+ };