@@ -5,6 +5,7 @@ import { SessionManager } from "@earendil-works/pi-coding-agent";
55import {
66 abortAgentHarnessRun ,
77 embeddedAgentLog ,
8+ invokeNativeHookRelay ,
89 nativeHookRelayTesting ,
910 onAgentEvent ,
1011 queueAgentHarnessMessage ,
@@ -860,6 +861,7 @@ describe("runCodexAppServerAttempt", () => {
860861 await closeCodexSandboxExecServersForTests ( ) ;
861862 resetCodexAppServerClientFactoryForTest ( ) ;
862863 testing . resetOpenClawCodingToolsFactoryForTests ( ) ;
864+ testing . clearPendingCodexNativeHookRelayUnregistersForTests ( ) ;
863865 resetCodexRateLimitCacheForTests ( ) ;
864866 nativeHookRelayTesting . clearNativeHookRelaysForTests ( ) ;
865867 clearPluginCommands ( ) ;
@@ -6777,6 +6779,8 @@ describe("runCodexAppServerAttempt", () => {
67776779 expect ( preToolUseState ?. enabled ) . toBe ( true ) ;
67786780 expect ( preToolUseState ?. trusted_hash ) . toMatch ( / ^ s h a 2 5 6 : [ a - f 0 - 9 ] { 64 } $ / ) ;
67796781 const relayId = extractRelayIdFromThreadRequest ( startRequest ?. params ) ;
6782+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeDefined ( ) ;
6783+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
67806784 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
67816785 } ) ;
67826786
@@ -6839,6 +6843,7 @@ describe("runCodexAppServerAttempt", () => {
68396843
68406844 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
68416845 await run ;
6846+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
68426847 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
68436848 } ) ;
68446849
@@ -6942,6 +6947,7 @@ describe("runCodexAppServerAttempt", () => {
69426947
69436948 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
69446949 await run ;
6950+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
69456951 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
69466952 } ) ;
69476953
@@ -7004,6 +7010,7 @@ describe("runCodexAppServerAttempt", () => {
70047010
70057011 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
70067012 await run ;
7013+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
70077014 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
70087015 } ) ;
70097016
@@ -7034,6 +7041,7 @@ describe("runCodexAppServerAttempt", () => {
70347041
70357042 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
70367043 await run ;
7044+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
70377045 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
70387046 } ) ;
70397047
@@ -7066,6 +7074,7 @@ describe("runCodexAppServerAttempt", () => {
70667074
70677075 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
70687076 await run ;
7077+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
70697078 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
70707079 } ) ;
70717080
@@ -7099,6 +7108,7 @@ describe("runCodexAppServerAttempt", () => {
70997108
71007109 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
71017110 await run ;
7111+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
71027112 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
71037113 } ) ;
71047114
@@ -7140,6 +7150,7 @@ describe("runCodexAppServerAttempt", () => {
71407150 await harness . completeTurn ( { threadId : "thread-1" , turnId : "turn-1" } ) ;
71417151 completed = true ;
71427152 await run ;
7153+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
71437154 expect (
71447155 nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ,
71457156 ) . toBeUndefined ( ) ;
@@ -7152,7 +7163,7 @@ describe("runCodexAppServerAttempt", () => {
71527163 }
71537164 } ) ;
71547165
7155- it ( "reuses the Codex native hook relay id across runs for the same session " , async ( ) => {
7166+ it ( "keeps a replacement Codex native hook relay registered when prior cleanup is pending " , async ( ) => {
71567167 const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
71577168 const workspaceDir = path . join ( tempDir , "workspace" ) ;
71587169 const firstHarness = createStartedThreadHarness ( ) ;
@@ -7171,9 +7182,22 @@ describe("runCodexAppServerAttempt", () => {
71717182 ( request ) => request . method === "thread/start" ,
71727183 ) ;
71737184 const firstRelayId = extractRelayIdFromThreadRequest ( firstStartRequest ?. params ) ;
7174- expect (
7175- nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( firstRelayId ) ,
7176- ) . toBeUndefined ( ) ;
7185+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( firstRelayId ) ?. runId ) . toBe (
7186+ "run-1" ,
7187+ ) ;
7188+ await expect (
7189+ invokeNativeHookRelay ( {
7190+ provider : "codex" ,
7191+ relayId : firstRelayId ,
7192+ event : "pre_tool_use" ,
7193+ rawPayload : {
7194+ hook_event_name : "PreToolUse" ,
7195+ tool_name : "Bash" ,
7196+ tool_use_id : "late-call-1" ,
7197+ tool_input : { command : "python3 -c 'print(\"x\")'" } ,
7198+ } ,
7199+ } ) ,
7200+ ) . resolves . toMatchObject ( { exitCode : 0 } ) ;
71777201
71787202 const secondHarness = createResumeHarness ( ) ;
71797203 const secondParams = createParams ( sessionFile , workspaceDir ) ;
@@ -7196,8 +7220,17 @@ describe("runCodexAppServerAttempt", () => {
71967220 expect ( resumedRegistration ?. runId ) . toBe ( "run-2" ) ;
71977221 expect ( resumedRegistration ?. allowedEvents ) . toEqual ( [ "pre_tool_use" ] ) ;
71987222
7223+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
7224+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( firstRelayId ) ?. runId ) . toBe (
7225+ "run-2" ,
7226+ ) ;
7227+
71997228 await secondHarness . completeTurn ( { threadId : "thread-existing" , turnId : "turn-1" } ) ;
72007229 await secondRun ;
7230+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( firstRelayId ) ?. runId ) . toBe (
7231+ "run-2" ,
7232+ ) ;
7233+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
72017234 expect (
72027235 nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( firstRelayId ) ,
72037236 ) . toBeUndefined ( ) ;
@@ -7215,6 +7248,13 @@ describe("runCodexAppServerAttempt", () => {
72157248 expect ( relayId ) . not . toContain ( "cu-pr-relay-smoke" ) ;
72167249 } ) ;
72177250
7251+ it ( "extends native hook relay cleanup grace for configured hook timeouts" , ( ) => {
7252+ expect ( testing . resolveCodexNativeHookRelayUnregisterGraceMs ( undefined ) ) . toBe ( 10_000 ) ;
7253+ expect ( testing . resolveCodexNativeHookRelayUnregisterGraceMs ( 5 ) ) . toBe ( 10_000 ) ;
7254+ expect ( testing . resolveCodexNativeHookRelayUnregisterGraceMs ( 9 ) ) . toBe ( 14_000 ) ;
7255+ expect ( testing . resolveCodexNativeHookRelayUnregisterGraceMs ( 60 ) ) . toBe ( 65_000 ) ;
7256+ } ) ;
7257+
72187258 it ( "sends clearing Codex native hook config when the relay is disabled" , async ( ) => {
72197259 const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
72207260 const workspaceDir = path . join ( tempDir , "workspace" ) ;
@@ -7455,6 +7495,20 @@ describe("runCodexAppServerAttempt", () => {
74557495
74567496 expect ( result . aborted ) . toBe ( true ) ;
74577497 expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
7498+ await expect (
7499+ invokeNativeHookRelay ( {
7500+ provider : "codex" ,
7501+ relayId,
7502+ event : "pre_tool_use" ,
7503+ rawPayload : {
7504+ hook_event_name : "PreToolUse" ,
7505+ tool_name : "Bash" ,
7506+ tool_input : { command : "pnpm test" } ,
7507+ } ,
7508+ } ) ,
7509+ ) . rejects . toThrow ( "native hook relay not found" ) ;
7510+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
7511+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
74587512 } ) ;
74597513
74607514 it ( "refreshes Codex account rate limits when a failed turn omits reset details" , async ( ) => {
@@ -8229,18 +8283,20 @@ describe("runCodexAppServerAttempt", () => {
82298283 expect ( result . timedOut ) . toBe ( false ) ;
82308284 } ) ;
82318285
8232- it ( "releases completion when Codex raw-events an interrupted turn marker" , async ( ) => {
8286+ it ( "releases completion and native hook relay state when Codex raw-events an interrupted turn marker" , async ( ) => {
82338287 const harness = createStartedThreadHarness ( ) ;
82348288 const run = runCodexAppServerAttempt (
82358289 createParams ( path . join ( tempDir , "session.jsonl" ) , path . join ( tempDir , "workspace" ) ) ,
8236- { turnTerminalIdleTimeoutMs : 60_000 } ,
8290+ { nativeHookRelay : { enabled : true } , turnTerminalIdleTimeoutMs : 60_000 } ,
82378291 ) ;
82388292 let resolved = false ;
82398293 void run . then ( ( ) => {
82408294 resolved = true ;
82418295 } ) ;
82428296
82438297 await harness . waitForMethod ( "turn/start" ) ;
8298+ const startRequest = harness . requests . find ( ( request ) => request . method === "thread/start" ) ;
8299+ const relayId = extractRelayIdFromThreadRequest ( startRequest ?. params ) ;
82448300 await harness . notify ( {
82458301 method : "rawResponseItem/completed" ,
82468302 params : {
@@ -8266,6 +8322,61 @@ describe("runCodexAppServerAttempt", () => {
82668322 expect ( result . timedOut ) . toBe ( false ) ;
82678323 expect ( result . promptError ) . toBeNull ( ) ;
82688324 expect ( harness . request . mock . calls . some ( ( [ method ] ) => method === "turn/interrupt" ) ) . toBe ( false ) ;
8325+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
8326+ await expect (
8327+ invokeNativeHookRelay ( {
8328+ provider : "codex" ,
8329+ relayId,
8330+ event : "pre_tool_use" ,
8331+ rawPayload : {
8332+ hook_event_name : "PreToolUse" ,
8333+ tool_name : "Bash" ,
8334+ tool_input : { command : "pnpm test" } ,
8335+ } ,
8336+ } ) ,
8337+ ) . rejects . toThrow ( "native hook relay not found" ) ;
8338+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
8339+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
8340+ } ) ;
8341+
8342+ it ( "cleans up native hook relay state when Codex completes the turn as interrupted" , async ( ) => {
8343+ const harness = createStartedThreadHarness ( ) ;
8344+ const run = runCodexAppServerAttempt (
8345+ createParams ( path . join ( tempDir , "session.jsonl" ) , path . join ( tempDir , "workspace" ) ) ,
8346+ { nativeHookRelay : { enabled : true } , turnTerminalIdleTimeoutMs : 60_000 } ,
8347+ ) ;
8348+
8349+ await harness . waitForMethod ( "turn/start" ) ;
8350+ const startRequest = harness . requests . find ( ( request ) => request . method === "thread/start" ) ;
8351+ const relayId = extractRelayIdFromThreadRequest ( startRequest ?. params ) ;
8352+ await harness . notify ( {
8353+ method : "turn/completed" ,
8354+ params : {
8355+ threadId : "thread-1" ,
8356+ turnId : "turn-1" ,
8357+ turn : { id : "turn-1" , status : "interrupted" , items : [ ] } ,
8358+ } ,
8359+ } ) ;
8360+
8361+ const result = await run ;
8362+ expect ( result . aborted ) . toBe ( false ) ;
8363+ expect ( result . timedOut ) . toBe ( false ) ;
8364+ expect ( result . promptError ) . toBeNull ( ) ;
8365+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
8366+ await expect (
8367+ invokeNativeHookRelay ( {
8368+ provider : "codex" ,
8369+ relayId,
8370+ event : "pre_tool_use" ,
8371+ rawPayload : {
8372+ hook_event_name : "PreToolUse" ,
8373+ tool_name : "Bash" ,
8374+ tool_input : { command : "pnpm test" } ,
8375+ } ,
8376+ } ) ,
8377+ ) . rejects . toThrow ( "native hook relay not found" ) ;
8378+ testing . flushPendingCodexNativeHookRelayUnregistersForTests ( ) ;
8379+ expect ( nativeHookRelayTesting . getNativeHookRelayRegistrationForTests ( relayId ) ) . toBeUndefined ( ) ;
82698380 } ) ;
82708381
82718382 it ( "keeps upstream cancellation aborted when Codex completes the turn as interrupted" , async ( ) => {
0 commit comments