Skip to content
Merged
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
144 changes: 67 additions & 77 deletions ink/main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ const down = '\u001B[B';
// const left = '\u001B[D';
const enter = '\r';

// Ink re-renders asynchronously after stdin writes; polling for the expected
// frame avoids the flakiness of a fixed-duration sleep under CI load.
const waitForFrame = (lastFrame: () => string | undefined, text: string) =>
vi.waitFor(() => expect(lastFrame()).toContain(text));
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If lastFrame() returns undefined (which can happen during initial render or before the first frame is drawn), calling expect(undefined).toContain(text) can throw a TypeError in some test runners because undefined is not iterable/a string. Using the nullish coalescing operator ?? '' ensures that we always perform a string assertion, which provides a much cleaner assertion failure message if the test times out, rather than a generic TypeError.

Suggested change
const waitForFrame = (lastFrame: () => string | undefined, text: string) =>
vi.waitFor(() => expect(lastFrame()).toContain(text));
const waitForFrame = (lastFrame: () => string | undefined, text: string) =>
vi.waitFor(() => expect(lastFrame() ?? '').toContain(text));


// ApiKeyStep renders input as a password field, masking typed characters as
// `*`. Wait for the masked value before submitting so the keystrokes have
// actually reached component state.
const waitForMaskedInput = (lastFrame: () => string | undefined, text: string) =>
vi.waitFor(() => expect(lastFrame()).toContain('*'.repeat(text.length)));
Comment on lines +42 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly to waitForFrame, if lastFrame() returns undefined, calling expect(undefined).toContain(...) can throw a TypeError. Using ?? '' ensures a safe string assertion and cleaner error messages upon timeout.

Suggested change
const waitForMaskedInput = (lastFrame: () => string | undefined, text: string) =>
vi.waitFor(() => expect(lastFrame()).toContain('*'.repeat(text.length)));
const waitForMaskedInput = (lastFrame: () => string | undefined, text: string) =>
vi.waitFor(() => expect(lastFrame() ?? '').toContain('*'.repeat(text.length)));


