瀏覽代碼

test(continuation): match beta defaults

Alvin Unreal 1 月之前
父節點
當前提交
d36ad202a2
共有 1 個文件被更改,包括 66 次插入61 次删除
  1. 66 61
      src/hooks/task-session-manager/index.test.ts

+ 66 - 61
src/hooks/task-session-manager/index.test.ts

@@ -35,17 +35,13 @@ async function flushChildIdleReconcile(): Promise<void> {
   await new Promise((resolve) => setTimeout(resolve, 5));
 }
 
-function createHook(options?: {
+type HookOptions = {
   shouldManageSession?: (sessionID: string) => boolean;
   registerSessionAsOrchestrator?: (sessionID: string) => void;
   readContextMinLines?: number;
   readContextMaxFiles?: number;
   strategy?: 'latest' | 'checkpoint-compatible';
   maxRetainedSnapshots?: number;
-  /**
-   * Keeps continuation behavior tests focused on the opt-in beta path;
-   * production defaults are exercised by the direct factory test below.
-   */
   continueOnIdle?: boolean;
   backgroundJobBoard?: BackgroundJobBoard;
   sessionStatus?: unknown;
@@ -53,7 +49,9 @@ function createHook(options?: {
   idleReconcileDelayMs?: number;
   isFallbackInProgress?: (sessionID: string) => boolean;
   coordinator?: SessionLifecycle;
-}) {
+};
+
+function createHook(options?: HookOptions) {
   const hook = createTaskSessionManagerHook(
     {
       client: {
@@ -72,7 +70,7 @@ function createHook(options?: {
       strategy: options?.strategy,
       readContextMinLines: options?.readContextMinLines,
       readContextMaxFiles: options?.readContextMaxFiles,
-      continueOnIdle: options?.continueOnIdle ?? true,
+      continueOnIdle: options?.continueOnIdle ?? false,
       backgroundJobBoard: options?.backgroundJobBoard,
       shouldManageSession: options?.shouldManageSession ?? (() => true),
       registerSessionAsOrchestrator: options?.registerSessionAsOrchestrator,
@@ -85,6 +83,13 @@ function createHook(options?: {
   return { hook };
 }
 
+function createContinuationHook(options?: HookOptions) {
+  return createHook({
+    ...options,
+    continueOnIdle: options?.continueOnIdle ?? true,
+  });
+}
+
 function createMessages(sessionID: string, text = 'user message') {
   return {
     messages: [
@@ -3555,7 +3560,7 @@ describe('task-session-manager hook', () => {
 
   test('nudges once for incomplete todos when parent and children are inactive', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       continueOnIdle: true,
       idleReconcileDelayMs: 0,
       sessionClient: {
@@ -3583,7 +3588,7 @@ describe('task-session-manager hook', () => {
 
   test('paired idle events submit at most one continuation', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -3720,7 +3725,7 @@ describe('task-session-manager hook', () => {
           resolvePrompt = resolve;
         }),
     );
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -3744,7 +3749,7 @@ describe('task-session-manager hook', () => {
     expect(promptAsync).toHaveBeenCalledTimes(1);
 
     await hook.event({ event: { type: 'server.instance.disposed' } });
-    const { hook: nextHook } = createHook({
+    const { hook: nextHook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -3768,7 +3773,7 @@ describe('task-session-manager hook', () => {
     const promptAsync = mock(async () => {
       throw new Error('prompt rejected');
     });
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -3808,7 +3813,7 @@ describe('task-session-manager hook', () => {
         }),
     );
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -3916,7 +3921,7 @@ describe('task-session-manager hook', () => {
       promptAsync,
     };
     let fallbackInProgress = false;
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       isFallbackInProgress: () => fallbackInProgress,
       sessionClient,
@@ -3955,7 +3960,7 @@ describe('task-session-manager hook', () => {
       if (todoCalls === 1) return { data: undefined };
       return { data: [{ status: 'pending' }] };
     });
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -3989,7 +3994,7 @@ describe('task-session-manager hook', () => {
       }
       return { data: {} };
     });
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4015,7 +4020,7 @@ describe('task-session-manager hook', () => {
   test('does not evaluate or nudge while a question or permission waits', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4049,7 +4054,7 @@ describe('task-session-manager hook', () => {
   test('does not evaluate or nudge after wait_for_user requests text-only HITL', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4071,7 +4076,7 @@ describe('task-session-manager hook', () => {
 
   test('a distinct external user message releases wait_for_user', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4104,7 +4109,7 @@ describe('task-session-manager hook', () => {
   test('a duplicate external message cannot clear a newer user wait', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4146,7 +4151,7 @@ describe('task-session-manager hook', () => {
   test('synthetic and internal messages do not clear wait_for_user', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4183,7 +4188,7 @@ describe('task-session-manager hook', () => {
   test('a foreground-fallback replay marker does not clear wait_for_user', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4222,7 +4227,7 @@ describe('task-session-manager hook', () => {
     const children = mock(async () => ({ data: [] }));
     const status = mock(async () => ({ data: {} }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: { todo, children, status, promptAsync },
     });
@@ -4248,7 +4253,7 @@ describe('task-session-manager hook', () => {
         }),
     );
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4286,11 +4291,11 @@ describe('task-session-manager hook', () => {
       status: mock(async () => ({ data: {} })),
       promptAsync,
     };
-    const owner = createHook({
+    const owner = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient,
     }).hook;
-    const waiter = createHook({
+    const waiter = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient,
     }).hook;
@@ -4310,7 +4315,7 @@ describe('task-session-manager hook', () => {
 
   test('external user input clears only the explicit wait while a question remains', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4367,7 +4372,7 @@ describe('task-session-manager hook', () => {
       promptAsync,
     };
     const makeHook = () =>
-      createHook({ idleReconcileDelayMs: 0, sessionClient }).hook;
+      createContinuationHook({ idleReconcileDelayMs: 0, sessionClient }).hook;
     const owner = makeHook();
 
     owner.beginUserWait('parent-1');
@@ -4395,7 +4400,7 @@ describe('task-session-manager hook', () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
     let fallbackInProgress = true;
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       isFallbackInProgress: () => fallbackInProgress,
       sessionClient: {
@@ -4425,7 +4430,7 @@ describe('task-session-manager hook', () => {
     const children = mock(async () => ({ data: [] }));
     const status = mock(async () => ({ data: {} }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: { todo, children, status, promptAsync },
     });
@@ -4450,7 +4455,7 @@ describe('task-session-manager hook', () => {
   test('fails closed when an id-less ask races a scheduled continuation', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4491,7 +4496,7 @@ describe('task-session-manager hook', () => {
 
   test('clears only the resolved input wait and resumes on a later idle', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4566,7 +4571,7 @@ describe('task-session-manager hook', () => {
 
   test('resumes on a later idle after a question rejection', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4609,7 +4614,7 @@ describe('task-session-manager hook', () => {
         }),
     );
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4640,7 +4645,7 @@ describe('task-session-manager hook', () => {
   test('internal and synthetic messages do not clear an input wait', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4682,7 +4687,7 @@ describe('task-session-manager hook', () => {
   test('retains input waits across a session error', async () => {
     const todo = mock(async () => ({ data: [{ status: 'pending' }] }));
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -4727,7 +4732,7 @@ describe('task-session-manager hook', () => {
       },
     ] as const) {
       const promptAsync = mock(async () => ({}));
-      const { hook } = createHook({
+      const { hook } = createContinuationHook({
         idleReconcileDelayMs: 0,
         sessionClient: {
           todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4756,7 +4761,7 @@ describe('task-session-manager hook', () => {
   test('coalesces paired idle events and suppresses active children', async () => {
     const promptAsync = mock(async () => ({}));
     const children = mock(async () => ({ data: [{ id: 'child-1' }] }));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4783,7 +4788,7 @@ describe('task-session-manager hook', () => {
 
   test('runtime-shaped external messages rearm a consumed nudge', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4818,7 +4823,7 @@ describe('task-session-manager hook', () => {
 
   test('output.message.id rearms when input.messageID is missing', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4854,7 +4859,7 @@ describe('task-session-manager hook', () => {
 
   test('ID-less output.message object identity rearms once', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4946,7 +4951,7 @@ describe('task-session-manager hook', () => {
 
   test('distinct ID-less message objects each open a new epoch', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -4992,7 +4997,7 @@ describe('task-session-manager hook', () => {
 
   test('missing id and output.message fails closed without rearm', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5153,7 +5158,7 @@ describe('task-session-manager hook', () => {
 
   test('file-only external messages rearm a consumed nudge', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5188,7 +5193,7 @@ describe('task-session-manager hook', () => {
 
   test('synthetic completion messages do not rearm a consumed nudge', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5229,7 +5234,7 @@ describe('task-session-manager hook', () => {
 
   test('nudge busy-to-idle cycle does not send a second unchanged nudge', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5266,7 +5271,7 @@ describe('task-session-manager hook', () => {
         }),
     );
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo,
@@ -5298,7 +5303,7 @@ describe('task-session-manager hook', () => {
     const board = new BackgroundJobBoard();
     setupCompletedJob(board);
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       backgroundJobBoard: board,
       idleReconcileDelayMs: 0,
       sessionClient: {
@@ -5323,7 +5328,7 @@ describe('task-session-manager hook', () => {
 
   test('missing SDK response data fails closed without nudging', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: undefined })),
@@ -5343,7 +5348,7 @@ describe('task-session-manager hook', () => {
 
   test('does not nudge when todos are completed or cancelled only', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({
@@ -5365,7 +5370,7 @@ describe('task-session-manager hook', () => {
 
   test('does not nudge while the parent is active', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5385,7 +5390,7 @@ describe('task-session-manager hook', () => {
 
   test('does not nudge while a child is retrying', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5407,7 +5412,7 @@ describe('task-session-manager hook', () => {
 
   test('does not rearm a consumed nudge for its actual internal part', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5444,7 +5449,7 @@ describe('task-session-manager hook', () => {
     const promptAsync = mock(async () => {
       throw new Error('prompt failed');
     });
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5468,7 +5473,7 @@ describe('task-session-manager hook', () => {
 
   test('keeps a failed prompt response consumed', async () => {
     const promptAsync = mock(async () => ({ error: 'prompt failed' }));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => ({ data: [{ status: 'pending' }] })),
@@ -5492,12 +5497,12 @@ describe('task-session-manager hook', () => {
 
   test('fails closed for missing or throwing SDK endpoints', async () => {
     const missingPrompt = mock(async () => ({}));
-    const { hook: missingHook } = createHook({
+    const { hook: missingHook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: { promptAsync: missingPrompt },
     });
     const throwingPrompt = mock(async () => ({}));
-    const { hook: throwingHook } = createHook({
+    const { hook: throwingHook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => {
@@ -5522,7 +5527,7 @@ describe('task-session-manager hook', () => {
 
   test('does not nudge when fallback is already in progress', async () => {
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       isFallbackInProgress: () => true,
       sessionClient: {
@@ -5550,7 +5555,7 @@ describe('task-session-manager hook', () => {
       },
     );
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       isFallbackInProgress: () => fallbackInProgress,
       sessionClient: {
@@ -5580,7 +5585,7 @@ describe('task-session-manager hook', () => {
       releaseLatestChildren = () => resolve({ data: [] });
     });
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       backgroundJobBoard: board,
       idleReconcileDelayMs: 0,
       sessionClient: {
@@ -5613,7 +5618,7 @@ describe('task-session-manager hook', () => {
       },
     );
     const promptAsync = mock(async () => ({}));
-    const { hook } = createHook({
+    const { hook } = createContinuationHook({
       idleReconcileDelayMs: 0,
       sessionClient: {
         todo: mock(async () => todos),