Skip to content
Draft
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
61 changes: 61 additions & 0 deletions src/lib/__tests__/agent-interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,61 @@ describe('runAgent', () => {
// ui.log.error should NOT have been called (errors suppressed for user)
expect(mockUIInstance.log.error).not.toHaveBeenCalled();
});

it('passes resume to the SDK and returns the active session id', async () => {
function* mockGeneratorWithSessionId() {
yield {
type: 'system',
subtype: 'init',
model: 'claude-opus-4-5-20251101',
tools: [],
mcp_servers: [],
session_id: 'session-123',
};

yield {
type: 'assistant',
session_id: 'session-123',
message: {
content: [{ type: 'text', text: '[STATUS] Working through queue' }],
},
};

yield {
type: 'result',
subtype: 'success',
is_error: false,
session_id: 'session-123',
result: 'Queued step complete',
};
}

mockQuery.mockReturnValue(mockGeneratorWithSessionId());

const result = await runAgent(
defaultAgentConfig,
'queued prompt',
defaultOptions,
mockSpinner as unknown as SpinnerHandle,
{
successMessage: 'Queued success',
resumeSessionId: 'session-123',
requestRemark: false,
captureOutputText: true,
captureSessionId: true,
},
);

expect(mockQuery).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
resume: 'session-123',
}),
}),
);
expect(result.sessionId).toBe('session-123');
expect(result.outputText).toContain('Queued step complete');
});
});
});

Expand Down Expand Up @@ -382,4 +437,10 @@ describe('createStopHook', () => {
expect(first).toHaveProperty('decision', 'block');
expect((first as { reason: string }).reason).toContain('WIZARD-REMARK');
});

it('can skip the remark collection phase for intermediate queued steps', () => {
const hook = createStopHook([], [], { requestRemark: false });

expect(hook(hookInput)).toEqual({});
});
});
19 changes: 19 additions & 0 deletions src/lib/__tests__/agent-runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { extractInstalledSkillId } from '../agent-runner';

describe('extractInstalledSkillId', () => {
it('extracts the installed skill id from bootstrap output', () => {
const output = `
Bootstrap complete.
[WIZARD-SKILL-ID] integration-nextjs-app-router
Waiting for next queued step.
`;

expect(extractInstalledSkillId(output)).toBe(
'integration-nextjs-app-router',
);
});

it('returns null when the marker is missing', () => {
expect(extractInstalledSkillId('No marker present')).toBeNull();
});
});
182 changes: 182 additions & 0 deletions src/lib/__tests__/workflow-queue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import {
WizardWorkflowQueue,
createInitialWizardWorkflowQueue,
createPostBootstrapQueue,
parseWorkflowStepsFromSkillMd,
type WorkflowStepSeed,
} from '../workflow-queue';

const BASIC_INTEGRATION_STEPS: WorkflowStepSeed[] = [
{ stepId: '1.0-begin', referenceFilename: 'basic-integration-1.0-begin.md' },
{ stepId: '1.1-edit', referenceFilename: 'basic-integration-1.1-edit.md' },
{
stepId: '1.2-revise',
referenceFilename: 'basic-integration-1.2-revise.md',
},
{
stepId: '1.3-conclude',
referenceFilename: 'basic-integration-1.3-conclude.md',
},
];

