Skip to content

Commit 6ab9978

Browse files
authored
chore: some new tests to lock down behavior (#373)
PR 1/many. This helps us make sure we preserve behavior while doing le refactoring just tests!
1 parent 5b296a8 commit 6ab9978

3 files changed

Lines changed: 381 additions & 1 deletion

File tree

src/ui/tui/__tests__/flows.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { buildSession, RunPhase } from '../../../lib/wizard-session.js';
2+
import { WizardReadiness } from '../../../lib/health-checks/readiness.js';
3+
import { FLOWS, Flow, Screen } from '../flows.js';
4+
5+
function getEntry(flow: Flow, screen: Screen) {
6+
const entry = FLOWS[flow].find((candidate) => candidate.screen === screen);
7+
if (!entry) {
8+
throw new Error(`Missing flow entry for ${flow}:${screen}`);
9+
}
10+
return entry;
11+
}
12+
13+
describe('FLOWS', () => {
14+
describe('Wizard setup predicate', () => {
15+
it('hides setup when there are no setup questions', () => {
16+
const session = buildSession({});
17+
const entry = getEntry(Flow.Wizard, Screen.Setup);
18+
19+
expect(entry.show?.(session)).toBe(false);
20+
expect(entry.isComplete?.(session)).toBe(true);
21+
});
22+
23+
it('shows setup when framework questions are missing answers', () => {
24+
const session = buildSession({});
25+
const entry = getEntry(Flow.Wizard, Screen.Setup);
26+
27+
session.frameworkConfig = {
28+
metadata: {
29+
setup: {
30+
questions: [{ key: 'packageManager' }, { key: 'srcDir' }],
31+
},
32+
},
33+
} as never;
34+
session.frameworkContext = { packageManager: 'pnpm' };
35+
36+
expect(entry.show?.(session)).toBe(true);
37+
expect(entry.isComplete?.(session)).toBe(false);
38+
});
39+
40+
it('marks setup complete once all required answers are present', () => {
41+
const session = buildSession({});
42+
const entry = getEntry(Flow.Wizard, Screen.Setup);
43+
44+
session.frameworkConfig = {
45+
metadata: {
46+
setup: {
47+
questions: [{ key: 'packageManager' }, { key: 'srcDir' }],
48+
},
49+
},
50+
} as never;
51+
session.frameworkContext = {
52+
packageManager: 'pnpm',
53+
srcDir: 'src',
54+
};
55+
56+
expect(entry.show?.(session)).toBe(false);
57+
expect(entry.isComplete?.(session)).toBe(true);
58+
});
59+
});
60+
61+
describe('Wizard health-check predicate', () => {
62+
it('stays incomplete before readiness exists', () => {
63+
const session = buildSession({});
64+
const entry = getEntry(Flow.Wizard, Screen.HealthCheck);
65+
66+
expect(entry.isComplete?.(session)).toBe(false);
67+
});
68+
69+
it('stays incomplete for blocking readiness until outage is dismissed', () => {
70+
const session = buildSession({});
71+
const entry = getEntry(Flow.Wizard, Screen.HealthCheck);
72+
73+
session.readinessResult = {
74+
decision: WizardReadiness.No,
75+
health: {} as never,
76+
reasons: ['Anthropic: down'],
77+
};
78+
79+
expect(entry.isComplete?.(session)).toBe(false);
80+
81+
session.outageDismissed = true;
82+
83+
expect(entry.isComplete?.(session)).toBe(true);
84+
});
85+
86+
it('completes immediately for non-blocking readiness', () => {
87+
const session = buildSession({});
88+
const entry = getEntry(Flow.Wizard, Screen.HealthCheck);
89+
90+
session.readinessResult = {
91+
decision: WizardReadiness.YesWithWarnings,
92+
health: {} as never,
93+
reasons: [],
94+
};
95+
96+
expect(entry.isComplete?.(session)).toBe(true);
97+
});
98+
});
99+
100+
describe('Wizard run predicate', () => {
101+
it('stays incomplete while run is idle or running', () => {
102+
const session = buildSession({});
103+
const entry = getEntry(Flow.Wizard, Screen.Run);
104+
105+
session.runPhase = RunPhase.Idle;
106+
expect(entry.isComplete?.(session)).toBe(false);
107+
108+
session.runPhase = RunPhase.Running;
109+
expect(entry.isComplete?.(session)).toBe(false);
110+
});
111+
112+
it('completes when run finishes or errors', () => {
113+
const session = buildSession({});
114+
const entry = getEntry(Flow.Wizard, Screen.Run);
115+
116+
session.runPhase = RunPhase.Completed;
117+
expect(entry.isComplete?.(session)).toBe(true);
118+
119+
session.runPhase = RunPhase.Error;
120+
expect(entry.isComplete?.(session)).toBe(true);
121+
});
122+
});
123+
124+
describe('MCP flow predicates', () => {
125+
it('uses mcpComplete for McpAdd', () => {
126+
const session = buildSession({});
127+
const entry = getEntry(Flow.McpAdd, Screen.McpAdd);
128+
129+
expect(entry.isComplete?.(session)).toBe(false);
130+
131+
session.mcpComplete = true;
132+
133+
expect(entry.isComplete?.(session)).toBe(true);
134+
});
135+
136+
it('uses mcpComplete for McpRemove', () => {
137+
const session = buildSession({});
138+
const entry = getEntry(Flow.McpRemove, Screen.McpRemove);
139+
140+
expect(entry.isComplete?.(session)).toBe(false);
141+
142+
session.mcpComplete = true;
143+
144+
expect(entry.isComplete?.(session)).toBe(true);
145+
});
146+
});
147+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { buildSession, RunPhase } from '../../../lib/wizard-session.js';
2+
import { WizardReadiness } from '../../../lib/health-checks/readiness.js';
3+
import { WizardRouter, Flow, Screen, Overlay } from '../router.js';
4+
5+
function baseWizardSession() {
6+
return buildSession({});
7+
}
8+
9+
describe('WizardRouter', () => {
10+
describe('resolve', () => {
11+
it('returns the first incomplete visible screen for the wizard flow', () => {
12+
const router = new WizardRouter(Flow.Wizard);
13+
const session = baseWizardSession();
14+
15+
expect(router.resolve(session)).toBe(Screen.Intro);
16+
17+
session.setupConfirmed = true;
18+
session.readinessResult = {
19+
decision: WizardReadiness.Yes,
20+
health: {} as never,
21+
reasons: [],
22+
};
23+
session.credentials = {
24+
accessToken: 'tok',
25+
projectApiKey: 'pk',
26+
host: 'https://app.posthog.com',
27+
projectId: 1,
28+
};
29+
30+
expect(router.resolve(session)).toBe(Screen.Run);
31+
});
32+
33+
it('skips the setup screen when there are no unanswered framework questions', () => {
34+
const router = new WizardRouter(Flow.Wizard);
35+
const session = baseWizardSession();
36+
37+
session.setupConfirmed = true;
38+
session.readinessResult = {
39+
decision: WizardReadiness.Yes,
40+
health: {} as never,
41+
reasons: [],
42+
};
43+
session.frameworkConfig = {
44+
metadata: {
45+
setup: {
46+
questions: [{ key: 'packageManager' }],
47+
},
48+
},
49+
} as never;
50+
session.frameworkContext = { packageManager: 'pnpm' };
51+
52+
expect(router.resolve(session)).toBe(Screen.Auth);
53+
});
54+
55+
it('returns the last flow screen when every entry is complete', () => {
56+
const router = new WizardRouter(Flow.Wizard);
57+
const session = baseWizardSession();
58+
59+
session.setupConfirmed = true;
60+
session.readinessResult = {
61+
decision: WizardReadiness.Yes,
62+
health: {} as never,
63+
reasons: [],
64+
};
65+
session.credentials = {
66+
accessToken: 'tok',
67+
projectApiKey: 'pk',
68+
host: 'https://app.posthog.com',
69+
projectId: 1,
70+
};
71+
session.runPhase = RunPhase.Completed;
72+
session.mcpComplete = true;
73+
74+
expect(router.resolve(session)).toBe(Screen.Outro);
75+
});
76+
77+
it('gives the topmost overlay precedence over the flow screen', () => {
78+
const router = new WizardRouter(Flow.Wizard);
79+
const session = baseWizardSession();
80+
81+
router.pushOverlay(Overlay.SettingsOverride);
82+
router.pushOverlay(Overlay.AuthError);
83+
84+
expect(router.resolve(session)).toBe(Overlay.AuthError);
85+
86+
router.popOverlay();
87+
expect(router.resolve(session)).toBe(Overlay.SettingsOverride);
88+
});
89+
});
90+
91+
describe('activeScreen', () => {
92+
it('defaults to the first screen in the active flow', () => {
93+
const router = new WizardRouter(Flow.McpRemove);
94+
95+
expect(router.activeScreen).toBe(Screen.McpRemove);
96+
});
97+
98+
it('returns the top overlay when overlays are active', () => {
99+
const router = new WizardRouter(Flow.Wizard);
100+
101+
router.pushOverlay(Overlay.ManagedSettings);
102+
103+
expect(router.activeScreen).toBe(Overlay.ManagedSettings);
104+
});
105+
});
106+
});

0 commit comments

Comments
 (0)