Skip to content

Commit 260a2c1

Browse files
committed
feat: Phase 3 question & routing
- parseQuestionConfig for extracting question blocks - routeFromQuestion for routing based on user response - shouldLoopStep for max-iter safety - getNextStepIndex for combined routing logic - Handle No* (editable) responses
1 parent e9f9ff8 commit 260a2c1

1 file changed

Lines changed: 173 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* Question & Routing tests
3+
*/
4+
5+
import { describe, test, expect } from 'bun:test';
6+
import {
7+
parseLiterateMarkdown,
8+
parseQuestionConfig,
9+
routeFromQuestion,
10+
shouldLoopStep,
11+
type Step
12+
} from './literate-commands';
13+
14+
// ============================================================================
15+
// Test Data
16+
// ============================================================================
17+
18+
const QUESTION_STEP = `---
19+
\`\`\`yaml {config}
20+
step: approval
21+
question:
22+
title: Do you approve the plan?
23+
options:
24+
Yes: collect
25+
No*: refine
26+
\`\`\`
27+
Please approve or refine.
28+
`;
29+
30+
const LOOP_STEP = `---
31+
\`\`\`yaml {config}
32+
step: refine
33+
next: approval
34+
max-iter: 3
35+
\`\`\`
36+
Refine the plan based on feedback.
37+
`;
38+
39+
const LINEAR_STEP = `---
40+
\`\`\`yaml {config}
41+
step: step1
42+
next: step2
43+
\`\`\`
44+
Do step 1.
45+
`;
46+
47+
const LINEAR_STEP_2 = `---
48+
\`\`\`yaml {config}
49+
step: step2
50+
\`\`\`
51+
Do step 2.
52+
`;
53+
54+
// ============================================================================
55+
// Tests
56+
// ============================================================================
57+
58+
describe('parseQuestionConfig', () => {
59+
test('extracts question config from step', () => {
60+
const steps = parseLiterateMarkdown(QUESTION_STEP);
61+
const step = steps[0];
62+
63+
const config = parseQuestionConfig(step.config);
64+
65+
expect(config).toBeDefined();
66+
expect(config?.title).toBe('Do you approve the plan?');
67+
expect(config?.options).toEqual({
68+
'Yes': 'collect',
69+
'No*': 'refine'
70+
});
71+
});
72+
73+
test('returns null for steps without question', () => {
74+
const steps = parseLiterateMarkdown(LINEAR_STEP);
75+
const step = steps[0];
76+
77+
const config = parseQuestionConfig(step.config);
78+
79+
expect(config).toBeNull();
80+
});
81+
});
82+
83+
describe('routeFromQuestion', () => {
84+
test('routes based on Yes response', () => {
85+
const steps = parseLiterateMarkdown(QUESTION_STEP);
86+
const step = steps[0];
87+
88+
const nextStep = routeFromQuestion(step.config, 'Yes');
89+
90+
expect(nextStep).toBe('collect');
91+
});
92+
93+
test('routes based on No response', () => {
94+
const steps = parseLiterateMarkdown(QUESTION_STEP);
95+
const step = steps[0];
96+
97+
const nextStep = routeFromQuestion(step.config, 'No');
98+
99+
expect(nextStep).toBe('refine');
100+
});
101+
102+
test('returns null for unknown response', () => {
103+
const steps = parseLiterateMarkdown(QUESTION_STEP);
104+
const step = steps[0];
105+
106+
const nextStep = routeFromQuestion(step.config, 'Maybe');
107+
108+
expect(nextStep).toBeNull();
109+
});
110+
});
111+
112+
describe('shouldLoopStep', () => {
113+
test('returns false for steps without max-iter', () => {
114+
const steps = parseLiterateMarkdown(QUESTION_STEP);
115+
const step = steps[0];
116+
117+
const result = shouldLoopStep(step.config, 0);
118+
119+
expect(result).toBe(false);
120+
});
121+
122+
test('returns false when max-iter exceeded', () => {
123+
const steps = parseLiterateMarkdown(LOOP_STEP);
124+
const step = steps[0];
125+
126+
// At iteration 3, should not loop (max is 3)
127+
const result = shouldLoopStep(step.config, 3);
128+
129+
expect(result).toBe(false);
130+
});
131+
132+
test('returns true when under max-iter', () => {
133+
const steps = parseLiterateMarkdown(LOOP_STEP);
134+
const step = steps[0];
135+
136+
// At iteration 2, should loop
137+
const result = shouldLoopStep(step.config, 2);
138+
139+
expect(result).toBe(true);
140+
});
141+
});
142+
143+
describe('routing flow', () => {
144+
test('linear routing via next:', () => {
145+
const steps = parseLiterateMarkdown(LINEAR_STEP);
146+
const step1 = steps[0];
147+
148+
expect(step1.config.next).toBe('step2');
149+
});
150+
151+
test('routing integration - approval loop', () => {
152+
// Simulate approval → No → refine → approval → Yes → collect
153+
const approvalStep = { config: {
154+
step: 'approval',
155+
question: {
156+
title: 'Approve?',
157+
options: { 'Yes': 'collect', 'No*': 'refine' }
158+
}
159+
}};
160+
161+
// User says No
162+
let next = routeFromQuestion(approvalStep.config, 'No');
163+
expect(next).toBe('refine');
164+
165+
// After refine, next is approval (loop)
166+
const refineStep = { config: { step: 'refine', next: 'approval', maxIter: 3 }};
167+
expect(refineStep.config.next).toBe('approval');
168+
169+
// Loop check
170+
expect(shouldLoopStep(refineStep.config, 0)).toBe(true);
171+
expect(shouldLoopStep(refineStep.config, 3)).toBe(false); // Max reached
172+
});
173+
});

0 commit comments

Comments
 (0)