@@ -632,6 +632,30 @@ describe("runtime rotation proxy", () => {
632632 ] ) ;
633633 } ) ;
634634
635+ it ( "rejects anonymous blocked thread goal fallbacks instead of sharing state" , async ( ) => {
636+ const now = Date . now ( ) ;
637+ const accountManager = new AccountManager ( undefined , createStorage ( now ) ) ;
638+ const { calls, fetchImpl } = createRecordingFetch (
639+ ( ) =>
640+ new Response ( "<html>blocked</html>" , {
641+ status : HTTP_STATUS . FORBIDDEN ,
642+ headers : { "content-type" : "text/html" } ,
643+ } ) ,
644+ ) ;
645+ const proxy = await startProxy ( { accountManager, fetchImpl } ) ;
646+
647+ const response = await postThreadGoal ( proxy , { goal : "ship it" } , "/thread/goal/set" ) ;
648+
649+ expect ( response . status ) . toBe ( HTTP_STATUS . BAD_REQUEST ) ;
650+ expect ( await response . json ( ) ) . toEqual ( {
651+ error : {
652+ message : "Thread goal fallback requires a thread_id, threadId, or session header." ,
653+ code : "thread_goal_session_key_required" ,
654+ } ,
655+ } ) ;
656+ expect ( calls ) . toHaveLength ( 1 ) ;
657+ } ) ;
658+
635659 it ( "keys blocked GET thread goal fallbacks by query thread id" , async ( ) => {
636660 const now = Date . now ( ) ;
637661 const accountManager = new AccountManager ( undefined , createStorage ( now ) ) ;
@@ -659,6 +683,88 @@ describe("runtime rotation proxy", () => {
659683 ] ) ;
660684 } ) ;
661685
686+ it ( "passes through non-fallback thread goal client errors" , async ( ) => {
687+ const now = Date . now ( ) ;
688+ const accountManager = new AccountManager ( undefined , createStorage ( now ) ) ;
689+ const recordSuccessSpy = vi . spyOn ( accountManager , "recordSuccess" ) ;
690+ const { calls, fetchImpl } = createRecordingFetch (
691+ ( ) =>
692+ new Response ( '{"error":{"code":"bad_goal"}}\n' , {
693+ status : HTTP_STATUS . BAD_REQUEST ,
694+ headers : { "content-type" : "application/json" } ,
695+ } ) ,
696+ ) ;
697+ const proxy = await startProxy ( { accountManager, fetchImpl } ) ;
698+
699+ const response = await postThreadGoal (
700+ proxy ,
701+ { threadId : "thread-1" , goal : "" } ,
702+ "/thread/goal/set" ,
703+ ) ;
704+
705+ expect ( response . status ) . toBe ( HTTP_STATUS . BAD_REQUEST ) ;
706+ expect ( await response . text ( ) ) . toBe ( '{"error":{"code":"bad_goal"}}\n' ) ;
707+ expect ( calls ) . toHaveLength ( 1 ) ;
708+ expect ( recordSuccessSpy ) . not . toHaveBeenCalled ( ) ;
709+ } ) ;
710+
711+ it ( "rejects unauthenticated thread goal requests" , async ( ) => {
712+ const now = Date . now ( ) ;
713+ const accountManager = new AccountManager ( undefined , createStorage ( now ) ) ;
714+ const { calls, fetchImpl } = createRecordingFetch (
715+ ( ) => new Response ( '{"ok":true}' , { status : HTTP_STATUS . OK } ) ,
716+ ) ;
717+ const proxy = await startProxy ( { accountManager, fetchImpl } ) ;
718+
719+ const response = await postThreadGoal (
720+ proxy ,
721+ { threadId : "thread-1" } ,
722+ "/thread/goal/get" ,
723+ { authorization : "Bearer caller-token" , "x-api-key" : "caller-key" } ,
724+ ) ;
725+
726+ expect ( response . status ) . toBe ( HTTP_STATUS . UNAUTHORIZED ) ;
727+ expect ( calls ) . toHaveLength ( 0 ) ;
728+ } ) ;
729+
730+ it ( "isolates local thread goal fallback state across concurrent threads" , async ( ) => {
731+ const now = Date . now ( ) ;
732+ const accountManager = new AccountManager ( undefined , createStorage ( now ) ) ;
733+ const { calls, fetchImpl } = createRecordingFetch (
734+ ( ) =>
735+ new Response ( "<html>blocked</html>" , {
736+ status : HTTP_STATUS . FORBIDDEN ,
737+ headers : { "content-type" : "text/html" } ,
738+ } ) ,
739+ ) ;
740+ const proxy = await startProxy ( { accountManager, fetchImpl } ) ;
741+
742+ const [ setA , setB ] = await Promise . all ( [
743+ postThreadGoal ( proxy , { threadId : "thread-a" , goal : "goal-a" } , "/thread/goal/set" ) ,
744+ postThreadGoal ( proxy , { threadId : "thread-b" , goal : "goal-b" } , "/thread/goal/set" ) ,
745+ ] ) ;
746+ const [ getA , getB ] = await Promise . all ( [
747+ postThreadGoal ( proxy , { threadId : "thread-a" } , "/thread/goal/get" ) ,
748+ postThreadGoal ( proxy , { threadId : "thread-b" } , "/thread/goal/get" ) ,
749+ ] ) ;
750+
751+ expect ( setA . status ) . toBe ( HTTP_STATUS . OK ) ;
752+ expect ( setB . status ) . toBe ( HTTP_STATUS . OK ) ;
753+ expect ( await getA . json ( ) ) . toEqual ( { goal : "goal-a" } ) ;
754+ expect ( await getB . json ( ) ) . toEqual ( { goal : "goal-b" } ) ;
755+ expect ( calls ) . toHaveLength ( 4 ) ;
756+ expect (
757+ calls . filter (
758+ ( call ) => call . url === "https://example.test/backend-api/codex/thread/goal/set" ,
759+ ) ,
760+ ) . toHaveLength ( 2 ) ;
761+ expect (
762+ calls . filter (
763+ ( call ) => call . url === "https://example.test/backend-api/codex/thread/goal/get" ,
764+ ) ,
765+ ) . toHaveLength ( 2 ) ;
766+ } ) ;
767+
662768 it ( "rejects unauthenticated model discovery requests" , async ( ) => {
663769 const now = Date . now ( ) ;
664770 const accountManager = new AccountManager ( undefined , createStorage ( now ) ) ;
0 commit comments