@@ -49,10 +49,33 @@ describe("github-token", () => {
4949 ) . toBeUndefined ( ) ;
5050 } ) ;
5151
52+ it ( "fails closed ('') when the file exists but is unreadable (not ENOENT)" , ( ) => {
53+ // A directory path triggers EISDIR, standing in for a transiently unreadable
54+ // managed file during a transition — must not resurrect the process env.
55+ const dir = mkdtempSync ( join ( tmpdir ( ) , "agent-env-dir-" ) ) ;
56+ expect ( readGithubTokenFromSandboxEnvFile ( dir ) ) . toBe ( "" ) ;
57+ } ) ;
58+
5259 it ( "ignores an empty token value" , ( ) => {
5360 const path = writeEnvFile ( "GH_TOKEN=\0GITHUB_TOKEN=ghs_real\0" ) ;
5461 expect ( readGithubTokenFromSandboxEnvFile ( path ) ) . toBe ( "ghs_real" ) ;
5562 } ) ;
63+
64+ it ( "returns '' (explicit logout) when every token var is present but empty" , ( ) => {
65+ const path = writeEnvFile ( "PATH=/usr/bin\0GH_TOKEN=\0GITHUB_TOKEN=\0" ) ;
66+ expect ( readGithubTokenFromSandboxEnvFile ( path ) ) . toBe ( "" ) ;
67+ } ) ;
68+
69+ it ( "returns '' (logout) when the managed file is truncated to zero bytes" , ( ) => {
70+ // The backend logs the sandbox out by writing an empty file, not emptied vars.
71+ expect ( readGithubTokenFromSandboxEnvFile ( writeEnvFile ( "" ) ) ) . toBe ( "" ) ;
72+ expect ( readGithubTokenFromSandboxEnvFile ( writeEnvFile ( " \n" ) ) ) . toBe ( "" ) ;
73+ } ) ;
74+
75+ it ( "returns undefined when the file carries no token var at all" , ( ) => {
76+ const path = writeEnvFile ( "PATH=/usr/bin\0HOME=/root\0" ) ;
77+ expect ( readGithubTokenFromSandboxEnvFile ( path ) ) . toBeUndefined ( ) ;
78+ } ) ;
5679 } ) ;
5780
5881 describe ( "resolveGithubToken" , ( ) => {
@@ -72,5 +95,35 @@ describe("github-token", () => {
7295 "ghs_fromprocess" ,
7396 ) ;
7497 } ) ;
98+
99+ it ( "does not resurrect the process-env token after a logout (emptied file)" , ( ) => {
100+ // The backend logs the sandbox out by emptying the token vars in the file.
101+ // The frozen launch-time process env still holds the previous actor's
102+ // token; resolving must NOT fall back to it.
103+ vi . stubEnv ( "GH_TOKEN" , "ghs_previous_actor" ) ;
104+ const path = writeEnvFile ( "GH_TOKEN=\0GITHUB_TOKEN=\0" ) ;
105+ expect ( resolveGithubToken ( path ) ) . toBe ( "" ) ;
106+ } ) ;
107+
108+ it ( "does not resurrect the process-env token when the file is zero bytes (logout)" , ( ) => {
109+ // The backend's actual logout truncates the file to zero bytes; resolving
110+ // must treat that as logout, not fall back to the frozen process env.
111+ vi . stubEnv ( "GH_TOKEN" , "ghs_previous_actor" ) ;
112+ expect ( resolveGithubToken ( writeEnvFile ( "" ) ) ) . toBe ( "" ) ;
113+ } ) ;
114+
115+ it ( "falls back to the process env when the file carries no token var" , ( ) => {
116+ vi . stubEnv ( "GH_TOKEN" , "ghs_fromprocess" ) ;
117+ const path = writeEnvFile ( "PATH=/usr/bin\0" ) ;
118+ expect ( resolveGithubToken ( path ) ) . toBe ( "ghs_fromprocess" ) ;
119+ } ) ;
120+
121+ it ( "does not fall back to the process env when the file is unreadable" , ( ) => {
122+ // Present-but-unreadable (EISDIR here) is a managed sandbox mid-transition,
123+ // not an absent file, so it must not resurrect the frozen process token.
124+ vi . stubEnv ( "GH_TOKEN" , "ghs_previous_actor" ) ;
125+ const dir = mkdtempSync ( join ( tmpdir ( ) , "agent-env-dir-" ) ) ;
126+ expect ( resolveGithubToken ( dir ) ) . toBe ( "" ) ;
127+ } ) ;
75128 } ) ;
76129} ) ;
0 commit comments