describe('WizardWorkflowQueue', () => {
it('seeds a queue from workflow steps in the expected order', () => {
const queue = createInitialWizardWorkflowQueue(BASIC_INTEGRATION_STEPS);

expect(queue.toArray()).toEqual([
{ id: 'bootstrap', kind: 'bootstrap' },
{
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
},
{
id: 'workflow:1.1-edit',
kind: 'workflow',
referenceFilename: 'basic-integration-1.1-edit.md',
},
{
id: 'workflow:1.2-revise',
kind: 'workflow',
referenceFilename: 'basic-integration-1.2-revise.md',
},
{
id: 'workflow:1.3-conclude',
kind: 'workflow',
referenceFilename: 'basic-integration-1.3-conclude.md',
},
{ id: 'env-vars', kind: 'env-vars' },
]);
});

it('builds a queue from arbitrary steps, not just basic-integration', () => {
const customSteps: WorkflowStepSeed[] = [
{ stepId: 'setup', referenceFilename: 'feature-flags-setup.md' },
{ stepId: 'verify', referenceFilename: 'feature-flags-verify.md' },
];
const queue = createInitialWizardWorkflowQueue(customSteps);

expect(queue.toArray()).toEqual([
{ id: 'bootstrap', kind: 'bootstrap' },
{
id: 'workflow:setup',
kind: 'workflow',
referenceFilename: 'feature-flags-setup.md',
},
{
id: 'workflow:verify',
kind: 'workflow',
referenceFilename: 'feature-flags-verify.md',
},
{ id: 'env-vars', kind: 'env-vars' },
]);
});

it('createPostBootstrapQueue omits bootstrap', () => {
const queue = createPostBootstrapQueue(BASIC_INTEGRATION_STEPS);
const items = queue.toArray();

expect(items[0]).toEqual({
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
});
expect(items[items.length - 1]).toEqual({
id: 'env-vars',
kind: 'env-vars',
});
expect(items.find((i) => i.id === 'bootstrap')).toBeUndefined();
});

it('supports enqueue and dequeue operations', () => {
const queue = new WizardWorkflowQueue();

queue.enqueue({ id: 'bootstrap', kind: 'bootstrap' });
queue.enqueue({
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
});

expect(queue.peek()).toEqual({ id: 'bootstrap', kind: 'bootstrap' });
expect(queue.dequeue()).toEqual({ id: 'bootstrap', kind: 'bootstrap' });
expect(queue).toHaveLength(1);
expect(queue.dequeue()).toEqual({
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
});
expect(queue).toHaveLength(0);
});
});

describe('parseWorkflowStepsFromSkillMd', () => {
it('parses workflow steps from SKILL.md frontmatter', () => {
const skillMd = `---
name: integration-nextjs-app-router
description: PostHog integration for Next.js App Router applications
metadata:
author: PostHog
version: dev
workflow:
- step_id: 1.0-begin
reference: basic-integration-1.0-begin.md
title: PostHog Setup - Begin
next:
- basic-integration-1.1-edit.md
- step_id: 1.1-edit
reference: basic-integration-1.1-edit.md
title: PostHog Setup - Edit
next:
- basic-integration-1.2-revise.md
- step_id: 1.2-revise
reference: basic-integration-1.2-revise.md
title: PostHog Setup - Revise
next:
- basic-integration-1.3-conclude.md
- step_id: 1.3-conclude
reference: basic-integration-1.3-conclude.md
title: PostHog Setup - Conclusion
next: []
---

# PostHog integration for Next.js App Router
`;

const steps = parseWorkflowStepsFromSkillMd(skillMd);

expect(steps).toEqual([
{
stepId: '1.0-begin',
referenceFilename: 'basic-integration-1.0-begin.md',
},
{
stepId: '1.1-edit',
referenceFilename: 'basic-integration-1.1-edit.md',
},
{
stepId: '1.2-revise',
referenceFilename: 'basic-integration-1.2-revise.md',
},
{
stepId: '1.3-conclude',
referenceFilename: 'basic-integration-1.3-conclude.md',
},
]);
});

it('returns empty array when no frontmatter', () => {
expect(parseWorkflowStepsFromSkillMd('# No frontmatter')).toEqual([]);
});

it('returns empty array when no workflow key', () => {
const skillMd = `---
name: feature-flags-nextjs
description: docs only
---

# Feature flags
`;
expect(parseWorkflowStepsFromSkillMd(skillMd)).toEqual([]);
});
});
Loading
Loading