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
147 changes: 147 additions & 0 deletions src/ui/tui/__tests__/flows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { buildSession, RunPhase } from '../../../lib/wizard-session.js';
import { WizardReadiness } from '../../../lib/health-checks/readiness.js';
import { FLOWS, Flow, Screen } from '../flows.js';

function getEntry(flow: Flow, screen: Screen) {
const entry = FLOWS[flow].find((candidate) => candidate.screen === screen);
if (!entry) {
throw new Error(`Missing flow entry for ${flow}:${screen}`);
}
return entry;
}

describe('FLOWS', () => {
describe('Wizard setup predicate', () => {
it('hides setup when there are no setup questions', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.Setup);

expect(entry.show?.(session)).toBe(false);
expect(entry.isComplete?.(session)).toBe(true);
});

it('shows setup when framework questions are missing answers', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.Setup);

session.frameworkConfig = {
metadata: {
setup: {
questions: [{ key: 'packageManager' }, { key: 'srcDir' }],
},
},
} as never;
session.frameworkContext = { packageManager: 'pnpm' };

expect(entry.show?.(session)).toBe(true);
expect(entry.isComplete?.(session)).toBe(false);
});

it('marks setup complete once all required answers are present', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.Setup);

session.frameworkConfig = {
metadata: {
setup: {
questions: [{ key: 'packageManager' }, { key: 'srcDir' }],
},
},
} as never;
session.frameworkContext = {
packageManager: 'pnpm',
srcDir: 'src',
};

expect(entry.show?.(session)).toBe(false);
expect(entry.isComplete?.(session)).toBe(true);
});
});

describe('Wizard health-check predicate', () => {
it('stays incomplete before readiness exists', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.HealthCheck);

expect(entry.isComplete?.(session)).toBe(false);
});

it('stays incomplete for blocking readiness until outage is dismissed', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.HealthCheck);

session.readinessResult = {
decision: WizardReadiness.No,
health: {} as never,
reasons: ['Anthropic: down'],
};

expect(entry.isComplete?.(session)).toBe(false);

session.outageDismissed = true;

expect(entry.isComplete?.(session)).toBe(true);
});

it('completes immediately for non-blocking readiness', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.HealthCheck);

session.readinessResult = {
decision: WizardReadiness.YesWithWarnings,
health: {} as never,
reasons: [],
};

expect(entry.isComplete?.(session)).toBe(true);
});
});

describe('Wizard run predicate', () => {
it('stays incomplete while run is idle or running', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.Run);

session.runPhase = RunPhase.Idle;
expect(entry.isComplete?.(session)).toBe(false);

session.runPhase = RunPhase.Running;
expect(entry.isComplete?.(session)).toBe(false);
});

it('completes when run finishes or errors', () => {
const session = buildSession({});
const entry = getEntry(Flow.Wizard, Screen.Run);

session.runPhase = RunPhase.Completed;
expect(entry.isComplete?.(session)).toBe(true);

session.runPhase = RunPhase.Error;
expect(entry.isComplete?.(session)).toBe(true);
});
});

describe('MCP flow predicates', () => {
it('uses mcpComplete for McpAdd', () => {
const session = buildSession({});
const entry = getEntry(Flow.McpAdd, Screen.McpAdd);

expect(entry.isComplete?.(session)).toBe(false);

session.mcpComplete = true;

expect(entry.isComplete?.(session)).toBe(true);
});

it('uses mcpComplete for McpRemove', () => {
const session = buildSession({});
const entry = getEntry(Flow.McpRemove, Screen.McpRemove);

expect(entry.isComplete?.(session)).toBe(false);

session.mcpComplete = true;

expect(entry.isComplete?.(session)).toBe(true);
});
});
});
106 changes: 106 additions & 0 deletions src/ui/tui/__tests__/router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { buildSession, RunPhase } from '../../../lib/wizard-session.js';
import { WizardReadiness } from '../../../lib/health-checks/readiness.js';
import { WizardRouter, Flow, Screen, Overlay } from '../router.js';

function baseWizardSession() {
return buildSession({});
}

describe('WizardRouter', () => {
describe('resolve', () => {
it('returns the first incomplete visible screen for the wizard flow', () => {
const router = new WizardRouter(Flow.Wizard);
const session = baseWizardSession();

expect(router.resolve(session)).toBe(Screen.Intro);

session.setupConfirmed = true;
session.readinessResult = {
decision: WizardReadiness.Yes,
health: {} as never,
reasons: [],
};
session.credentials = {
accessToken: 'tok',
projectApiKey: 'pk',
host: 'https://app.posthog.com',
projectId: 1,
};

expect(router.resolve(session)).toBe(Screen.Run);
});

it('skips the setup screen when there are no unanswered framework questions', () => {
const router = new WizardRouter(Flow.Wizard);
const session = baseWizardSession();

session.setupConfirmed = true;
session.readinessResult = {
decision: WizardReadiness.Yes,
health: {} as never,
reasons: [],
};
session.frameworkConfig = {
metadata: {
setup: {
questions: [{ key: 'packageManager' }],
},
},
} as never;
session.frameworkContext = { packageManager: 'pnpm' };

expect(router.resolve(session)).toBe(Screen.Auth);
});

it('returns the last flow screen when every entry is complete', () => {
const router = new WizardRouter(Flow.Wizard);
const session = baseWizardSession();

session.setupConfirmed = true;
session.readinessResult = {
decision: WizardReadiness.Yes,
health: {} as never,
reasons: [],
};
session.credentials = {
accessToken: 'tok',
projectApiKey: 'pk',
host: 'https://app.posthog.com',
projectId: 1,
};
session.runPhase = RunPhase.Completed;
session.mcpComplete = true;

expect(router.resolve(session)).toBe(Screen.Outro);
});

it('gives the topmost overlay precedence over the flow screen', () => {
const router = new WizardRouter(Flow.Wizard);
const session = baseWizardSession();

router.pushOverlay(Overlay.SettingsOverride);
router.pushOverlay(Overlay.AuthError);

expect(router.resolve(session)).toBe(Overlay.AuthError);

router.popOverlay();
expect(router.resolve(session)).toBe(Overlay.SettingsOverride);
});
});

describe('activeScreen', () => {
it('defaults to the first screen in the active flow', () => {
const router = new WizardRouter(Flow.McpRemove);

expect(router.activeScreen).toBe(Screen.McpRemove);
});

it('returns the top overlay when overlays are active', () => {
const router = new WizardRouter(Flow.Wizard);

router.pushOverlay(Overlay.ManagedSettings);

expect(router.activeScreen).toBe(Overlay.ManagedSettings);
});
});
});
Loading
Loading