describe('MainConfig', () => {
const mockExit = vi.fn();

Expand Down Expand Up @@ -74,35 +85,30 @@ describe('MainConfig', () => {
const onComplete = vi.fn();
const { lastFrame, stdin } = render(<MainConfig onComplete={onComplete} />);

const simulateKey = async (char: string) => {
stdin.write(char);
// Wait for re-render
await new Promise((resolve) => setTimeout(resolve, 100));
};

// 1. ProviderStep - Choose OpenAI (default)
expect(lastFrame()).toContain('What model provider would you like to use today?');
await simulateKey(enter);
await waitForFrame(lastFrame, 'What model provider would you like to use today?');
stdin.write(enter);

// 2. ApiKeyStep
expect(lastFrame()).toContain('Can you provide us with your OpenAI API key?');
await simulateKey('sk-test-key');
await simulateKey(enter);
await waitForFrame(lastFrame, 'Can you provide us with your OpenAI API key?');
stdin.write('sk-test-key');
await waitForMaskedInput(lastFrame, 'sk-test-key');
stdin.write(enter);

// 3. ModelSelectionStep - Model
expect(lastFrame()).toContain('What model would you like to use?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model would you like to use?');
stdin.write(enter); // Accept default

// 4. ModelSelectionStep - Compactor
expect(lastFrame()).toContain('What model should we use for memory compaction?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model should we use for memory compaction?');
stdin.write(enter); // Accept default

// 5. EnvironmentSettingsStep
expect(lastFrame()).toContain('Additional Settings');
await simulateKey(enter); // Accept defaults
await waitForFrame(lastFrame, 'Additional Settings');
stdin.write(enter); // Accept defaults

// Verification
expect(onComplete).toHaveBeenCalled();
await vi.waitFor(() => expect(onComplete).toHaveBeenCalled());
expect(vi.mocked(writeFileSync)).toHaveBeenCalled();

// Check process.env
Expand All @@ -115,36 +121,31 @@ describe('MainConfig', () => {
const onComplete = vi.fn();
const { lastFrame, stdin } = render(<MainConfig onComplete={onComplete} />);

const simulateKey = async (char: string) => {
stdin.write(char);
// Wait for re-render
await new Promise((resolve) => setTimeout(resolve, 100));
};

// 1. ProviderStep - Choose Anthropic
expect(lastFrame()).toContain('What model provider would you like to use today?');
await simulateKey(down); // to Anthropic
await simulateKey(enter);
await waitForFrame(lastFrame, 'What model provider would you like to use today?');
stdin.write(down); // to Anthropic
stdin.write(enter);

// 2. ApiKeyStep
expect(lastFrame()).toContain('Can you provide us with your Anthropic API key?');
await simulateKey('sk-ant-test-key');
await simulateKey(enter);
await waitForFrame(lastFrame, 'Can you provide us with your Anthropic API key?');
stdin.write('sk-ant-test-key');
await waitForMaskedInput(lastFrame, 'sk-ant-test-key');
stdin.write(enter);

// 3. ModelSelectionStep - Model
expect(lastFrame()).toContain('What model would you like to use?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model would you like to use?');
stdin.write(enter); // Accept default

// 4. ModelSelectionStep - Compactor
expect(lastFrame()).toContain('What model should we use for memory compaction?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model should we use for memory compaction?');
stdin.write(enter); // Accept default

// 5. EnvironmentSettingsStep
expect(lastFrame()).toContain('Additional Settings');
await simulateKey(enter); // Accept defaults
await waitForFrame(lastFrame, 'Additional Settings');
stdin.write(enter); // Accept defaults

// Verification
expect(onComplete).toHaveBeenCalled();
await vi.waitFor(() => expect(onComplete).toHaveBeenCalled());
expect(vi.mocked(writeFileSync)).toHaveBeenCalled();

// Check process.env
Expand All @@ -157,37 +158,32 @@ describe('MainConfig', () => {
const onComplete = vi.fn();
const { lastFrame, stdin } = render(<MainConfig onComplete={onComplete} />);

const simulateKey = async (char: string) => {
stdin.write(char);
// Wait for re-render
await new Promise((resolve) => setTimeout(resolve, 100));
};

// 1. ProviderStep - Choose Google
expect(lastFrame()).toContain('What model provider would you like to use today?');
await simulateKey(down); // to Anthropic
await simulateKey(down); // to Google
await simulateKey(enter);
await waitForFrame(lastFrame, 'What model provider would you like to use today?');
stdin.write(down); // to Anthropic
stdin.write(down); // to Google
stdin.write(enter);

// 2. ApiKeyStep
expect(lastFrame()).toContain('Can you provide us with your Google API key?');
await simulateKey('google-test-key');
await simulateKey(enter);
await waitForFrame(lastFrame, 'Can you provide us with your Google API key?');
stdin.write('google-test-key');
await waitForMaskedInput(lastFrame, 'google-test-key');
stdin.write(enter);

// 3. ModelSelectionStep - Model
expect(lastFrame()).toContain('What model would you like to use?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model would you like to use?');
stdin.write(enter); // Accept default

// 4. ModelSelectionStep - Compactor
expect(lastFrame()).toContain('What model should we use for memory compaction?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model should we use for memory compaction?');
stdin.write(enter); // Accept default

// 5. EnvironmentSettingsStep
expect(lastFrame()).toContain('Additional Settings');
await simulateKey(enter); // Accept defaults
await waitForFrame(lastFrame, 'Additional Settings');
stdin.write(enter); // Accept defaults

// Verification
expect(onComplete).toHaveBeenCalled();
await vi.waitFor(() => expect(onComplete).toHaveBeenCalled());
expect(vi.mocked(writeFileSync)).toHaveBeenCalled();

// Check process.env
Expand All @@ -200,38 +196,32 @@ describe('MainConfig', () => {
const onComplete = vi.fn();
const { lastFrame, stdin } = render(<MainConfig onComplete={onComplete} />);

const simulateKey = async (char: string) => {
stdin.write(char);
// Wait for re-render
await new Promise((resolve) => setTimeout(resolve, 100));
};

// 1. ProviderStep - Choose Ollama
expect(lastFrame()).toContain('What model provider would you like to use today?');
await waitForFrame(lastFrame, 'What model provider would you like to use today?');

await simulateKey(down); // to Anthropic
await simulateKey(down); // to Google
await simulateKey(down); // to Ollama
await simulateKey(enter);
stdin.write(down); // to Anthropic
stdin.write(down); // to Google
stdin.write(down); // to Ollama
stdin.write(enter);

// 2. ApiUrlStep (since provider is Ollama)
expect(lastFrame()).toContain('Where are you hosting Ollama?');
await simulateKey(enter); // Accept default http://localhost:11434/api
await waitForFrame(lastFrame, 'Where are you hosting Ollama?');
stdin.write(enter); // Accept default http://localhost:11434/api

// 3. ModelSelectionStep - Model
expect(lastFrame()).toContain('What model would you like to use?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model would you like to use?');
stdin.write(enter); // Accept default

// 4. ModelSelectionStep - Compactor
expect(lastFrame()).toContain('What model should we use for memory compaction?');
await simulateKey(enter); // Accept default
await waitForFrame(lastFrame, 'What model should we use for memory compaction?');
stdin.write(enter); // Accept default

// 5. EnvironmentSettingsStep
expect(lastFrame()).toContain('Additional Settings');
await simulateKey(enter); // Accept defaults
await waitForFrame(lastFrame, 'Additional Settings');
stdin.write(enter); // Accept defaults

// Verification
expect(onComplete).toHaveBeenCalled();
await vi.waitFor(() => expect(onComplete).toHaveBeenCalled());
expect(vi.mocked(writeFileSync)).toHaveBeenCalled();

// Check process.env
Expand Down