Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions packages/cli/src/ui/utils/goal-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,36 @@ describe('waitForGoalRuntime', () => {
);
});

it('treats a synchronous persistence-unavailable throw as settled', async () => {
// Config.getGoalRuntimeReady() THROWS synchronously (rather than
// rejecting) when chat recording is disabled, e.g. --no-chat-recording.
// The startup gate must swallow that throw exactly like the rejected
// promise above; an escaped rejection kills the AppContainer init effect
// and freezes the TUI on "Connecting to MCP servers..." forever (#10293).
const getGoalRuntimeReady = vi.fn((): Promise<GoalRuntime> => {
throw new GoalPersistenceUnavailableError();
});

await expect(waitForGoalRuntime({ getGoalRuntimeReady })).resolves.toBe(
true,
);
await expect(
waitForGoalRuntime({ getGoalRuntimeReady }, { timeoutMs: 100 }),
).resolves.toBe(true);
expect(getGoalRuntimeReady).toHaveBeenCalledTimes(2);
});

it('does not hide a synchronous throw of any other error', async () => {
const failure = new Error('unsupported Goal lifecycle record');
const getGoalRuntimeReady = vi.fn((): Promise<GoalRuntime> => {
throw failure;
});

await expect(waitForGoalRuntime({ getGoalRuntimeReady })).rejects.toBe(
failure,
);
});

it('resolves true once the runtime settles within the timeout', async () => {
const getGoalRuntimeReady = vi.fn().mockResolvedValue({});
await expect(
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/ui/utils/goal-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ export async function waitForGoalRuntime(
config: Pick<Config, 'getGoalRuntimeReady'>,
options: { timeoutMs?: number } = {},
): Promise<boolean> {
const ready = config.getGoalRuntimeReady();
const awaitReady = async (): Promise<void> => {
try {
await ready;
// The call itself must stay inside the try: with chat recording
// disabled (--no-chat-recording, settings) getGoalRuntimeReady()
// THROWS synchronously instead of rejecting, and an escaped throw
// rejects the AppContainer startup effect, freezing the TUI on the
// init banner forever.
await config.getGoalRuntimeReady();
} catch (error) {
if (!(error instanceof GoalPersistenceUnavailableError)) throw error;
}
Expand Down
Loading