@pikku/core 0.12.69 → 0.12.70
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 +334 -0
- package/README.md +34 -2
- package/dist/function/functions.types.d.ts +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/pikku-state.js +1 -0
- package/dist/services/http-scenario-actors.d.ts +12 -4
- package/dist/services/http-scenario-actors.js +47 -45
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +1 -0
- package/dist/services/meta-service.d.ts +5 -1
- package/dist/services/meta-service.js +44 -18
- package/dist/services/scenario-actors-service.d.ts +108 -2
- package/dist/services/scenario-actors-service.js +40 -1
- package/dist/types/core.types.d.ts +21 -3
- package/dist/types/state.types.d.ts +3 -1
- package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
- package/dist/wirings/actor-flow/run-conversation.js +27 -27
- package/dist/wirings/cli/command-parser.js +11 -1
- package/dist/wirings/rpc/rpc-runner.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
- package/dist/wirings/workflow/feature.d.ts +28 -0
- package/dist/wirings/workflow/feature.js +57 -0
- package/dist/wirings/workflow/index.d.ts +13 -2
- package/dist/wirings/workflow/index.js +15 -0
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
- package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +103 -10
- package/dist/wirings/workflow/pikku-workflow-service.js +80 -135
- package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
- package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
- package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
- package/dist/wirings/workflow/scenario-poll.js +25 -0
- package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
- package/dist/wirings/workflow/scenario-prose.js +45 -0
- package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
- package/dist/wirings/workflow/scenario-step-guards.js +29 -0
- package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
- package/dist/wirings/workflow/scenario-step.types.js +1 -0
- package/dist/wirings/workflow/workflow.types.d.ts +81 -2
- package/package.json +3 -1
- package/src/function/functions.types.ts +32 -0
- package/src/index.ts +1 -0
- package/src/internal.ts +5 -1
- package/src/pikku-state.ts +1 -0
- package/src/services/http-scenario-actors.test.ts +85 -1
- package/src/services/http-scenario-actors.ts +65 -51
- package/src/services/index.ts +5 -0
- package/src/services/meta-service.test.ts +79 -0
- package/src/services/meta-service.ts +61 -26
- package/src/services/scenario-actors-service.ts +157 -2
- package/src/types/core.types.ts +27 -2
- package/src/types/state.types.ts +3 -0
- package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
- package/src/wirings/actor-flow/index.ts +1 -1
- package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
- package/src/wirings/actor-flow/run-conversation.ts +36 -41
- package/src/wirings/cli/command-parser.test.ts +60 -0
- package/src/wirings/cli/command-parser.ts +12 -1
- package/src/wirings/rpc/rpc-runner.test.ts +28 -5
- package/src/wirings/rpc/rpc-runner.ts +1 -1
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
- package/src/wirings/workflow/feature.test.ts +131 -0
- package/src/wirings/workflow/feature.ts +78 -0
- package/src/wirings/workflow/index.ts +73 -0
- package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +196 -208
- package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
- package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
- package/src/wirings/workflow/scenario-poll.test.ts +66 -0
- package/src/wirings/workflow/scenario-poll.ts +36 -0
- package/src/wirings/workflow/scenario-prose.test.ts +152 -0
- package/src/wirings/workflow/scenario-prose.ts +79 -0
- package/src/wirings/workflow/scenario-service.test.ts +155 -0
- package/src/wirings/workflow/scenario-step-guards.ts +43 -0
- package/src/wirings/workflow/scenario-step.test.ts +441 -8
- package/src/wirings/workflow/scenario-step.types.ts +157 -0
- package/src/wirings/workflow/workflow.types.ts +98 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ScenarioStepPhase } from './scenario-step.types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders the English sentence a reporter shows for a scenario step.
|
|
5
|
+
*
|
|
6
|
+
* This is the inversion of cucumber: rather than parsing English into a call,
|
|
7
|
+
* we render English out of a typed call, so the readable report survives
|
|
8
|
+
* without a regex registry paying for it.
|
|
9
|
+
*
|
|
10
|
+
* Lives in core so the CLI reporter and the console render identically.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Fill a step's `template` from the input that step was actually called with,
|
|
14
|
+
* so the reported sentence names the values under test — "sees @pikku/addon-todos"
|
|
15
|
+
* rather than "sees an addon in the gallery" repeated three times.
|
|
16
|
+
*
|
|
17
|
+
* A placeholder with no recorded value renders as nothing and the surrounding
|
|
18
|
+
* whitespace collapses, so an optional input that was omitted reads as a shorter
|
|
19
|
+
* sentence rather than a literal `{state}` leaking into the report.
|
|
20
|
+
*/
|
|
21
|
+
export const renderStepTemplate = (
|
|
22
|
+
template: string,
|
|
23
|
+
input: unknown
|
|
24
|
+
): string => {
|
|
25
|
+
const values =
|
|
26
|
+
input && typeof input === 'object' ? (input as Record<string, unknown>) : {}
|
|
27
|
+
return template
|
|
28
|
+
.replace(/\{(\w+)\}/g, (_match, key: string) => formatValue(values[key]))
|
|
29
|
+
.replace(/\s+/g, ' ')
|
|
30
|
+
.trim()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const formatValue = (value: unknown): string => {
|
|
34
|
+
if (value === undefined || value === null) {
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === 'object') {
|
|
38
|
+
return JSON.stringify(value)
|
|
39
|
+
}
|
|
40
|
+
return String(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const composeStepProse = ({
|
|
44
|
+
phase,
|
|
45
|
+
description,
|
|
46
|
+
template,
|
|
47
|
+
input,
|
|
48
|
+
actor,
|
|
49
|
+
keywordWidth,
|
|
50
|
+
}: {
|
|
51
|
+
phase: ScenarioStepPhase
|
|
52
|
+
description: string
|
|
53
|
+
/**
|
|
54
|
+
* The prose this step renders, with `{placeholders}` filled from `input`.
|
|
55
|
+
* Unlike `description`, which documents what the step does, this is what a
|
|
56
|
+
* reader of the report sees. Falls back to `description` when absent.
|
|
57
|
+
*/
|
|
58
|
+
template?: string
|
|
59
|
+
/** The input this step was called with, as recorded on the run. */
|
|
60
|
+
input?: unknown
|
|
61
|
+
actor?: string
|
|
62
|
+
/**
|
|
63
|
+
* Pad the keyword to this width so a ladder of steps lines its sentences up
|
|
64
|
+
* under each other. Omit for inline prose.
|
|
65
|
+
*/
|
|
66
|
+
keywordWidth?: number
|
|
67
|
+
}): string => {
|
|
68
|
+
const keyword = phase === 'step' ? '' : capitalise(phase)
|
|
69
|
+
const subject = actor ? `the ${actor}` : ''
|
|
70
|
+
const rendered = template ? renderStepTemplate(template, input) : description
|
|
71
|
+
const sentence = [subject, rendered].filter(Boolean).join(' ')
|
|
72
|
+
if (keywordWidth === undefined) {
|
|
73
|
+
return [keyword, sentence].filter(Boolean).join(' ')
|
|
74
|
+
}
|
|
75
|
+
return `${keyword.padEnd(keywordWidth)} ${sentence}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const capitalise = (value: string) =>
|
|
79
|
+
value.charAt(0).toUpperCase() + value.slice(1)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, test, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
|
|
5
|
+
import {
|
|
6
|
+
PikkuScenarioService,
|
|
7
|
+
createScenarioRunner,
|
|
8
|
+
} from './pikku-scenario-service.js'
|
|
9
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
10
|
+
|
|
11
|
+
const noopLogger = { error() {}, info() {}, warn() {}, debug() {} }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Members that only a scenario run reaches. None of them may live on the class
|
|
15
|
+
* a production app instantiates: a bundler removes unused *modules*, never
|
|
16
|
+
* unused class members, so anything declared here ships in every server built
|
|
17
|
+
* on Pikku whether or not the app has a single scenario.
|
|
18
|
+
*/
|
|
19
|
+
const SCENARIO_MEMBERS = [
|
|
20
|
+
'scenarioStep',
|
|
21
|
+
'runScenarioHook',
|
|
22
|
+
'resolveScenarioActors',
|
|
23
|
+
'setScenarioBrowserProvider',
|
|
24
|
+
'getScenarioBrowserProvider',
|
|
25
|
+
'setScenarioEnvironment',
|
|
26
|
+
'getScenarioEnvironment',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/** Wire members declared by `PikkuScenarioWire` and absent from `PikkuWorkflowWire`. */
|
|
30
|
+
const SCENARIO_WIRE_MEMBERS = [
|
|
31
|
+
'step',
|
|
32
|
+
'given',
|
|
33
|
+
'when',
|
|
34
|
+
'then',
|
|
35
|
+
'expectEventually',
|
|
36
|
+
'expectError',
|
|
37
|
+
'expectService',
|
|
38
|
+
'runScheduledTask',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const setup = async (ws: InMemoryWorkflowService) => {
|
|
42
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
43
|
+
logger: noopLogger,
|
|
44
|
+
} as any)
|
|
45
|
+
const runId = await ws.createRun('surfaceTest', {}, true, 'hash', {
|
|
46
|
+
type: 'test',
|
|
47
|
+
} as any)
|
|
48
|
+
ws.registerInlineRun(runId)
|
|
49
|
+
return runId
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('the production workflow service carries no scenario surface', () => {
|
|
53
|
+
beforeEach(() => resetPikkuState())
|
|
54
|
+
|
|
55
|
+
test('no scenario method is declared on the workflow service prototype chain', () => {
|
|
56
|
+
const ws = new InMemoryWorkflowService()
|
|
57
|
+
for (const member of SCENARIO_MEMBERS) {
|
|
58
|
+
assert.equal(
|
|
59
|
+
typeof (ws as any)[member],
|
|
60
|
+
'undefined',
|
|
61
|
+
`'${member}' ships in every production bundle while it is a member of PikkuWorkflowService`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('the workflow wire exposes only workflow members', async () => {
|
|
67
|
+
const ws = new InMemoryWorkflowService()
|
|
68
|
+
const runId = await setup(ws)
|
|
69
|
+
const wire = ws.createWorkflowWire('surfaceTest', runId, {}) as any
|
|
70
|
+
|
|
71
|
+
for (const member of SCENARIO_WIRE_MEMBERS) {
|
|
72
|
+
assert.equal(
|
|
73
|
+
typeof wire[member],
|
|
74
|
+
'undefined',
|
|
75
|
+
`wire.${member} is a scenario affordance and must not be built for a production workflow run`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
for (const member of ['do', 'sleep', 'suspend', 'approval', 'getRun']) {
|
|
79
|
+
assert.equal(
|
|
80
|
+
typeof wire[member],
|
|
81
|
+
'function',
|
|
82
|
+
`wire.${member} is a workflow member and must survive`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('a service with no extension installed runs a plain workflow untouched', async () => {
|
|
88
|
+
const ws = new InMemoryWorkflowService()
|
|
89
|
+
assert.equal(ws.getRunExtension(), undefined)
|
|
90
|
+
const runId = await setup(ws)
|
|
91
|
+
const wire = ws.createWorkflowWire('surfaceTest', runId, {}) as any
|
|
92
|
+
assert.equal(typeof wire.do, 'function')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('the actor client is not reachable from the workflow service module graph', async () => {
|
|
96
|
+
const { readFileSync } = await import('node:fs')
|
|
97
|
+
const source = readFileSync(
|
|
98
|
+
new URL('./pikku-workflow-service.ts', import.meta.url),
|
|
99
|
+
'utf8'
|
|
100
|
+
)
|
|
101
|
+
assert.ok(
|
|
102
|
+
!source.includes('http-scenario-actors'),
|
|
103
|
+
'PikkuWorkflowService must not reference the HTTP actor client — it pulls the AI persona loop into every bundle'
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('PikkuScenarioService carries the scenario surface', () => {
|
|
109
|
+
beforeEach(() => resetPikkuState())
|
|
110
|
+
|
|
111
|
+
test('every scenario method is declared on the scenario service', () => {
|
|
112
|
+
const { scenarioService } = createScenarioRunner()
|
|
113
|
+
for (const member of SCENARIO_MEMBERS) {
|
|
114
|
+
assert.equal(
|
|
115
|
+
typeof (scenarioService as any)[member],
|
|
116
|
+
'function',
|
|
117
|
+
`PikkuScenarioService is missing '${member}'`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('the wire it decorates carries both the workflow and the scenario members', async () => {
|
|
123
|
+
const { workflowService: ws } = createScenarioRunner()
|
|
124
|
+
const runId = await setup(ws)
|
|
125
|
+
const wire = ws.createWorkflowWire('surfaceTest', runId, {}) as any
|
|
126
|
+
|
|
127
|
+
for (const member of [
|
|
128
|
+
...SCENARIO_WIRE_MEMBERS,
|
|
129
|
+
'do',
|
|
130
|
+
'sleep',
|
|
131
|
+
'approval',
|
|
132
|
+
]) {
|
|
133
|
+
assert.equal(
|
|
134
|
+
typeof wire[member],
|
|
135
|
+
'function',
|
|
136
|
+
`PikkuScenarioService's wire is missing ${member}`
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('it is not a workflow service, so it cannot be registered as one by mistake', () => {
|
|
142
|
+
const { scenarioService } = createScenarioRunner()
|
|
143
|
+
assert.ok(!(scenarioService instanceof InMemoryWorkflowService))
|
|
144
|
+
assert.equal(
|
|
145
|
+
typeof (scenarioService as any).startWorkflow,
|
|
146
|
+
'undefined',
|
|
147
|
+
'the scenario capability is layered onto a run, it does not own one'
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('the runner installs it as the workflow service extension', () => {
|
|
152
|
+
const { workflowService, scenarioService } = createScenarioRunner()
|
|
153
|
+
assert.equal(workflowService.getRunExtension(), scenarioService)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PikkuScenarioStepWire,
|
|
3
|
+
ScenarioEnvironment,
|
|
4
|
+
} from './scenario-step.types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The actor this step was called with, or a loud error naming the step.
|
|
8
|
+
*
|
|
9
|
+
* A step that talks to the target app needs an identity, but `actor` is
|
|
10
|
+
* optional on the wire because a pure assertion step needs none. This is the
|
|
11
|
+
* one place that narrowing happens, so every step file stops writing its own
|
|
12
|
+
* `actorOf(...)` guard.
|
|
13
|
+
*/
|
|
14
|
+
export const requireActor = <TActor>(
|
|
15
|
+
scenarioStep: PikkuScenarioStepWire<TActor> | undefined
|
|
16
|
+
): TActor => {
|
|
17
|
+
const actor = scenarioStep?.actor
|
|
18
|
+
if (!actor) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`[scenario] step '${scenarioStep?.name ?? 'unknown'}' was called without an actor. ` +
|
|
21
|
+
`Pass { actor: actors.<name> } so it runs as that persona.`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return actor
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The environment this run targets, or a loud error naming the step. A run
|
|
29
|
+
* started outside `pikku scenario run` only carries one when the server has
|
|
30
|
+
* `API_URL` configured.
|
|
31
|
+
*/
|
|
32
|
+
export const requireScenarioEnv = (
|
|
33
|
+
scenarioStep: PikkuScenarioStepWire<unknown> | undefined
|
|
34
|
+
): ScenarioEnvironment => {
|
|
35
|
+
const env = scenarioStep?.env
|
|
36
|
+
if (!env) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[scenario] step '${scenarioStep?.name ?? 'unknown'}' needs the target environment, but this run carries none. ` +
|
|
39
|
+
`Run it through 'pikku scenario run <environment>' with scenarios.environments.<environment> declared in pikku.config.json.`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return env
|
|
43
|
+
}
|