Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
526 changes: 526 additions & 0 deletions src/__tests__/main/cue/cue-db-integration.test.ts

Large diffs are not rendered by default.

508 changes: 508 additions & 0 deletions src/__tests__/main/cue/cue-engine-integration.test.ts

Large diffs are not rendered by default.

402 changes: 402 additions & 0 deletions src/__tests__/main/cue/cue-fan-in-edge-cases.test.ts

Large diffs are not rendered by default.

454 changes: 454 additions & 0 deletions src/__tests__/main/cue/cue-integration-test-helpers.ts

Large diffs are not rendered by default.

419 changes: 419 additions & 0 deletions src/__tests__/main/cue/cue-race-conditions.test.ts

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions src/__tests__/main/cue/cue-run-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,4 +1116,141 @@ describe('createCueRunManager', () => {
expect(deps.onLog).toHaveBeenCalledWith('cue', expect.stringContaining('Phase 3 skipped'));
});
});

// ─── Phase 15A additions ────────────────────────────────────────────────
// Output-prompt second-phase scenarios that live in the run-manager (the
// executor is a single-phase spawner; the chained "main task → output
// prompt" phase is orchestrated here).

describe('output prompt phase — failure and stop interactions', () => {
it('preserves main-task stdout when the output prompt returns a non-completed status', async () => {
const onCueRun = vi.fn<(req: { subscriptionName: string }) => Promise<CueRunResult>>();
// Call 1 = main task (completes with real stdout). Call 2 = output
// prompt phase, returns failed — run-manager must fall back to the
// main task output and log a warning instead of overwriting the
// result.stdout with the empty output-prompt stdout.
onCueRun
.mockResolvedValueOnce(makeResult({ status: 'completed', stdout: 'MAIN_TASK_OUTPUT' }))
.mockResolvedValueOnce(
makeResult({ status: 'failed', stdout: '', stderr: 'output prompt died' })
);

const deps = createDeps({ onCueRun });
const manager = createCueRunManager(deps);

manager.execute('session-1', 'main-prompt', createEvent(), 'test-sub', 'output-prompt-body');
await vi.advanceTimersByTimeAsync(0);

expect(onCueRun).toHaveBeenCalledTimes(2);
// The second call is the output-prompt phase. The run-manager
// builds `contextPrompt = outputPrompt + "\n---\nContext from
// completed task:\n" + mainStdout` and passes that as the
// `prompt` field. It also stashes the main-task stdout on the
// event payload under `sourceOutput` for downstream chain
// consumers. Verify both channels carry MAIN_TASK_OUTPUT so a
// refactor that drops either one fails fast.
const outputPromptRequest = onCueRun.mock.calls[1][0] as {
subscriptionName: string;
prompt: string;
event: { payload: { sourceOutput?: string; outputPromptPhase?: boolean } };
};
expect(outputPromptRequest.subscriptionName).toBe('test-sub:output');
expect(outputPromptRequest.prompt).toContain('output-prompt-body');
expect(outputPromptRequest.prompt).toContain('MAIN_TASK_OUTPUT');
expect(outputPromptRequest.event.payload.sourceOutput).toBe('MAIN_TASK_OUTPUT');
expect(outputPromptRequest.event.payload.outputPromptPhase).toBe(true);
// onRunCompleted carries the MAIN task output — not the failed
// output-prompt's empty string.
expect(deps.onRunCompleted).toHaveBeenCalledWith(
'session-1',
expect.objectContaining({ stdout: 'MAIN_TASK_OUTPUT', status: 'completed' }),
'test-sub',
undefined
);
// Warning explaining the fallback was surfaced to the activity log.
expect(
(deps.onLog as ReturnType<typeof vi.fn>).mock.calls.some(
(call) =>
call[0] === 'cue' && typeof call[1] === 'string' && /output prompt failed/.test(call[1])
)
).toBe(true);
});

it('survives an output-prompt onCueRun that rejects (exception path)', async () => {
const onCueRun = vi.fn<(req: { subscriptionName: string }) => Promise<CueRunResult>>();
onCueRun
.mockResolvedValueOnce(makeResult({ status: 'completed', stdout: 'MAIN_OK' }))
.mockRejectedValueOnce(new Error('spawn ENOENT'));

const deps = createDeps({ onCueRun });
const manager = createCueRunManager(deps);

manager.execute('session-1', 'main', createEvent(), 'test-sub', 'out-prompt');
await vi.advanceTimersByTimeAsync(0);

// The outer catch treats the rejection as a run failure — main task
// output is discarded because the `await outputResult` line threw
// before the stdout reassignment could happen.
expect(deps.onRunCompleted).toHaveBeenCalledWith(
'session-1',
expect.objectContaining({
status: 'failed',
stderr: expect.stringContaining('spawn ENOENT'),
}),
'test-sub',
undefined
);
});

it('stopRun during output-prompt phase kills BOTH the parent and the output-prompt child process', async () => {
const mainDeferred: { resolve?: (r: CueRunResult) => void } = {};
const outputDeferred: { resolve?: (r: CueRunResult) => void } = {};

const onCueRun = vi.fn((req: { runId: string; subscriptionName: string }) => {
if (req.subscriptionName === 'test-sub') {
return new Promise<CueRunResult>((res) => {
mainDeferred.resolve = (r) => res({ ...r, runId: req.runId });
});
}
return new Promise<CueRunResult>((res) => {
outputDeferred.resolve = (r) => res({ ...r, runId: req.runId });
});
});

const onStopCueRun = vi.fn(() => true);
const deps = createDeps({ onCueRun, onStopCueRun });
const manager = createCueRunManager(deps);

manager.execute('session-1', 'main', createEvent(), 'test-sub', 'out-prompt');
// Let the main task complete; run-manager now dispatches the
// output-prompt phase.
mainDeferred.resolve!(makeResult({ status: 'completed', stdout: 'MAIN_OK' }));
await vi.advanceTimersByTimeAsync(0);
expect(onCueRun).toHaveBeenCalledTimes(2);

// The output-prompt spawn is now in-flight. The active run carries
// the output-prompt child's processRunId so stopRun can signal both.
const parentRunId = manager.getActiveRuns()[0].runId;
const run = manager.getActiveRunMap().get(parentRunId)!;
expect(run.processRunId).toBeDefined();
expect(run.processRunId).not.toBe(parentRunId);

// User hits stop.
const stopped = manager.stopRun(parentRunId);
expect(stopped).toBe(true);
expect(onStopCueRun).toHaveBeenCalledWith(parentRunId);
expect(onStopCueRun).toHaveBeenCalledWith(run.processRunId!);

expect(deps.onRunStopped).toHaveBeenCalledTimes(1);
expect(deps.onRunStopped).toHaveBeenCalledWith(
expect.objectContaining({ status: 'stopped' })
);

// Output-prompt resolves late — the run-manager must skip
// onRunCompleted because stopRun already cleaned up.
outputDeferred.resolve!(makeResult({ status: 'completed', stdout: 'LATE' }));
await vi.advanceTimersByTimeAsync(0);
expect(deps.onRunCompleted).not.toHaveBeenCalled();
});
});
});
Loading