diff --git a/packages/cli/src/ui/utils/goal-runtime.test.ts b/packages/cli/src/ui/utils/goal-runtime.test.ts index f07a3e38725..77b9c22839e 100644 --- a/packages/cli/src/ui/utils/goal-runtime.test.ts +++ b/packages/cli/src/ui/utils/goal-runtime.test.ts @@ -28,31 +28,43 @@ describe('waitForGoalRuntime', () => { expect(getGoalRuntimeReady).toHaveBeenCalledTimes(1); }); - it('does not hide malformed or unsupported persisted Goal state', async () => { - const failure = new Error('unsupported Goal lifecycle record'); - const getGoalRuntimeReady = vi.fn().mockRejectedValue(failure); - - await expect(waitForGoalRuntime({ getGoalRuntimeReady })).rejects.toBe( - failure, - ); - }); - - it('allows Goal-less sessions when readiness throws synchronously', async () => { + 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 the init banner forever (#10311). const getGoalRuntimeReady = vi.fn((): Promise => { 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 synchronous readiness errors', async () => { + 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 => { throw failure; }); + await expect(waitForGoalRuntime({ getGoalRuntimeReady })).rejects.toBe( + failure, + ); + await expect( + waitForGoalRuntime({ getGoalRuntimeReady }, { timeoutMs: 100 }), + ).rejects.toBe(failure); + }); + + it('does not hide malformed or unsupported persisted Goal state', async () => { + const failure = new Error('unsupported Goal lifecycle record'); + const getGoalRuntimeReady = vi.fn().mockRejectedValue(failure); + await expect(waitForGoalRuntime({ getGoalRuntimeReady })).rejects.toBe( failure, ); diff --git a/packages/cli/src/ui/utils/goal-runtime.ts b/packages/cli/src/ui/utils/goal-runtime.ts index 373f3d57737..3c8810b725b 100644 --- a/packages/cli/src/ui/utils/goal-runtime.ts +++ b/packages/cli/src/ui/utils/goal-runtime.ts @@ -56,6 +56,11 @@ export async function waitForGoalRuntime( ): Promise { const awaitReady = async (): Promise => { try { + // The call must stay inside the try: with chat recording disabled + // (--no-chat-recording, settings) getGoalRuntimeReady() THROWS + // synchronously instead of rejecting, and an escaped rejection kills + // the AppContainer init effect, freezing the TUI on the init banner + // forever. await config.getGoalRuntimeReady(); } catch (error) { if (!(error instanceof GoalPersistenceUnavailableError)) throw error; diff --git a/packages/core/src/agents/team/teamHelpers.test.ts b/packages/core/src/agents/team/teamHelpers.test.ts index 5c46931c721..9a0d23e3a28 100644 --- a/packages/core/src/agents/team/teamHelpers.test.ts +++ b/packages/core/src/agents/team/teamHelpers.test.ts @@ -53,9 +53,7 @@ vi.mock('../../config/storage.js', async (importOriginal) => { // otherwise the real readFile runs. vi.mock('node:fs/promises', async (importOriginal) => { const actual = await importOriginal(); - type ReadFileHook = ( - ...args: Parameters - ) => unknown; + type ReadFileHook = (...args: Parameters) => unknown; let readFileHook: ReadFileHook | undefined; return { ...actual,