@xdelivered/emberflow 0.2.0 → 0.5.0
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/bin/commands.ts +7 -3
- package/bin/init.test.ts +94 -0
- package/bin/init.ts +108 -2
- package/dist/bin/commands.d.ts +1 -0
- package/dist/bin/commands.js +6 -3
- package/dist/bin/init.d.ts +4 -0
- package/dist/bin/init.js +96 -2
- package/dist/server/agents/codexParse.js +31 -0
- package/dist/server/agents/detect.d.ts +18 -8
- package/dist/server/agents/detect.js +78 -13
- package/dist/server/agents/modelRejectionHint.js +1 -1
- package/dist/server/agents/prompt.d.ts +15 -1
- package/dist/server/agents/prompt.js +59 -37
- package/dist/server/agents/runManager.d.ts +23 -2
- package/dist/server/agents/runManager.js +36 -8
- package/dist/server/client.d.ts +1 -0
- package/dist/server/client.js +7 -2
- package/dist/server/index.js +208 -16
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/server/subflowRunner.d.ts +48 -1
- package/dist/server/subflowRunner.js +34 -7
- package/dist/server/updateCheck.d.ts +68 -0
- package/dist/server/updateCheck.js +142 -0
- package/dist/src/engine/executor.js +4 -3
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +28 -7
- package/server/agentRoute.test.ts +20 -0
- package/server/agents/codexParse.test.ts +37 -0
- package/server/agents/codexParse.ts +34 -0
- package/server/agents/detect.test.ts +26 -7
- package/server/agents/detect.ts +82 -14
- package/server/agents/modelRejectionHint.ts +2 -1
- package/server/agents/prompt.test.ts +128 -0
- package/server/agents/prompt.ts +102 -3
- package/server/agents/runManager.test.ts +9 -0
- package/server/agents/runManager.ts +37 -7
- package/server/client.ts +10 -4
- package/server/index.ts +213 -15
- package/server/nodeRun.test.ts +189 -0
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -0
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/server/stepDrill.test.ts +380 -0
- package/server/subflowRunner.ts +92 -11
- package/server/updateCheck.test.ts +209 -0
- package/server/updateCheck.ts +175 -0
- package/server/workflowTestRoute.test.ts +1 -1
- package/src/App.tsx +82 -2
- package/src/components/AgentConsole.tsx +7 -280
- package/src/components/AgentStream.test.tsx +88 -0
- package/src/components/AgentStream.tsx +303 -0
- package/src/components/CreateModal.tsx +43 -9
- package/src/components/Dock.tsx +1 -1
- package/src/components/EmptyState.test.tsx +49 -0
- package/src/components/EmptyState.tsx +61 -0
- package/src/components/EnvironmentDialog.tsx +4 -1
- package/src/components/EnvironmentPicker.tsx +8 -3
- package/src/components/EnvironmentsDialog.tsx +4 -1
- package/src/components/InfraPanel.tsx +30 -2
- package/src/components/InfrastructureDialog.test.tsx +97 -0
- package/src/components/InfrastructureDialog.tsx +111 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/components/StatusBar.tsx +227 -26
- package/src/components/Toolbar.tsx +28 -16
- package/src/components/UpdateChip.test.tsx +31 -0
- package/src/components/WelcomeDialog.test.tsx +384 -13
- package/src/components/WelcomeDialog.tsx +996 -177
- package/src/engine/executor.ts +4 -3
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/lib/guidedQuestions.test.ts +224 -0
- package/src/lib/guidedQuestions.ts +181 -0
- package/src/lib/runbookModel.ts +3 -3
- package/src/lib/runbookProjection.test.ts +43 -0
- package/src/lib/runbookProjection.ts +26 -6
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/agentClient.ts +27 -8
- package/src/store/apiTree.ts +12 -0
- package/src/store/builderStore.test.ts +441 -99
- package/src/store/builderStore.ts +385 -314
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-BbzppDpt.js +65 -0
- package/studio-dist/assets/index-CLf6blcA.css +1 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +92 -9
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { pinsOf, useBuilderStore } from './builderStore';
|
|
3
3
|
import { createLoginFlow } from '../flows/login-flow';
|
|
4
|
-
import { createDefaultRegistry } from '../nodes';
|
|
5
4
|
import * as serverRunner from './serverRunner';
|
|
6
5
|
import * as agentClient from './agentClient';
|
|
7
|
-
import type { WorkflowDefinition
|
|
8
|
-
import type { ScenarioTestReport, ServerRunHandlers } from './serverRunner';
|
|
6
|
+
import type { WorkflowDefinition } from '../engine';
|
|
7
|
+
import type { ScenarioTestReport, ServerRunHandlers, StepResult } from './serverRunner';
|
|
9
8
|
|
|
10
9
|
vi.mock('./serverRunner', async () => {
|
|
11
10
|
const actual = await vi.importActual<typeof import('./serverRunner')>('./serverRunner');
|
|
@@ -22,8 +21,10 @@ vi.mock('./serverRunner', async () => {
|
|
|
22
21
|
runnerHealthy: vi.fn(),
|
|
23
22
|
testWorkflow: vi.fn(),
|
|
24
23
|
startServerRun: vi.fn(),
|
|
24
|
+
stepServerRun: vi.fn(),
|
|
25
25
|
subscribeServerRun: vi.fn(),
|
|
26
26
|
setServingMode: vi.fn(),
|
|
27
|
+
runNodeOnServer: vi.fn(),
|
|
27
28
|
};
|
|
28
29
|
});
|
|
29
30
|
|
|
@@ -173,18 +174,23 @@ describe('builderStore workflows', () => {
|
|
|
173
174
|
|
|
174
175
|
describe('builderStore pinning', () => {
|
|
175
176
|
beforeEach(() => {
|
|
176
|
-
|
|
177
|
+
vi.mocked(serverRunner.startServerRun).mockReset().mockResolvedValue('run-p');
|
|
178
|
+
vi.mocked(serverRunner.subscribeServerRun).mockReset().mockImplementation(() => () => {});
|
|
179
|
+
useBuilderStore.setState({
|
|
180
|
+
flow: createLoginFlow(), run: null, logs: [], activeRun: null,
|
|
181
|
+
activeServerRunId: null, runnerOnline: true,
|
|
182
|
+
});
|
|
177
183
|
});
|
|
178
184
|
|
|
179
|
-
it('pinned
|
|
185
|
+
it('threads pinned outputs to the server run (the runner honours the pin)', async () => {
|
|
180
186
|
useBuilderStore
|
|
181
187
|
.getState()
|
|
182
188
|
.pinNodeOutput('validate', { userId: 'user-pinned', username: 'pinned' });
|
|
183
189
|
await useBuilderStore.getState().runToEnd();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
expect((
|
|
187
|
-
}
|
|
190
|
+
// pins are startServerRun's 3rd arg (flow, mode, pins, input, options).
|
|
191
|
+
const pins = vi.mocked(serverRunner.startServerRun).mock.calls[0][2];
|
|
192
|
+
expect(pins).toEqual({ validate: { userId: 'user-pinned', username: 'pinned' } });
|
|
193
|
+
});
|
|
188
194
|
|
|
189
195
|
it('unpinNode clears the pin and empties metadata', () => {
|
|
190
196
|
useBuilderStore.getState().pinNodeOutput('validate', { userId: 'x' });
|
|
@@ -205,37 +211,66 @@ describe('builderStore pinning', () => {
|
|
|
205
211
|
});
|
|
206
212
|
});
|
|
207
213
|
|
|
208
|
-
describe('builderStore isolated node run', () => {
|
|
214
|
+
describe('builderStore isolated node run (server-backed)', () => {
|
|
209
215
|
beforeEach(() => {
|
|
210
|
-
|
|
216
|
+
vi.mocked(serverRunner.runNodeOnServer).mockReset();
|
|
217
|
+
useBuilderStore.setState({
|
|
218
|
+
flow: createLoginFlow(),
|
|
219
|
+
run: null,
|
|
220
|
+
logs: [],
|
|
221
|
+
activeRun: null,
|
|
222
|
+
runnerOnline: true,
|
|
223
|
+
selectedEnvironment: 'local',
|
|
224
|
+
safeMode: true,
|
|
225
|
+
});
|
|
211
226
|
});
|
|
212
227
|
|
|
213
|
-
it('
|
|
228
|
+
it('calls the runner with the node type, input, config and env, and records a local sample', async () => {
|
|
229
|
+
vi.mocked(serverRunner.runNodeOnServer).mockResolvedValue({
|
|
230
|
+
output: { plan: 'pro' },
|
|
231
|
+
logs: [{ timestamp: 't', level: 'info', runId: 'node-run', message: 'plan pro' }],
|
|
232
|
+
});
|
|
214
233
|
const before = useBuilderStore.getState().trace.samplesFor('checkPlan').length;
|
|
234
|
+
|
|
215
235
|
const result = await useBuilderStore
|
|
216
236
|
.getState()
|
|
217
237
|
.runNodeIsolated('checkPlan', { user: { plan: 'pro' } });
|
|
238
|
+
|
|
218
239
|
expect(result.error).toBeUndefined();
|
|
219
240
|
expect((result.output as { plan: string }).plan).toBe('pro');
|
|
220
241
|
expect(result.logs.some((l) => l.message.includes('pro'))).toBe(true);
|
|
242
|
+
const call = vi.mocked(serverRunner.runNodeOnServer).mock.calls[0][0];
|
|
243
|
+
// Sends the node TYPE (CheckPlan), not the node id (checkPlan).
|
|
244
|
+
expect(call.type).toBe('CheckPlan');
|
|
245
|
+
expect(call.input).toEqual({ user: { plan: 'pro' } });
|
|
246
|
+
expect(call.environment).toBe('local');
|
|
221
247
|
const samples = useBuilderStore.getState().trace.samplesFor('checkPlan');
|
|
222
248
|
expect(samples.length).toBe(before + 1);
|
|
223
249
|
expect(samples[0].input).toEqual({ user: { plan: 'pro' } });
|
|
224
250
|
expect(useBuilderStore.getState().run).toBeNull();
|
|
225
251
|
});
|
|
226
252
|
|
|
227
|
-
it('
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
253
|
+
it('surfaces a runner error as the result error and records a failed sample', async () => {
|
|
254
|
+
vi.mocked(serverRunner.runNodeOnServer).mockResolvedValue({
|
|
255
|
+
error: 'Password too short',
|
|
256
|
+
logs: [],
|
|
257
|
+
});
|
|
233
258
|
const failed = await useBuilderStore
|
|
234
259
|
.getState()
|
|
235
260
|
.runNodeIsolated('validate', { username: 'ada', password: 'x' });
|
|
236
261
|
expect(failed.error).toContain('Password too short');
|
|
237
262
|
expect(useBuilderStore.getState().trace.samplesFor('validate')[0].status).toBe('failed');
|
|
238
263
|
});
|
|
264
|
+
|
|
265
|
+
it('shows the offline notice when the runner is unreachable', async () => {
|
|
266
|
+
useBuilderStore.setState({ runnerOnline: false });
|
|
267
|
+
vi.mocked(serverRunner.runNodeOnServer).mockRejectedValue(new Error('Failed to fetch'));
|
|
268
|
+
const result = await useBuilderStore
|
|
269
|
+
.getState()
|
|
270
|
+
.runNodeIsolated('validate', { username: 'ada', password: 'x' });
|
|
271
|
+
expect(result.error).toContain('Runner offline');
|
|
272
|
+
expect(result.logs).toEqual([]);
|
|
273
|
+
});
|
|
239
274
|
});
|
|
240
275
|
|
|
241
276
|
describe('builderStore runner sync', () => {
|
|
@@ -428,6 +463,8 @@ describe('builderStore run history', () => {
|
|
|
428
463
|
|
|
429
464
|
describe('builderStore scenarios', () => {
|
|
430
465
|
beforeEach(() => {
|
|
466
|
+
vi.mocked(serverRunner.startServerRun).mockReset().mockResolvedValue('run-s');
|
|
467
|
+
vi.mocked(serverRunner.subscribeServerRun).mockReset().mockImplementation(() => () => {});
|
|
431
468
|
// Start from a scenario-free flow: the seeded examples would offset counts.
|
|
432
469
|
const flow = createLoginFlow();
|
|
433
470
|
delete flow.scenarios;
|
|
@@ -436,9 +473,10 @@ describe('builderStore scenarios', () => {
|
|
|
436
473
|
run: null,
|
|
437
474
|
logs: [],
|
|
438
475
|
activeRun: null,
|
|
476
|
+
activeServerRunId: null,
|
|
439
477
|
runHistory: [],
|
|
440
478
|
activeScenarioId: null,
|
|
441
|
-
|
|
479
|
+
runnerOnline: true,
|
|
442
480
|
});
|
|
443
481
|
});
|
|
444
482
|
|
|
@@ -458,7 +496,12 @@ describe('builderStore scenarios', () => {
|
|
|
458
496
|
expect(useBuilderStore.getState().flow.scenarios).toBeUndefined();
|
|
459
497
|
});
|
|
460
498
|
|
|
461
|
-
it('runScenario
|
|
499
|
+
it('runScenario starts a server run with the scenario payload + name and tags history', async () => {
|
|
500
|
+
let capturedHandlers: ServerRunHandlers | undefined;
|
|
501
|
+
vi.mocked(serverRunner.subscribeServerRun).mockImplementation((_runId, handlers) => {
|
|
502
|
+
capturedHandlers = handlers;
|
|
503
|
+
return () => {};
|
|
504
|
+
});
|
|
462
505
|
useBuilderStore
|
|
463
506
|
.getState()
|
|
464
507
|
.addScenario('new user', { username: 'newton', password: 'apple123' });
|
|
@@ -466,25 +509,50 @@ describe('builderStore scenarios', () => {
|
|
|
466
509
|
|
|
467
510
|
await useBuilderStore.getState().runScenario(id);
|
|
468
511
|
|
|
469
|
-
const
|
|
470
|
-
expect(
|
|
471
|
-
expect((
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
512
|
+
const call = vi.mocked(serverRunner.startServerRun).mock.calls[0];
|
|
513
|
+
expect(call[1]).toBe('run');
|
|
514
|
+
expect(call[3]).toEqual({ username: 'newton', password: 'apple123' });
|
|
515
|
+
expect(call[4]).toMatchObject({ scenarioName: 'new user' });
|
|
516
|
+
|
|
517
|
+
const flowId = useBuilderStore.getState().flow.id;
|
|
518
|
+
capturedHandlers!.onFinished({
|
|
519
|
+
id: 'run-s',
|
|
520
|
+
workflowId: flowId,
|
|
521
|
+
status: 'succeeded',
|
|
522
|
+
startedAt: '2026-07-10T10:00:00Z',
|
|
523
|
+
completedAt: '2026-07-10T10:00:01Z',
|
|
524
|
+
nodeStates: {},
|
|
525
|
+
});
|
|
475
526
|
expect(useBuilderStore.getState().runHistory[0].scenarioName).toBe('new user');
|
|
476
|
-
}
|
|
527
|
+
});
|
|
477
528
|
|
|
478
529
|
it('a plain run after a scenario run is not tagged', async () => {
|
|
530
|
+
const handlersByRun: Record<string, ServerRunHandlers> = {};
|
|
531
|
+
vi.mocked(serverRunner.subscribeServerRun).mockImplementation((runId, handlers) => {
|
|
532
|
+
handlersByRun[runId] = handlers;
|
|
533
|
+
return () => {};
|
|
534
|
+
});
|
|
479
535
|
useBuilderStore
|
|
480
536
|
.getState()
|
|
481
537
|
.addScenario('new user', { username: 'newton', password: 'apple123' });
|
|
482
538
|
const id = useBuilderStore.getState().flow.scenarios![0].id;
|
|
539
|
+
const flowId = useBuilderStore.getState().flow.id;
|
|
540
|
+
|
|
541
|
+
vi.mocked(serverRunner.startServerRun).mockResolvedValueOnce('run-a');
|
|
483
542
|
await useBuilderStore.getState().runScenario(id);
|
|
543
|
+
handlersByRun['run-a'].onFinished({
|
|
544
|
+
id: 'run-a', workflowId: flowId, status: 'succeeded',
|
|
545
|
+
startedAt: '2026-07-10T10:00:00Z', completedAt: '2026-07-10T10:00:01Z', nodeStates: {},
|
|
546
|
+
});
|
|
484
547
|
|
|
548
|
+
vi.mocked(serverRunner.startServerRun).mockResolvedValueOnce('run-b');
|
|
485
549
|
await useBuilderStore.getState().runToEnd();
|
|
550
|
+
handlersByRun['run-b'].onFinished({
|
|
551
|
+
id: 'run-b', workflowId: flowId, status: 'succeeded',
|
|
552
|
+
startedAt: '2026-07-10T10:00:02Z', completedAt: '2026-07-10T10:00:03Z', nodeStates: {},
|
|
553
|
+
});
|
|
486
554
|
expect(useBuilderStore.getState().runHistory[0].scenarioName).toBeUndefined();
|
|
487
|
-
}
|
|
555
|
+
});
|
|
488
556
|
});
|
|
489
557
|
|
|
490
558
|
describe('builderStore environments + safe mode', () => {
|
|
@@ -665,6 +733,103 @@ describe('builderStore agent picker', () => {
|
|
|
665
733
|
.runAgent({ action: 'edit-flow', flowId: 'f1', instruction: 'go' }, { model: 'other' });
|
|
666
734
|
expect(agentClient.startAgent).toHaveBeenCalledWith(expect.anything(), { model: 'other' });
|
|
667
735
|
});
|
|
736
|
+
|
|
737
|
+
it('beginInfrastructureScout: a leaked MouseEvent (onClick wiring) falls back to the full-rescan instruction', () => {
|
|
738
|
+
// Regression: the Dock's scout button once passed the click event as
|
|
739
|
+
// `instruction`, crashing on instruction.trim(). Non-strings = full rescan.
|
|
740
|
+
useBuilderStore.getState().beginInfrastructureScout(
|
|
741
|
+
{ type: 'click' } as unknown as string,
|
|
742
|
+
);
|
|
743
|
+
expect(agentClient.startAgent).toHaveBeenCalledWith(
|
|
744
|
+
expect.objectContaining({
|
|
745
|
+
action: 'scout-infrastructure',
|
|
746
|
+
instruction: expect.stringContaining('Scan this project'),
|
|
747
|
+
}),
|
|
748
|
+
expect.anything(),
|
|
749
|
+
);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('beginInfrastructureScout: a real instruction threads verbatim', () => {
|
|
753
|
+
useBuilderStore.getState().beginInfrastructureScout('add our Redis cache');
|
|
754
|
+
expect(agentClient.startAgent).toHaveBeenCalledWith(
|
|
755
|
+
expect.objectContaining({ instruction: 'add our Redis cache' }),
|
|
756
|
+
expect.anything(),
|
|
757
|
+
);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe('builderStore guided setup transcript', () => {
|
|
762
|
+
beforeEach(() => {
|
|
763
|
+
vi.mocked(agentClient.startAgent).mockClear().mockResolvedValue('run-1');
|
|
764
|
+
useBuilderStore.setState({ agentChoice: {}, agentRun: null, guidedTranscript: [] });
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const QUESTION_TEXT =
|
|
768
|
+
'Two things to decide.\n\n' +
|
|
769
|
+
'```emberflow-questions\n{"questions":[{"id":"envs","text":"Which environments?","options":["dev + prod"]}]}\n```\n';
|
|
770
|
+
|
|
771
|
+
it('continuation: appends the prior guided run (question blocks stripped) + a **You:** message', () => {
|
|
772
|
+
useBuilderStore.setState({
|
|
773
|
+
guidedTranscript: [{ type: 'message', text: 'Earlier turn.' }],
|
|
774
|
+
agentRun: {
|
|
775
|
+
id: 'run-0',
|
|
776
|
+
status: 'done',
|
|
777
|
+
guided: true,
|
|
778
|
+
events: [
|
|
779
|
+
{ type: 'message', text: 'Reading the ground truth first.' },
|
|
780
|
+
{ type: 'command', command: 'git status', commandStatus: 'completed' },
|
|
781
|
+
{ type: 'message', text: QUESTION_TEXT },
|
|
782
|
+
],
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
useBuilderStore.getState().beginGuidedSetup('Which environments?: dev + prod');
|
|
787
|
+
|
|
788
|
+
expect(useBuilderStore.getState().guidedTranscript).toEqual([
|
|
789
|
+
{ type: 'message', text: 'Earlier turn.' },
|
|
790
|
+
{ type: 'message', text: 'Reading the ground truth first.' },
|
|
791
|
+
{ type: 'command', command: 'git status', commandStatus: 'completed' },
|
|
792
|
+
// The answered question block is stripped — only the prose survives.
|
|
793
|
+
{ type: 'message', text: 'Two things to decide.' },
|
|
794
|
+
{ type: 'message', text: '**You:** Which environments?: dev + prod' },
|
|
795
|
+
]);
|
|
796
|
+
expect(agentClient.startAgent).toHaveBeenCalledWith(
|
|
797
|
+
{ action: 'guided-setup', instruction: 'Which environments?: dev + prod' },
|
|
798
|
+
expect.anything(),
|
|
799
|
+
);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('fresh start (no notes) clears the transcript', () => {
|
|
803
|
+
useBuilderStore.setState({
|
|
804
|
+
guidedTranscript: [{ type: 'message', text: 'Stale conversation.' }],
|
|
805
|
+
});
|
|
806
|
+
useBuilderStore.getState().beginGuidedSetup();
|
|
807
|
+
expect(useBuilderStore.getState().guidedTranscript).toEqual([]);
|
|
808
|
+
expect(agentClient.startAgent).toHaveBeenCalledWith(
|
|
809
|
+
{ action: 'guided-setup', instruction: '' },
|
|
810
|
+
expect.anything(),
|
|
811
|
+
);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('a leaked MouseEvent (onClick wiring) counts as a fresh start, not a continuation', () => {
|
|
815
|
+
useBuilderStore.setState({
|
|
816
|
+
guidedTranscript: [{ type: 'message', text: 'Stale conversation.' }],
|
|
817
|
+
agentRun: { id: 'run-0', status: 'done', guided: true, events: [] },
|
|
818
|
+
});
|
|
819
|
+
useBuilderStore.getState().beginGuidedSetup({ type: 'click' } as unknown as string);
|
|
820
|
+
expect(useBuilderStore.getState().guidedTranscript).toEqual([]);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('notes without a prior guided run leave the transcript untouched', () => {
|
|
824
|
+
useBuilderStore.setState({
|
|
825
|
+
guidedTranscript: [{ type: 'message', text: 'Kept.' }],
|
|
826
|
+
agentRun: { id: 'run-9', status: 'done', events: [{ type: 'message', text: 'edit-flow run' }] },
|
|
827
|
+
});
|
|
828
|
+
useBuilderStore.getState().beginGuidedSetup('an answer');
|
|
829
|
+
expect(useBuilderStore.getState().guidedTranscript).toEqual([
|
|
830
|
+
{ type: 'message', text: 'Kept.' },
|
|
831
|
+
]);
|
|
832
|
+
});
|
|
668
833
|
});
|
|
669
834
|
|
|
670
835
|
describe('builderStore node sizing', () => {
|
|
@@ -924,75 +1089,8 @@ describe('builderStore run console dismissal', () => {
|
|
|
924
1089
|
// the toolbar's visual reflection of it, is only verifiable in a rendered
|
|
925
1090
|
// browser/component-test environment, not from the store alone.
|
|
926
1091
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
id, type, label: id, position: { x: 0, y: 0 }, config: {}, ...extra,
|
|
930
|
-
});
|
|
931
|
-
const mkFlow = (
|
|
932
|
-
id: string, name: string, nodes: WorkflowNode[], edges: WorkflowDefinition['edges'],
|
|
933
|
-
): WorkflowDefinition => ({
|
|
934
|
-
id, name, version: 1, nodes, edges,
|
|
935
|
-
createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
beforeEach(() => {
|
|
939
|
-
// Delay-free registry so end-to-end runs are fast; browser mode so runs
|
|
940
|
-
// execute in-tab through the store's subflow runner.
|
|
941
|
-
useBuilderStore.setState({
|
|
942
|
-
registry: createDefaultRegistry(0),
|
|
943
|
-
runnerOnline: false,
|
|
944
|
-
executionMode: 'browser',
|
|
945
|
-
run: null,
|
|
946
|
-
logs: [],
|
|
947
|
-
activeRun: null,
|
|
948
|
-
activeServerRunId: null,
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
it('resolves a shelf workflow and forwards its logs with prefixing', async () => {
|
|
953
|
-
const child = mkFlow('child', 'Child', [node('cres', 'Result', { label: 'Result' })], []);
|
|
954
|
-
const parent = mkFlow(
|
|
955
|
-
'parent', 'Parent',
|
|
956
|
-
[
|
|
957
|
-
node('sub', 'Subflow', { config: { workflowId: 'child' } }),
|
|
958
|
-
node('res', 'Result', { inputMap: { data: { sourceNodeId: 'sub', sourceField: '$' } } }),
|
|
959
|
-
],
|
|
960
|
-
[{ id: 'e', source: 'sub', target: 'res', targetHandle: 'data' }],
|
|
961
|
-
);
|
|
962
|
-
useBuilderStore.setState({ flow: parent, shelf: [child] });
|
|
963
|
-
|
|
964
|
-
await useBuilderStore.getState().runToEnd();
|
|
965
|
-
const s = useBuilderStore.getState();
|
|
966
|
-
expect(s.run?.status).toBe('succeeded');
|
|
967
|
-
expect(s.run?.nodeStates.sub.status).toBe('succeeded');
|
|
968
|
-
// The child's Result-collected output flows into the parent's Result.
|
|
969
|
-
expect(s.run?.nodeStates.res.output).toEqual({ data: {} });
|
|
970
|
-
|
|
971
|
-
const forwarded = s.logs.find((l) => l.nodeId === 'sub/cres');
|
|
972
|
-
expect(forwarded, JSON.stringify(s.logs)).toBeDefined();
|
|
973
|
-
expect(forwarded?.nodeLabel).toBe('Child › Result');
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
it('rejects an unknown workflow id', async () => {
|
|
977
|
-
const parent = mkFlow('parent', 'Parent', [node('sub', 'Subflow', { config: { workflowId: 'nope' } })], []);
|
|
978
|
-
useBuilderStore.setState({ flow: parent, shelf: [] });
|
|
979
|
-
await useBuilderStore.getState().runToEnd();
|
|
980
|
-
const s = useBuilderStore.getState();
|
|
981
|
-
expect(s.run?.status).toBe('failed');
|
|
982
|
-
expect(s.run?.nodeStates.sub.error).toBe('Unknown workflow: nope');
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
it('catches an A→B→A cycle at runtime', async () => {
|
|
986
|
-
const a = mkFlow('a', 'A', [node('subA', 'Subflow', { config: { workflowId: 'b' } })], []);
|
|
987
|
-
const b = mkFlow('b', 'B', [node('subB', 'Subflow', { config: { workflowId: 'a' } })], []);
|
|
988
|
-
useBuilderStore.setState({ flow: a, shelf: [b] });
|
|
989
|
-
await useBuilderStore.getState().runToEnd();
|
|
990
|
-
const s = useBuilderStore.getState();
|
|
991
|
-
expect(s.run?.status).toBe('failed');
|
|
992
|
-
// The cycle is reported from deep in the child; its error log is forwarded up.
|
|
993
|
-
expect(s.logs.some((l) => l.message === 'subflow cycle: a → b → a')).toBe(true);
|
|
994
|
-
});
|
|
995
|
-
});
|
|
1092
|
+
// Subflow execution moved server-side (server/subflowRunner.ts, exercised by the
|
|
1093
|
+
// runner's run tests) — the studio no longer runs child workflows in-tab.
|
|
996
1094
|
|
|
997
1095
|
describe('builderStore.syncNodeMeta', () => {
|
|
998
1096
|
afterEach(() => vi.unstubAllGlobals());
|
|
@@ -1097,7 +1195,6 @@ describe('builderStore mock runs (studio wiring)', () => {
|
|
|
1097
1195
|
logs: [],
|
|
1098
1196
|
activeRun: null,
|
|
1099
1197
|
activeServerRunId: null,
|
|
1100
|
-
executionMode: 'server',
|
|
1101
1198
|
runnerOnline: true,
|
|
1102
1199
|
runnerMock: true,
|
|
1103
1200
|
runHistory: [],
|
|
@@ -1174,7 +1271,6 @@ describe('builderStore server-run finished event → errorHandler tag', () => {
|
|
|
1174
1271
|
logs: [],
|
|
1175
1272
|
activeRun: null,
|
|
1176
1273
|
activeServerRunId: null,
|
|
1177
|
-
executionMode: 'server',
|
|
1178
1274
|
runnerOnline: true,
|
|
1179
1275
|
runHistory: [],
|
|
1180
1276
|
activeScenarioId: null,
|
|
@@ -1232,3 +1328,249 @@ describe('builderStore server-run finished event → errorHandler tag', () => {
|
|
|
1232
1328
|
expect(entry?.errorHandler).toBeUndefined();
|
|
1233
1329
|
});
|
|
1234
1330
|
});
|
|
1331
|
+
|
|
1332
|
+
describe('builderStore subflow step drill-in', () => {
|
|
1333
|
+
const drillFlow = (id: string, name: string, nodeIds: string[]): WorkflowDefinition => ({
|
|
1334
|
+
id,
|
|
1335
|
+
name,
|
|
1336
|
+
version: 1,
|
|
1337
|
+
nodes: nodeIds.map((nid, i) => ({
|
|
1338
|
+
id: nid,
|
|
1339
|
+
type: 'Noop',
|
|
1340
|
+
label: nid,
|
|
1341
|
+
position: { x: i * 100, y: 0 },
|
|
1342
|
+
config: {},
|
|
1343
|
+
})),
|
|
1344
|
+
edges: [],
|
|
1345
|
+
createdAt: '2026-07-13T00:00:00Z',
|
|
1346
|
+
updatedAt: '2026-07-13T00:00:00Z',
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
let handlers: ServerRunHandlers | undefined;
|
|
1350
|
+
|
|
1351
|
+
beforeEach(() => {
|
|
1352
|
+
handlers = undefined;
|
|
1353
|
+
vi.mocked(serverRunner.startServerRun).mockReset().mockResolvedValue('run-d');
|
|
1354
|
+
vi.mocked(serverRunner.stepServerRun).mockReset();
|
|
1355
|
+
vi.mocked(serverRunner.subscribeServerRun).mockReset().mockImplementation((_runId, h) => {
|
|
1356
|
+
handlers = h;
|
|
1357
|
+
return () => {};
|
|
1358
|
+
});
|
|
1359
|
+
useBuilderStore.setState({
|
|
1360
|
+
flow: drillFlow('parent', 'Parent Flow', ['start', 'sub', 'finish']),
|
|
1361
|
+
shelf: [drillFlow('child', 'Child Flow', ['c1', 'c2']), drillFlow('grandchild', 'Grandchild Flow', ['g1'])],
|
|
1362
|
+
run: null,
|
|
1363
|
+
logs: [],
|
|
1364
|
+
activeRun: null,
|
|
1365
|
+
activeServerRunId: null,
|
|
1366
|
+
runnerOnline: true,
|
|
1367
|
+
runHistory: [],
|
|
1368
|
+
activeScenarioId: null,
|
|
1369
|
+
stepMode: false,
|
|
1370
|
+
stepDrill: [],
|
|
1371
|
+
drillPeek: null,
|
|
1372
|
+
selectedNodeId: null,
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
/** One Step click: queue the next step response and drive stepRun. */
|
|
1377
|
+
const step = (result: StepResult): Promise<void> => {
|
|
1378
|
+
vi.mocked(serverRunner.stepServerRun).mockResolvedValueOnce(result);
|
|
1379
|
+
return useBuilderStore.getState().stepRun();
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
it('entered pushes a drill level, swaps the view to the child, and routes nodeState by workflowId', async () => {
|
|
1383
|
+
await step({ done: false }); // first parent node
|
|
1384
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1385
|
+
|
|
1386
|
+
let s = useBuilderStore.getState();
|
|
1387
|
+
expect(s.flow.id).toBe('child');
|
|
1388
|
+
expect(s.stepDrill).toHaveLength(1);
|
|
1389
|
+
expect(s.stepDrill[0]).toMatchObject({ workflowId: 'child', viaNodeId: 'sub' });
|
|
1390
|
+
expect(s.stepDrill[0].savedFlow.id).toBe('parent');
|
|
1391
|
+
// Synthetic child run: running, no node states yet (first child node runs
|
|
1392
|
+
// on the NEXT step), same id as the root run so SSE routing applies.
|
|
1393
|
+
expect(s.run?.workflowId).toBe('child');
|
|
1394
|
+
expect(s.run?.status).toBe('running');
|
|
1395
|
+
expect(s.run?.nodeStates).toEqual({});
|
|
1396
|
+
expect(s.run?.id).toBe('run-d');
|
|
1397
|
+
// The live server run and step mode are untouched by drilling.
|
|
1398
|
+
expect(s.activeServerRunId).toBe('run-d');
|
|
1399
|
+
expect(s.stepMode).toBe(true);
|
|
1400
|
+
|
|
1401
|
+
// Child workflowId → lights the visible (child) run.
|
|
1402
|
+
handlers!.onNodeState('child', 'c1', { status: 'running' });
|
|
1403
|
+
expect(useBuilderStore.getState().run?.nodeStates.c1?.status).toBe('running');
|
|
1404
|
+
|
|
1405
|
+
// Parent workflowId → lands on the stashed parent run, not the child view.
|
|
1406
|
+
handlers!.onNodeState('parent', 'sub', { status: 'running' });
|
|
1407
|
+
s = useBuilderStore.getState();
|
|
1408
|
+
expect(s.run?.nodeStates.sub).toBeUndefined();
|
|
1409
|
+
expect(s.stepDrill[0].savedRun?.nodeStates.sub?.status).toBe('running');
|
|
1410
|
+
|
|
1411
|
+
// Unknown workflowId → ignored.
|
|
1412
|
+
handlers!.onNodeState('elsewhere', 'x', { status: 'running' });
|
|
1413
|
+
expect(useBuilderStore.getState().run?.nodeStates.x).toBeUndefined();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
it('drilling does not clear the root run logs', async () => {
|
|
1417
|
+
await step({ done: false });
|
|
1418
|
+
useBuilderStore.setState({
|
|
1419
|
+
logs: [{ timestamp: 't', level: 'info', runId: 'run-d', message: 'parent log' }],
|
|
1420
|
+
});
|
|
1421
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1422
|
+
expect(useBuilderStore.getState().logs.map((l) => l.message)).toEqual(['parent log']);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
it('exited restores the parent view including its updated Subflow node state and selection', async () => {
|
|
1426
|
+
await step({ done: false });
|
|
1427
|
+
useBuilderStore.setState({ selectedNodeId: 'start' });
|
|
1428
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1429
|
+
expect(useBuilderStore.getState().selectedNodeId).toBeNull();
|
|
1430
|
+
|
|
1431
|
+
handlers!.onNodeState('child', 'c1', { status: 'succeeded' });
|
|
1432
|
+
handlers!.onNodeState('parent', 'sub', { status: 'succeeded' });
|
|
1433
|
+
await step({ done: false, exited: true });
|
|
1434
|
+
|
|
1435
|
+
const s = useBuilderStore.getState();
|
|
1436
|
+
expect(s.flow.id).toBe('parent');
|
|
1437
|
+
expect(s.stepDrill).toHaveLength(0);
|
|
1438
|
+
expect(s.run?.workflowId).toBe('parent');
|
|
1439
|
+
expect(s.run?.nodeStates.sub?.status).toBe('succeeded');
|
|
1440
|
+
expect(s.selectedNodeId).toBe('start');
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
it('nested enter/enter/exit/exit walks the stack one level at a time', async () => {
|
|
1444
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1445
|
+
await step({ done: false, entered: { workflowId: 'grandchild', nodeId: 'c2' } });
|
|
1446
|
+
|
|
1447
|
+
let s = useBuilderStore.getState();
|
|
1448
|
+
expect(s.flow.id).toBe('grandchild');
|
|
1449
|
+
expect(s.stepDrill.map((d) => d.workflowId)).toEqual(['child', 'grandchild']);
|
|
1450
|
+
expect(s.stepDrill[1].savedFlow.id).toBe('child');
|
|
1451
|
+
|
|
1452
|
+
// Mid-level (child) states land on its stashed run while two deep.
|
|
1453
|
+
handlers!.onNodeState('child', 'c2', { status: 'running' });
|
|
1454
|
+
s = useBuilderStore.getState();
|
|
1455
|
+
expect(s.stepDrill[1].savedRun?.nodeStates.c2?.status).toBe('running');
|
|
1456
|
+
|
|
1457
|
+
await step({ done: false, exited: true });
|
|
1458
|
+
s = useBuilderStore.getState();
|
|
1459
|
+
expect(s.flow.id).toBe('child');
|
|
1460
|
+
expect(s.stepDrill).toHaveLength(1);
|
|
1461
|
+
expect(s.run?.nodeStates.c2?.status).toBe('running');
|
|
1462
|
+
|
|
1463
|
+
await step({ done: false, exited: true });
|
|
1464
|
+
s = useBuilderStore.getState();
|
|
1465
|
+
expect(s.flow.id).toBe('parent');
|
|
1466
|
+
expect(s.stepDrill).toHaveLength(0);
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it('entered with an unknown child flow id stays on the parent but pushes a placeholder level', async () => {
|
|
1470
|
+
await step({ done: false, entered: { workflowId: 'missing', nodeId: 'sub' } });
|
|
1471
|
+
let s = useBuilderStore.getState();
|
|
1472
|
+
expect(s.flow.id).toBe('parent');
|
|
1473
|
+
// The server's drill stack grew — the client mirrors the depth with a
|
|
1474
|
+
// placeholder so the matching `exited` pops the right level.
|
|
1475
|
+
expect(s.stepDrill).toHaveLength(1);
|
|
1476
|
+
expect(s.stepDrill[0]).toMatchObject({ workflowId: 'missing', viaNodeId: 'sub', placeholder: true });
|
|
1477
|
+
expect(s.stepDrill[0].savedFlow.id).toBe('parent');
|
|
1478
|
+
|
|
1479
|
+
// Parent states keep applying to the visible run as before.
|
|
1480
|
+
handlers!.onNodeState('parent', 'sub', { status: 'running' });
|
|
1481
|
+
expect(useBuilderStore.getState().run?.nodeStates.sub?.status).toBe('running');
|
|
1482
|
+
|
|
1483
|
+
// The matching exited pops the placeholder and leaves the parent view —
|
|
1484
|
+
// including states received while "inside" — untouched.
|
|
1485
|
+
await step({ done: false, exited: true });
|
|
1486
|
+
s = useBuilderStore.getState();
|
|
1487
|
+
expect(s.flow.id).toBe('parent');
|
|
1488
|
+
expect(s.stepDrill).toHaveLength(0);
|
|
1489
|
+
expect(s.run?.nodeStates.sub?.status).toBe('running');
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('exited+entered in one step (Subflow retry) pops the failed child before pushing the fresh one', async () => {
|
|
1493
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1494
|
+
expect(useBuilderStore.getState().flow.id).toBe('child');
|
|
1495
|
+
|
|
1496
|
+
// The retrying Subflow node pops its failed child AND re-enters a new
|
|
1497
|
+
// child run in the same composite step.
|
|
1498
|
+
await step({ done: false, exited: true, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1499
|
+
const s = useBuilderStore.getState();
|
|
1500
|
+
expect(s.flow.id).toBe('child');
|
|
1501
|
+
expect(s.stepDrill).toHaveLength(1);
|
|
1502
|
+
expect(s.stepDrill[0].savedFlow.id).toBe('parent');
|
|
1503
|
+
// Fresh synthetic child run for the retry attempt.
|
|
1504
|
+
expect(s.run?.workflowId).toBe('child');
|
|
1505
|
+
expect(s.run?.nodeStates).toEqual({});
|
|
1506
|
+
|
|
1507
|
+
await step({ done: false, exited: true });
|
|
1508
|
+
expect(useBuilderStore.getState().flow.id).toBe('parent');
|
|
1509
|
+
expect(useBuilderStore.getState().stepDrill).toHaveLength(0);
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
it('exited plus done pops the drill and ends the live run', async () => {
|
|
1513
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1514
|
+
await step({ done: true, exited: true });
|
|
1515
|
+
const s = useBuilderStore.getState();
|
|
1516
|
+
expect(s.flow.id).toBe('parent');
|
|
1517
|
+
expect(s.stepDrill).toHaveLength(0);
|
|
1518
|
+
expect(s.activeServerRunId).toBeNull();
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
it('peek shows an ancestor without popping; child states during the peek still land on the child run', async () => {
|
|
1522
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1523
|
+
|
|
1524
|
+
useBuilderStore.getState().peekDrill(0);
|
|
1525
|
+
let s = useBuilderStore.getState();
|
|
1526
|
+
expect(s.drillPeek).toBe(0);
|
|
1527
|
+
// View-only: the drill stack and the live (deepest) level are untouched —
|
|
1528
|
+
// the runbook derives the peeked flow/run from stepDrill[drillPeek].
|
|
1529
|
+
expect(s.stepDrill).toHaveLength(1);
|
|
1530
|
+
expect(s.flow.id).toBe('child');
|
|
1531
|
+
expect(s.stepDrill[0].savedFlow.id).toBe('parent');
|
|
1532
|
+
|
|
1533
|
+
// Child states arriving during the peek land on the child's (live) run.
|
|
1534
|
+
handlers!.onNodeState('child', 'c1', { status: 'succeeded' });
|
|
1535
|
+
expect(useBuilderStore.getState().run?.nodeStates.c1?.status).toBe('succeeded');
|
|
1536
|
+
// Parent states still land on the stashed parent run.
|
|
1537
|
+
handlers!.onNodeState('parent', 'sub', { status: 'running' });
|
|
1538
|
+
expect(useBuilderStore.getState().stepDrill[0].savedRun?.nodeStates.sub?.status).toBe('running');
|
|
1539
|
+
|
|
1540
|
+
// The last crumb returns to the deepest level.
|
|
1541
|
+
useBuilderStore.getState().peekDrill(null);
|
|
1542
|
+
expect(useBuilderStore.getState().drillPeek).toBeNull();
|
|
1543
|
+
|
|
1544
|
+
// Out-of-range peek indexes clear rather than dangle.
|
|
1545
|
+
useBuilderStore.getState().peekDrill(7);
|
|
1546
|
+
expect(useBuilderStore.getState().drillPeek).toBeNull();
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
it('a further entered while peeking resets the peek to the new deepest level', async () => {
|
|
1550
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1551
|
+
useBuilderStore.getState().peekDrill(0);
|
|
1552
|
+
await step({ done: false, entered: { workflowId: 'grandchild', nodeId: 'c2' } });
|
|
1553
|
+
const s = useBuilderStore.getState();
|
|
1554
|
+
expect(s.drillPeek).toBeNull();
|
|
1555
|
+
expect(s.flow.id).toBe('grandchild');
|
|
1556
|
+
expect(s.stepDrill).toHaveLength(2);
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it('the finished SSE event unwinds any remaining drill before recording the root run', async () => {
|
|
1560
|
+
await step({ done: false, entered: { workflowId: 'child', nodeId: 'sub' } });
|
|
1561
|
+
handlers!.onFinished({
|
|
1562
|
+
id: 'run-d',
|
|
1563
|
+
workflowId: 'parent',
|
|
1564
|
+
status: 'failed',
|
|
1565
|
+
startedAt: '2026-07-13T10:00:00Z',
|
|
1566
|
+
completedAt: '2026-07-13T10:00:01Z',
|
|
1567
|
+
nodeStates: {},
|
|
1568
|
+
});
|
|
1569
|
+
const s = useBuilderStore.getState();
|
|
1570
|
+
expect(s.flow.id).toBe('parent');
|
|
1571
|
+
expect(s.stepDrill).toHaveLength(0);
|
|
1572
|
+
expect(s.run?.id).toBe('run-d');
|
|
1573
|
+
expect(s.run?.workflowId).toBe('parent');
|
|
1574
|
+
expect(s.runHistory[0]?.id).toBe('run-d');
|
|
1575
|
+
});
|
|
1576
|
+
});
|