@pikku/core 0.12.7 → 0.12.9
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 +30 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/errors/errors.d.ts +14 -0
- package/dist/errors/errors.js +27 -0
- package/dist/handle-error.js +2 -1
- package/dist/middleware/auth-bearer.js +10 -1
- package/dist/middleware/auth-cookie.js +34 -25
- package/dist/middleware/timeout.js +11 -5
- package/dist/pikku-state.js +1 -1
- package/dist/services/local-content.d.ts +7 -4
- package/dist/services/local-content.js +42 -12
- package/dist/services/local-secrets.js +2 -2
- package/dist/testing/service-tests.js +1 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
- package/dist/wirings/channel/local/local-channel-runner.js +11 -6
- package/dist/wirings/http/http-runner.js +4 -2
- package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
- package/dist/wirings/http/web-request.js +3 -1
- package/dist/wirings/workflow/graph/graph-runner.d.ts +7 -0
- package/dist/wirings/workflow/graph/graph-runner.js +72 -7
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +3 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +59 -16
- package/dist/wirings/workflow/workflow.types.d.ts +4 -0
- package/package.json +1 -1
- package/src/env.ts +1 -0
- package/src/errors/errors.ts +35 -0
- package/src/handle-error.ts +2 -1
- package/src/middleware/auth-bearer.ts +10 -1
- package/src/middleware/auth-cookie.ts +11 -4
- package/src/middleware/timeout.ts +14 -7
- package/src/pikku-state.ts +1 -1
- package/src/services/local-content.ts +61 -13
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +2 -2
- package/src/testing/service-tests.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
- package/src/wirings/channel/local/local-channel-runner.ts +11 -6
- package/src/wirings/http/http-runner.ts +4 -2
- package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
- package/src/wirings/http/web-request.ts +3 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
- package/src/wirings/workflow/graph/graph-runner.ts +84 -7
- package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +95 -25
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from './ai-agent.types.js'
|
|
12
12
|
import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js'
|
|
13
13
|
import { PikkuError } from '../../errors/error-handler.js'
|
|
14
|
+
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
14
15
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
15
16
|
import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
|
|
16
17
|
import type { SessionService } from '../../services/user-session-service.js'
|
|
@@ -520,7 +521,7 @@ export async function prepareAgentRun(
|
|
|
520
521
|
|
|
521
522
|
const agentRunner = singletonServices.aiAgentRunner
|
|
522
523
|
if (!agentRunner) {
|
|
523
|
-
throw new
|
|
524
|
+
throw new AIProviderNotConfiguredError()
|
|
524
525
|
}
|
|
525
526
|
|
|
526
527
|
if (agent.dynamicWorkflows && singletonServices.workflowService) {
|
|
@@ -5,6 +5,7 @@ import { resetPikkuState, pikkuState } from '../../pikku-state.js'
|
|
|
5
5
|
import { runAIAgent } from './ai-agent-runner.js'
|
|
6
6
|
import type { CoreAIAgent, PikkuAIMiddlewareHooks } from './ai-agent.types.js'
|
|
7
7
|
import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
|
|
8
|
+
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
8
9
|
|
|
9
10
|
beforeEach(() => {
|
|
10
11
|
resetPikkuState()
|
|
@@ -42,6 +43,39 @@ const makeStepResult = (
|
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
describe('runAIAgent', () => {
|
|
46
|
+
test('throws AIProviderNotConfiguredError when aiAgentRunner is missing', async () => {
|
|
47
|
+
addTestAgent('no-provider-agent')
|
|
48
|
+
|
|
49
|
+
const mockServices = {
|
|
50
|
+
logger: {
|
|
51
|
+
info: () => {},
|
|
52
|
+
warn: () => {},
|
|
53
|
+
error: () => {},
|
|
54
|
+
debug: () => {},
|
|
55
|
+
},
|
|
56
|
+
aiRunState: {
|
|
57
|
+
createRun: async () => 'run-no-provider',
|
|
58
|
+
updateRun: async () => {},
|
|
59
|
+
},
|
|
60
|
+
} as any
|
|
61
|
+
|
|
62
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
63
|
+
|
|
64
|
+
await assert.rejects(
|
|
65
|
+
() =>
|
|
66
|
+
runAIAgent(
|
|
67
|
+
'no-provider-agent',
|
|
68
|
+
{ message: 'hello', threadId: 't', resourceId: 'r' },
|
|
69
|
+
{}
|
|
70
|
+
),
|
|
71
|
+
(error: unknown) => {
|
|
72
|
+
assert.ok(error instanceof AIProviderNotConfiguredError)
|
|
73
|
+
assert.match((error as Error).message, /AI provider/)
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
45
79
|
test('marks run as failed when runner throws', async () => {
|
|
46
80
|
addTestAgent('failing-agent')
|
|
47
81
|
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js'
|
|
32
32
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
33
33
|
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
34
|
+
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
34
35
|
import { randomUUID } from 'crypto'
|
|
35
36
|
|
|
36
37
|
export async function runAIAgent(
|
|
@@ -367,7 +368,7 @@ export async function resumeAIAgentSync(
|
|
|
367
368
|
const memoryConfig = agent.memory
|
|
368
369
|
const agentRunner = singletonServices.aiAgentRunner
|
|
369
370
|
if (!agentRunner) {
|
|
370
|
-
throw new
|
|
371
|
+
throw new AIProviderNotConfiguredError()
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
const approvedIds = new Set(
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
AIAgentMemoryConfig,
|
|
11
11
|
} from './ai-agent.types.js'
|
|
12
12
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
13
|
+
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
13
14
|
import {
|
|
14
15
|
combineChannelMiddleware,
|
|
15
16
|
wrapChannelWithMiddleware,
|
|
@@ -748,7 +749,7 @@ export async function resumeAIAgent(
|
|
|
748
749
|
const memoryConfig = agent.memory
|
|
749
750
|
const agentRunner = singletonServices.aiAgentRunner
|
|
750
751
|
if (!agentRunner) {
|
|
751
|
-
throw new
|
|
752
|
+
throw new AIProviderNotConfiguredError()
|
|
752
753
|
}
|
|
753
754
|
|
|
754
755
|
if (!input.approved) {
|
|
@@ -5,6 +5,8 @@ import { processMessageHandlers } from '../channel-handler.js'
|
|
|
5
5
|
import type { CoreChannel, RunChannelOptions } from '../channel.types.js'
|
|
6
6
|
import { PikkuLocalChannelHandler } from './local-channel-handler.js'
|
|
7
7
|
import type { PikkuWire, WireServices } from '../../../types/core.types.js'
|
|
8
|
+
import { isProduction } from '../../../env.js'
|
|
9
|
+
import { getErrorResponse } from '../../../errors/error-handler.js'
|
|
8
10
|
import { handleHTTPError } from '../../../handle-error.js'
|
|
9
11
|
import {
|
|
10
12
|
PikkuSessionService,
|
|
@@ -121,9 +123,10 @@ export const runLocalChannel = async ({
|
|
|
121
123
|
}
|
|
122
124
|
} catch (e: any) {
|
|
123
125
|
singletonServices.logger.error(`Error handling onConnect: ${e}`)
|
|
126
|
+
const errorResponse = getErrorResponse(e)
|
|
124
127
|
channel.send({
|
|
125
|
-
error:
|
|
126
|
-
errorName: e.constructor?.name,
|
|
128
|
+
error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
|
|
129
|
+
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
127
130
|
})
|
|
128
131
|
}
|
|
129
132
|
}
|
|
@@ -143,9 +146,10 @@ export const runLocalChannel = async ({
|
|
|
143
146
|
})
|
|
144
147
|
} catch (e: any) {
|
|
145
148
|
singletonServices.logger.error(`Error handling onDisconnect: ${e}`)
|
|
149
|
+
const errorResponse = getErrorResponse(e)
|
|
146
150
|
channel.send({
|
|
147
|
-
error:
|
|
148
|
-
errorName: e.constructor?.name,
|
|
151
|
+
error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
|
|
152
|
+
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
149
153
|
})
|
|
150
154
|
}
|
|
151
155
|
}
|
|
@@ -165,9 +169,10 @@ export const runLocalChannel = async ({
|
|
|
165
169
|
await channel.send(result)
|
|
166
170
|
} catch (e: any) {
|
|
167
171
|
singletonServices.logger.error(e)
|
|
172
|
+
const errorResponse = getErrorResponse(e)
|
|
168
173
|
channel.send({
|
|
169
|
-
error:
|
|
170
|
-
errorName: e.constructor?.name,
|
|
174
|
+
error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
|
|
175
|
+
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
171
176
|
})
|
|
172
177
|
setTimeout(() => channel.close(), 200)
|
|
173
178
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getCreateWireServices,
|
|
33
33
|
} from '../../pikku-state.js'
|
|
34
34
|
import { PikkuSessionService } from '../../services/user-session-service.js'
|
|
35
|
+
import { getErrorResponse } from '../../errors/error-handler.js'
|
|
35
36
|
import { handleHTTPError } from '../../handle-error.js'
|
|
36
37
|
import { pikkuState } from '../../pikku-state.js'
|
|
37
38
|
import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
|
|
@@ -530,7 +531,7 @@ export const fetchData = async <In, Out>(
|
|
|
530
531
|
apiRoute,
|
|
531
532
|
apiType,
|
|
532
533
|
})
|
|
533
|
-
throw new NotFoundError(
|
|
534
|
+
throw new NotFoundError()
|
|
534
535
|
}
|
|
535
536
|
|
|
536
537
|
// Execute the matched route along with its middleware and session management
|
|
@@ -552,10 +553,11 @@ export const fetchData = async <In, Out>(
|
|
|
552
553
|
// For SSE routes, send error through the stream since the response is already in stream mode
|
|
553
554
|
singletonServices.logger.error(e instanceof Error ? e.message : e)
|
|
554
555
|
try {
|
|
556
|
+
const errorResponse = getErrorResponse(e)
|
|
555
557
|
response.arrayBuffer(
|
|
556
558
|
JSON.stringify({
|
|
557
559
|
type: 'error',
|
|
558
|
-
errorText:
|
|
560
|
+
errorText: errorResponse?.message ?? 'Internal server error',
|
|
559
561
|
})
|
|
560
562
|
)
|
|
561
563
|
response.arrayBuffer(JSON.stringify({ type: 'done' }))
|
|
@@ -105,6 +105,9 @@ export class PikkuFetchHTTPRequest<In = unknown>
|
|
|
105
105
|
const merged: Record<string, unknown> = {}
|
|
106
106
|
for (const part of parts) {
|
|
107
107
|
for (const [key, value] of Object.entries(part)) {
|
|
108
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
108
111
|
if (key in merged && !valuesAreEquivalent(merged[key], value)) {
|
|
109
112
|
throw new UnprocessableContentError(
|
|
110
113
|
`Conflicting values for key "${key}": "${merged[key]}" vs "${value}"`
|
|
@@ -141,7 +144,14 @@ export class PikkuFetchHTTPRequest<In = unknown>
|
|
|
141
144
|
body = { data: buffer }
|
|
142
145
|
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
143
146
|
const text = await this.request.text()
|
|
144
|
-
|
|
147
|
+
const params = new URLSearchParams(text)
|
|
148
|
+
let count = 0
|
|
149
|
+
for (const _ of params) {
|
|
150
|
+
if (++count > 256) {
|
|
151
|
+
throw new UnprocessableContentError('Too many form parameters')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
body = Object.fromEntries(params)
|
|
145
155
|
} else {
|
|
146
156
|
throw new UnprocessableContentError(
|
|
147
157
|
`Unsupported content type ${contentType}`
|
|
@@ -6,9 +6,11 @@ import type { PikkuHTTPResponse } from './http.types.js'
|
|
|
6
6
|
* Useful for bridging Pikku routes to libraries that expect standard Web Request objects.
|
|
7
7
|
*/
|
|
8
8
|
export function toWebRequest(req: PikkuHTTPRequest, baseUrl?: string): Request {
|
|
9
|
+
const proto = req.header('x-forwarded-proto') ?? 'http'
|
|
10
|
+
const host = req.header('x-forwarded-host') ?? req.header('host') ?? 'localhost'
|
|
9
11
|
const url = new URL(
|
|
10
12
|
req.path(),
|
|
11
|
-
baseUrl ??
|
|
13
|
+
baseUrl ?? `${proto}://${host}`
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
const query = req.query()
|
|
@@ -484,4 +484,46 @@ describe('graph-runner bugs', () => {
|
|
|
484
484
|
|
|
485
485
|
delete metaState['testInlineRpcMissing']
|
|
486
486
|
})
|
|
487
|
+
|
|
488
|
+
test('graph workflow with inline: true in meta should run inline', async () => {
|
|
489
|
+
const ws = new InMemoryWorkflowService()
|
|
490
|
+
let executed = false
|
|
491
|
+
|
|
492
|
+
const mockRpcService = {
|
|
493
|
+
rpcWithWire: async () => {
|
|
494
|
+
executed = true
|
|
495
|
+
return { done: true }
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Set up queue service to verify nothing is queued
|
|
500
|
+
let queued = false
|
|
501
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
502
|
+
queueService: {
|
|
503
|
+
add: async () => {
|
|
504
|
+
queued = true
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
} as any)
|
|
508
|
+
|
|
509
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
510
|
+
metaState['testInlineMetaGraph'] = {
|
|
511
|
+
name: 'testInlineMetaGraph',
|
|
512
|
+
pikkuFuncId: 'testInlineMetaGraph',
|
|
513
|
+
source: 'graph',
|
|
514
|
+
inline: true,
|
|
515
|
+
entryNodeIds: ['a'],
|
|
516
|
+
graphHash: 'inline-meta-graph-hash',
|
|
517
|
+
nodes: {
|
|
518
|
+
a: { nodeId: 'a', rpcName: 'doA' },
|
|
519
|
+
},
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
await runWorkflowGraph(ws, 'testInlineMetaGraph', {}, mockRpcService, true)
|
|
523
|
+
|
|
524
|
+
assert.equal(executed, true, 'RPC should have been executed inline')
|
|
525
|
+
assert.equal(queued, false, 'nothing should have been queued')
|
|
526
|
+
|
|
527
|
+
delete metaState['testInlineMetaGraph']
|
|
528
|
+
})
|
|
487
529
|
})
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { PikkuWorkflowService } from '../pikku-workflow-service.js'
|
|
2
2
|
import type { GraphWireState, PikkuGraphWire } from './workflow-graph.types.js'
|
|
3
|
-
import { pikkuState } from '../../../pikku-state.js'
|
|
3
|
+
import { pikkuState, getSingletonServices } from '../../../pikku-state.js'
|
|
4
4
|
import type { WorkflowRuntimeMeta, WorkflowRunWire } from '../workflow.types.js'
|
|
5
5
|
import { RPCNotFoundError } from '../../rpc/rpc-runner.js'
|
|
6
6
|
|
|
7
|
+
export class ChildWorkflowStartedException extends Error {
|
|
8
|
+
name = 'ChildWorkflowStartedException'
|
|
9
|
+
constructor(
|
|
10
|
+
public parentRunId: string,
|
|
11
|
+
public stepId: string,
|
|
12
|
+
public childRunId: string
|
|
13
|
+
) {
|
|
14
|
+
super(`Child workflow started: ${childRunId}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
function buildTemplateRegex(nodeId: string): RegExp | null {
|
|
8
19
|
if (!nodeId.includes('${')) return null
|
|
9
20
|
const escaped = nodeId
|
|
@@ -442,9 +453,44 @@ export async function executeGraphStep(
|
|
|
442
453
|
}
|
|
443
454
|
|
|
444
455
|
try {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
456
|
+
let result: any
|
|
457
|
+
|
|
458
|
+
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
459
|
+
if (subWorkflowMeta) {
|
|
460
|
+
const childWire: WorkflowRunWire = {
|
|
461
|
+
type: 'workflow',
|
|
462
|
+
id: rpcName,
|
|
463
|
+
parentRunId: runId,
|
|
464
|
+
parentStepId: stepId,
|
|
465
|
+
}
|
|
466
|
+
const shouldInline =
|
|
467
|
+
subWorkflowMeta.inline || !getSingletonServices()?.queueService
|
|
468
|
+
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
469
|
+
rpcName,
|
|
470
|
+
data,
|
|
471
|
+
childWire,
|
|
472
|
+
rpcService,
|
|
473
|
+
{ inline: shouldInline }
|
|
474
|
+
)
|
|
475
|
+
await workflowService.setStepChildRunId(stepId, childRunId)
|
|
476
|
+
|
|
477
|
+
if (shouldInline) {
|
|
478
|
+
const childRun = await workflowService.getRun(childRunId)
|
|
479
|
+
if (childRun?.status === 'failed') {
|
|
480
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
481
|
+
}
|
|
482
|
+
if (childRun?.status === 'cancelled') {
|
|
483
|
+
throw new Error('Sub-workflow was cancelled')
|
|
484
|
+
}
|
|
485
|
+
result = childRun?.output
|
|
486
|
+
} else {
|
|
487
|
+
throw new ChildWorkflowStartedException(runId, stepId, childRunId)
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
491
|
+
graph: graphWire,
|
|
492
|
+
})
|
|
493
|
+
}
|
|
448
494
|
|
|
449
495
|
if (wireState.branchKey) {
|
|
450
496
|
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
@@ -452,6 +498,9 @@ export async function executeGraphStep(
|
|
|
452
498
|
|
|
453
499
|
return result
|
|
454
500
|
} catch (error) {
|
|
501
|
+
if (error instanceof ChildWorkflowStartedException) {
|
|
502
|
+
throw error
|
|
503
|
+
}
|
|
455
504
|
if (error instanceof RPCNotFoundError) {
|
|
456
505
|
await workflowService.updateRunStatus(runId, 'suspended', undefined, {
|
|
457
506
|
message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
|
|
@@ -541,9 +590,37 @@ async function executeGraphNodeInline(
|
|
|
541
590
|
}
|
|
542
591
|
|
|
543
592
|
try {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
593
|
+
let result: any
|
|
594
|
+
|
|
595
|
+
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
596
|
+
if (subWorkflowMeta) {
|
|
597
|
+
const childWire: WorkflowRunWire = {
|
|
598
|
+
type: 'workflow',
|
|
599
|
+
id: rpcName,
|
|
600
|
+
parentRunId: runId,
|
|
601
|
+
parentStepId: stepState.stepId,
|
|
602
|
+
}
|
|
603
|
+
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
604
|
+
rpcName,
|
|
605
|
+
input,
|
|
606
|
+
childWire,
|
|
607
|
+
rpcService,
|
|
608
|
+
{ inline: true }
|
|
609
|
+
)
|
|
610
|
+
await workflowService.setStepChildRunId(stepState.stepId, childRunId)
|
|
611
|
+
const childRun = await workflowService.getRun(childRunId)
|
|
612
|
+
if (childRun?.status === 'failed') {
|
|
613
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
614
|
+
}
|
|
615
|
+
if (childRun?.status === 'cancelled') {
|
|
616
|
+
throw new Error('Sub-workflow was cancelled')
|
|
617
|
+
}
|
|
618
|
+
result = childRun?.output
|
|
619
|
+
} else {
|
|
620
|
+
result = await rpcService.rpcWithWire(rpcName, input, {
|
|
621
|
+
graph: graphWire,
|
|
622
|
+
})
|
|
623
|
+
}
|
|
547
624
|
|
|
548
625
|
if (wireState.branchKey) {
|
|
549
626
|
await workflowService.setBranchTaken(
|
|
@@ -9,6 +9,115 @@ import {
|
|
|
9
9
|
type PikkuWorkflowWire,
|
|
10
10
|
} from './pikku-workflow-service.js'
|
|
11
11
|
|
|
12
|
+
describe('pikku-workflow-service inline meta flag', () => {
|
|
13
|
+
test('workflow with inline: true in meta should create inline run and not queue', async () => {
|
|
14
|
+
const ws = new InMemoryWorkflowService()
|
|
15
|
+
const workflowName = 'testInlineMetaFlag'
|
|
16
|
+
const graphHash = 'inline-meta-hash'
|
|
17
|
+
|
|
18
|
+
// Set up a queue service that tracks if anything was queued
|
|
19
|
+
let queued = false
|
|
20
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
21
|
+
logger: {
|
|
22
|
+
error: () => {},
|
|
23
|
+
info: () => {},
|
|
24
|
+
warn: () => {},
|
|
25
|
+
debug: () => {},
|
|
26
|
+
},
|
|
27
|
+
queueService: {
|
|
28
|
+
add: async () => {
|
|
29
|
+
queued = true
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} as any)
|
|
33
|
+
|
|
34
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
35
|
+
metaState[workflowName] = {
|
|
36
|
+
name: workflowName,
|
|
37
|
+
pikkuFuncId: workflowName,
|
|
38
|
+
source: 'dsl',
|
|
39
|
+
graphHash,
|
|
40
|
+
inline: true,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const functionMetaState = pikkuState(null, 'function', 'meta')
|
|
44
|
+
functionMetaState[workflowName] = {
|
|
45
|
+
name: workflowName,
|
|
46
|
+
sessionless: true,
|
|
47
|
+
permissions: [],
|
|
48
|
+
} as any
|
|
49
|
+
|
|
50
|
+
addWorkflow(workflowName, {
|
|
51
|
+
func: async () => {
|
|
52
|
+
return { ok: true }
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const { runId } = await ws.startWorkflow(workflowName, {}, {})
|
|
57
|
+
|
|
58
|
+
// The run should be created as inline
|
|
59
|
+
const run = await ws.getRun(runId)
|
|
60
|
+
assert.equal(run?.inline, true, 'run should be marked as inline')
|
|
61
|
+
assert.equal(
|
|
62
|
+
queued,
|
|
63
|
+
false,
|
|
64
|
+
'nothing should have been queued to the queue service'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
delete metaState[workflowName]
|
|
68
|
+
delete functionMetaState[workflowName]
|
|
69
|
+
pikkuState(null, 'workflows', 'registrations').delete(workflowName)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('workflow without inline flag should use queue when queueService exists', async () => {
|
|
73
|
+
const ws = new InMemoryWorkflowService()
|
|
74
|
+
const workflowName = 'testNonInlineMetaFlag'
|
|
75
|
+
const graphHash = 'non-inline-meta-hash'
|
|
76
|
+
|
|
77
|
+
let queued = false
|
|
78
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
79
|
+
queueService: {
|
|
80
|
+
add: async () => {
|
|
81
|
+
queued = true
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
} as any)
|
|
85
|
+
|
|
86
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
87
|
+
metaState[workflowName] = {
|
|
88
|
+
name: workflowName,
|
|
89
|
+
pikkuFuncId: workflowName,
|
|
90
|
+
source: 'dsl',
|
|
91
|
+
graphHash,
|
|
92
|
+
// no inline flag
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const functionMetaState = pikkuState(null, 'function', 'meta')
|
|
96
|
+
functionMetaState[workflowName] = {
|
|
97
|
+
name: workflowName,
|
|
98
|
+
sessionless: true,
|
|
99
|
+
permissions: [],
|
|
100
|
+
} as any
|
|
101
|
+
|
|
102
|
+
addWorkflow(workflowName, {
|
|
103
|
+
func: async () => {
|
|
104
|
+
return { ok: true }
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await ws.startWorkflow(workflowName, {}, {})
|
|
109
|
+
|
|
110
|
+
// Wait a tick
|
|
111
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
112
|
+
|
|
113
|
+
assert.equal(queued, true, 'workflow should have been queued')
|
|
114
|
+
|
|
115
|
+
delete metaState[workflowName]
|
|
116
|
+
delete functionMetaState[workflowName]
|
|
117
|
+
pikkuState(null, 'workflows', 'registrations').delete(workflowName)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
12
121
|
describe('pikku-workflow-service version mismatch fallback', () => {
|
|
13
122
|
test('should fall back to stored graph for dsl workflow version mismatch', async () => {
|
|
14
123
|
const ws = new InMemoryWorkflowService()
|