From be569602e95da10201de4a49ea07027407974cf7 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Tue, 7 Jul 2026 05:53:30 -0700 Subject: [PATCH 1/3] fix: add diagnostic EACCES guard to workdir setup When mkdirSync fails with EACCES (typically caused by stale root-owned directories from a previous AWF run), the error message now includes: - The blocking ancestor directory path - Its uid/gid/mode for immediate diagnosis - The current process UID - A clear explanation that the orchestrator must clean up stale directories - A suggested fix command (sudo rm -rf) This makes the failure actionable without requiring log archaeology. The guard is applied to both validateAndPrepareWorkDir (config-writer.ts) and ensureDirectory (workdir-setup.ts) which covers all directory creation paths. The actual fix for this class of failures belongs in the calling process (e.g., gh-aw setup action) which owns the paths and has the context to reclaim them. AWF intentionally does not attempt self-repair here because it should not require sudo in --network-isolation mode. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/config-writer.ts | 85 +++++++++++++++++++++++++++++++++++++------- src/workdir-setup.ts | 30 ++++++++++++++-- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/config-writer.ts b/src/config-writer.ts index 8f8851309..099916831 100644 --- a/src/config-writer.ts +++ b/src/config-writer.ts @@ -24,6 +24,50 @@ import { prepareWorkDirectories } from './workdir-setup'; // builds the identifier remains undeclared, so the typeof check below is safe. declare const __AWF_SECCOMP_PROFILE__: string | undefined; +/** + * Produces a human-readable diagnostic string explaining why EACCES occurred. + * Walks the path hierarchy to identify which ancestor is not writable. + */ +function diagnoseEacces(targetDir: string): string { + const resolvedTarget = path.resolve(targetDir); + let current = resolvedTarget; + const lines: string[] = []; + + // Walk up to find the blocking directory + while (current !== path.dirname(current)) { + if (fs.existsSync(current)) { + try { + const stat = fs.statSync(current); + const writable = isWritable(current); + lines.push( + ` ${current}: uid=${stat.uid} gid=${stat.gid} mode=${(stat.mode & 0o7777).toString(8)} writable=${writable}` + ); + if (!writable) { + lines.push(` └─ BLOCKED HERE: current process (uid=${process.getuid?.() ?? '?'}) cannot write to this directory`); + break; + } + } catch { + lines.push(` ${current}: (cannot stat)`); + break; + } + } + current = path.dirname(current); + } + + return lines.length > 0 + ? `Path diagnosis:\n${lines.join('\n')}` + : `Path diagnosis: could not determine blocking ancestor`; +} + +function isWritable(dirPath: string): boolean { + try { + fs.accessSync(dirPath, fs.constants.W_OK); + return true; + } catch { + return false; + } +} + /** Resolved network topology passed between setup phases. */ interface NetworkConfig { subnet: string; @@ -46,19 +90,34 @@ function validateAndPrepareWorkDir(config: WrapperConfig): void { // Ensure work directory exists with restricted permissions (owner-only access) // Defense-in-depth: even if tmpfs overlay fails, non-root processes on the host // cannot read the docker-compose.yml which contains sensitive tokens - const workDirCreated = Boolean( - fs.mkdirSync(config.workDir, { recursive: true, mode: 0o700 }) - ); - const workDirLstat = fs.lstatSync(config.workDir); - if (workDirLstat.isSymbolicLink()) { - throw new Error(`Refusing to use symlink as directory: ${config.workDir}`); - } - const workDirStat = fs.statSync(config.workDir); - if (!workDirStat.isDirectory()) { - throw new Error(`Expected directory but found non-directory path: ${config.workDir}`); - } - if (!workDirCreated) { - fs.chmodSync(config.workDir, 0o700); + try { + const workDirCreated = Boolean( + fs.mkdirSync(config.workDir, { recursive: true, mode: 0o700 }) + ); + const workDirLstat = fs.lstatSync(config.workDir); + if (workDirLstat.isSymbolicLink()) { + throw new Error(`Refusing to use symlink as directory: ${config.workDir}`); + } + const workDirStat = fs.statSync(config.workDir); + if (!workDirStat.isDirectory()) { + throw new Error(`Expected directory but found non-directory path: ${config.workDir}`); + } + if (!workDirCreated) { + fs.chmodSync(config.workDir, 0o700); + } + } catch (error: unknown) { + if (error && typeof error === 'object' && 'code' in error && error.code === 'EACCES') { + const diagnostic = diagnoseEacces(config.workDir); + throw new Error( + `EACCES: cannot create work directory: ${config.workDir}\n` + + `${diagnostic}\n` + + `This typically happens on persistent runners when a previous AWF run ` + + `left directories owned by root. The calling process (e.g., gh-aw setup) ` + + `must remove or chown the stale directory before invoking AWF.\n` + + ` Suggested fix: sudo rm -rf ${path.dirname(config.workDir)} && mkdir -p ${config.workDir}` + ); + } + throw error; } } diff --git a/src/workdir-setup.ts b/src/workdir-setup.ts index b7066a8a7..68de93c80 100644 --- a/src/workdir-setup.ts +++ b/src/workdir-setup.ts @@ -15,9 +15,33 @@ interface EnsureDirectoryOptions { function ensureDirectory(dirPath: string, options: EnsureDirectoryOptions = {}): boolean { const { mode, onCreate, onExists, onAfterEnsure } = options; - const created = Boolean( - fs.mkdirSync(dirPath, mode === undefined ? { recursive: true } : { recursive: true, mode }) - ); + let created: boolean; + try { + created = Boolean( + fs.mkdirSync(dirPath, mode === undefined ? { recursive: true } : { recursive: true, mode }) + ); + } catch (error: unknown) { + if (error && typeof error === 'object' && 'code' in error && error.code === 'EACCES') { + const uid = process.getuid?.() ?? '?'; + // Identify the blocking ancestor for actionable diagnostics + let blocker = dirPath; + let current = path.resolve(dirPath); + while (current !== path.dirname(current)) { + if (fs.existsSync(current)) { + try { fs.accessSync(current, fs.constants.W_OK); } catch { blocker = current; } + break; + } + current = path.dirname(current); + } + throw new Error( + `EACCES: cannot create directory ${dirPath} (running as uid=${uid}).\n` + + ` Blocked by: ${blocker}\n` + + ` This is typically caused by a previous AWF run leaving root-owned directories.\n` + + ` The orchestrator must clean up stale directories before invoking AWF.` + ); + } + throw error; + } const lstat = fs.lstatSync(dirPath); if (lstat.isSymbolicLink()) { From 681e2fe2ee21ffeeb2240a016fe42d3e66deba10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:22:57 +0000 Subject: [PATCH 2/3] fix: address review feedback on EACCES diagnostics - Fix isWritable() to check W_OK|X_OK instead of just W_OK, so the blocker is correctly identified when a directory lacks execute/search permission (config-writer.ts, workdir-setup.ts) - Fix suggested remediation in validateAndPrepareWorkDir: use the identified blocker path instead of path.dirname(workDir), preventing an overly broad sudo rm -rf (e.g. /tmp) when workDir is shallow - Fix ensureDirectory blocker detection: default blocker to the nearest existing ancestor rather than dirPath (which may not exist) - Add fsMockFactory entries for mkdirSync/accessSync/statSync so tests can mock them without hitting 'Cannot redefine property' - Add unit tests for EACCES diagnostic in ensureDirectory (workdir-setup.test.ts) - Add unit tests for EACCES diagnostic in validateAndPrepareWorkDir (config-writer-branches.test.ts)" --- src/config-writer-branches.test.ts | 106 +++++++++++++++++- src/config-writer.ts | 19 ++-- .../config-writer-test-harness.test-utils.ts | 11 ++ .../fs-mock-factory.test-utils.ts | 3 + src/workdir-setup.test.ts | 91 +++++++++++++++ src/workdir-setup.ts | 11 +- 6 files changed, 229 insertions(+), 12 deletions(-) diff --git a/src/config-writer-branches.test.ts b/src/config-writer-branches.test.ts index 50be5934d..b43e3ca5d 100644 --- a/src/config-writer-branches.test.ts +++ b/src/config-writer-branches.test.ts @@ -3,13 +3,14 @@ * - writeAuditArtifacts: auditDir symlink guard * - copySeccompProfile: alternate seccomp-profile.json fallback path * - initializeSslBump: non-Error rejection propagation + * - validateAndPrepareWorkDir: EACCES diagnostic */ import './test-helpers/config-writer-dependency-mocks.test-utils'; import * as fs from 'fs'; import * as path from 'path'; -import { writeConfigs } from './config-writer'; +import { writeConfigs, configWriterTestHelpers } from './config-writer'; import { isOpenSslAvailable, generateSessionCa, initSslDb } from './ssl-bump'; import { buildWriteConfig, @@ -125,4 +126,107 @@ describe('config-writer additional branches', () => { } }); }); + + // ─── validateAndPrepareWorkDir EACCES diagnostic ────────────────────────── + + describe('validateAndPrepareWorkDir EACCES diagnostic', () => { + it('throws with diagnostic naming the blocking ancestor in the suggested fix', () => { + const blockerDir = path.join(tempDir, 'stale-parent'); + const workDir = path.join(blockerDir, 'new-workdir'); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${workDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + // existsSync: only stale-parent exists; workDir does not + (fs.existsSync as jest.Mock).mockImplementation((p: fs.PathLike) => + String(p) === blockerDir + ); + // statSync: blockerDir is root-owned + (fs.statSync as jest.Mock).mockReturnValue({ uid: 0, gid: 0, mode: 0o40755 } as fs.Stats); + // accessSync: blockerDir is not writable + (fs.accessSync as jest.Mock).mockImplementation(() => { throw new Error('EACCES'); }); + + try { + expect(() => + configWriterTestHelpers.validateAndPrepareWorkDir(buildWriteConfig(tempDir, { workDir })) + ).toThrow(/EACCES: cannot create work directory/); + + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + expect(() => + configWriterTestHelpers.validateAndPrepareWorkDir(buildWriteConfig(tempDir, { workDir })) + ).toThrow(new RegExp(`Suggested fix: sudo rm -rf ${blockerDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)); + } finally { + (fs.existsSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').existsSync(...args) + ); + (fs.statSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').statSync(...args) + ); + (fs.accessSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').accessSync(...args) + ); + } + }); + + it('falls back to workDir in the suggested fix when no blocking ancestor is identified', () => { + const workDir = path.join(tempDir, 'new-workdir'); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${workDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + (fs.existsSync as jest.Mock).mockImplementation(() => false); + + try { + expect(() => + configWriterTestHelpers.validateAndPrepareWorkDir(buildWriteConfig(tempDir, { workDir })) + ).toThrow(new RegExp(`Suggested fix: sudo rm -rf ${workDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)); + } finally { + (fs.existsSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').existsSync(...args) + ); + } + }); + + it('uses W_OK | X_OK when checking writability of the ancestor', () => { + const blockerDir = path.join(tempDir, 'stale-parent'); + const workDir = path.join(blockerDir, 'new-workdir'); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${workDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + (fs.existsSync as jest.Mock).mockImplementation((p: fs.PathLike) => + String(p) === blockerDir + ); + (fs.statSync as jest.Mock).mockReturnValue({ uid: 0, gid: 0, mode: 0o40755 } as fs.Stats); + (fs.accessSync as jest.Mock).mockImplementation(() => { throw new Error('EACCES'); }); + + const actualFsConstants = jest.requireActual('fs').constants; + + try { + expect(() => + configWriterTestHelpers.validateAndPrepareWorkDir(buildWriteConfig(tempDir, { workDir })) + ).toThrow(/EACCES/); + expect(fs.accessSync as jest.Mock).toHaveBeenCalledWith( + blockerDir, + actualFsConstants.W_OK | actualFsConstants.X_OK + ); + } finally { + (fs.existsSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').existsSync(...args) + ); + (fs.statSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').statSync(...args) + ); + (fs.accessSync as jest.Mock).mockImplementation( + (...args: Parameters) => jest.requireActual('fs').accessSync(...args) + ); + } + }); + }); }); diff --git a/src/config-writer.ts b/src/config-writer.ts index 099916831..f19878a0f 100644 --- a/src/config-writer.ts +++ b/src/config-writer.ts @@ -26,12 +26,14 @@ declare const __AWF_SECCOMP_PROFILE__: string | undefined; /** * Produces a human-readable diagnostic string explaining why EACCES occurred. - * Walks the path hierarchy to identify which ancestor is not writable. + * Walks the path hierarchy to identify which ancestor is not writable/searchable. + * Returns the diagnostic string and the identified blocking path (if found). */ -function diagnoseEacces(targetDir: string): string { +function diagnoseEacces(targetDir: string): { diagnosis: string; blockerPath: string | null } { const resolvedTarget = path.resolve(targetDir); let current = resolvedTarget; const lines: string[] = []; + let blockerPath: string | null = null; // Walk up to find the blocking directory while (current !== path.dirname(current)) { @@ -43,6 +45,7 @@ function diagnoseEacces(targetDir: string): string { ` ${current}: uid=${stat.uid} gid=${stat.gid} mode=${(stat.mode & 0o7777).toString(8)} writable=${writable}` ); if (!writable) { + blockerPath = current; lines.push(` └─ BLOCKED HERE: current process (uid=${process.getuid?.() ?? '?'}) cannot write to this directory`); break; } @@ -54,14 +57,15 @@ function diagnoseEacces(targetDir: string): string { current = path.dirname(current); } - return lines.length > 0 + const diagnosis = lines.length > 0 ? `Path diagnosis:\n${lines.join('\n')}` : `Path diagnosis: could not determine blocking ancestor`; + return { diagnosis, blockerPath }; } function isWritable(dirPath: string): boolean { try { - fs.accessSync(dirPath, fs.constants.W_OK); + fs.accessSync(dirPath, fs.constants.W_OK | fs.constants.X_OK); return true; } catch { return false; @@ -107,14 +111,15 @@ function validateAndPrepareWorkDir(config: WrapperConfig): void { } } catch (error: unknown) { if (error && typeof error === 'object' && 'code' in error && error.code === 'EACCES') { - const diagnostic = diagnoseEacces(config.workDir); + const { diagnosis, blockerPath } = diagnoseEacces(config.workDir); + const suggestedPath = blockerPath ?? config.workDir; throw new Error( `EACCES: cannot create work directory: ${config.workDir}\n` + - `${diagnostic}\n` + + `${diagnosis}\n` + `This typically happens on persistent runners when a previous AWF run ` + `left directories owned by root. The calling process (e.g., gh-aw setup) ` + `must remove or chown the stale directory before invoking AWF.\n` + - ` Suggested fix: sudo rm -rf ${path.dirname(config.workDir)} && mkdir -p ${config.workDir}` + ` Suggested fix: sudo rm -rf ${suggestedPath} && mkdir -p ${suggestedPath}` ); } throw error; diff --git a/src/test-helpers/config-writer-test-harness.test-utils.ts b/src/test-helpers/config-writer-test-harness.test-utils.ts index 39450940f..045df8fbf 100644 --- a/src/test-helpers/config-writer-test-harness.test-utils.ts +++ b/src/test-helpers/config-writer-test-harness.test-utils.ts @@ -82,8 +82,19 @@ export function buildWriteConfig( */ export function setupConfigWriterTempDir(prefix = 'config-writer-test-'): string { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + const actualFs = jest.requireActual('fs'); jest.clearAllMocks(); (fs.chownSync as unknown as jest.Mock).mockImplementation(() => undefined); + // Restore passthrough impls for mocks added to fsMockFactory that may be overridden per-test + (fs.mkdirSync as jest.Mock).mockImplementation( + (...args: Parameters) => actualFs.mkdirSync(...args), + ); + (fs.accessSync as jest.Mock).mockImplementation( + (...args: Parameters) => actualFs.accessSync(...args), + ); + (fs.statSync as jest.Mock).mockImplementation( + (...args: Parameters) => actualFs.statSync(...args), + ); (getRealUserHome as jest.Mock).mockReturnValue(tempDir); return tempDir; } diff --git a/src/test-helpers/fs-mock-factory.test-utils.ts b/src/test-helpers/fs-mock-factory.test-utils.ts index b448c9759..e5fe6c14c 100644 --- a/src/test-helpers/fs-mock-factory.test-utils.ts +++ b/src/test-helpers/fs-mock-factory.test-utils.ts @@ -4,6 +4,9 @@ export function fsMockFactory() { const actual = jest.requireActual('fs'); return { ...actual, + mkdirSync: jest.fn((...args: Parameters) => actual.mkdirSync(...args)) as typeof actual.mkdirSync, + accessSync: jest.fn((...args: Parameters) => actual.accessSync(...args)) as typeof actual.accessSync, + statSync: jest.fn((...args: Parameters) => actual.statSync(...args)) as typeof actual.statSync, chmodSync: jest.fn((...args: Parameters) => actual.chmodSync(...args)), chownSync: jest.fn(), existsSync: jest.fn((...args: Parameters) => actual.existsSync(...args)), diff --git a/src/workdir-setup.test.ts b/src/workdir-setup.test.ts index 215c2b8fd..f90ca2744 100644 --- a/src/workdir-setup.test.ts +++ b/src/workdir-setup.test.ts @@ -41,6 +41,12 @@ function setupWorkdirFixture({ cleanupChrootHome = true } = {}) { (fs.chmodSync as jest.Mock).mockImplementation( (...args: Parameters) => actualFs.chmodSync(...args), ); + (fs.mkdirSync as jest.Mock).mockImplementation( + (...args: Parameters) => actualFs.mkdirSync(...args), + ); + (fs.accessSync as jest.Mock).mockImplementation( + (...args: Parameters) => actualFs.accessSync(...args), + ); (getRealUserHome as jest.Mock).mockReturnValue(tempDir); }); @@ -497,3 +503,88 @@ describe('prepareChrootHomeMounts (sub-function)', () => { expect(fs.existsSync(geminiDir)).toBe(false); }); }); + +describe('ensureDirectory EACCES diagnostic', () => { + const fixture = setupWorkdirFixture({ cleanupChrootHome: false }); + + it('throws a diagnostic error naming the nearest existing ancestor as blocker', () => { + const parentDir = path.join(fixture.tempDir, 'stale-parent'); + const targetDir = path.join(parentDir, 'new-child'); + actualFs.mkdirSync(parentDir, { recursive: true }); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${targetDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + (fs.accessSync as jest.Mock).mockImplementationOnce(() => { throw new Error('EACCES'); }); + + expect(() => workdirSetupTestHelpers.ensureDirectory(targetDir)).toThrow( + new RegExp(`Blocked by: .*stale-parent`) + ); + }); + + it('uses nearest existing ancestor as blocker even when it appears writable', () => { + const parentDir = path.join(fixture.tempDir, 'existing-parent'); + const targetDir = path.join(parentDir, 'new-child'); + actualFs.mkdirSync(parentDir, { recursive: true }); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${targetDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + // accessSync succeeds (writable), but nearest ancestor is still reported as blocker + + expect(() => workdirSetupTestHelpers.ensureDirectory(targetDir)).toThrow( + new RegExp(`Blocked by: .*existing-parent`) + ); + }); + + it('falls back to dirPath in Blocked by when no existing ancestor is found', () => { + const targetDir = path.join(fixture.tempDir, 'deep', 'nonexistent', 'dir'); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${targetDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + (fs.existsSync as jest.Mock).mockImplementation(() => false); + + try { + expect(() => workdirSetupTestHelpers.ensureDirectory(targetDir)).toThrow( + new RegExp(`Blocked by: ${targetDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`) + ); + } finally { + (fs.existsSync as jest.Mock).mockImplementation( + (...args: Parameters) => actualFs.existsSync(...args) + ); + } + }); + + it('checks W_OK | X_OK when probing the blocking ancestor', () => { + const parentDir = path.join(fixture.tempDir, 'stale-parent'); + const targetDir = path.join(parentDir, 'new-child'); + actualFs.mkdirSync(parentDir, { recursive: true }); + + const eacces = Object.assign( + new Error(`EACCES: permission denied, mkdir '${targetDir}'`), + { code: 'EACCES' } + ); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw eacces; }); + + expect(() => workdirSetupTestHelpers.ensureDirectory(targetDir)).toThrow(/EACCES/); + expect(fs.accessSync as jest.Mock).toHaveBeenCalledWith( + expect.any(String), + actualFs.constants.W_OK | actualFs.constants.X_OK + ); + }); + + it('rethrows non-EACCES errors from mkdirSync unchanged', () => { + const targetDir = path.join(fixture.tempDir, 'some-dir'); + const enoent = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' }); + (fs.mkdirSync as jest.Mock).mockImplementationOnce(() => { throw enoent; }); + + expect(() => workdirSetupTestHelpers.ensureDirectory(targetDir)).toThrow(enoent); + }); +}); diff --git a/src/workdir-setup.ts b/src/workdir-setup.ts index 68de93c80..640a5a884 100644 --- a/src/workdir-setup.ts +++ b/src/workdir-setup.ts @@ -23,19 +23,22 @@ function ensureDirectory(dirPath: string, options: EnsureDirectoryOptions = {}): } catch (error: unknown) { if (error && typeof error === 'object' && 'code' in error && error.code === 'EACCES') { const uid = process.getuid?.() ?? '?'; - // Identify the blocking ancestor for actionable diagnostics - let blocker = dirPath; + // Identify the blocking ancestor for actionable diagnostics. + // Default to the nearest existing ancestor (more actionable than a path that + // does not exist), and confirm with a W_OK|X_OK access check. + let blocker: string | null = null; let current = path.resolve(dirPath); while (current !== path.dirname(current)) { if (fs.existsSync(current)) { - try { fs.accessSync(current, fs.constants.W_OK); } catch { blocker = current; } + blocker = current; + try { fs.accessSync(current, fs.constants.W_OK | fs.constants.X_OK); } catch { /* confirmed blocker */ } break; } current = path.dirname(current); } throw new Error( `EACCES: cannot create directory ${dirPath} (running as uid=${uid}).\n` + - ` Blocked by: ${blocker}\n` + + ` Blocked by: ${blocker ?? dirPath}\n` + ` This is typically caused by a previous AWF run leaving root-owned directories.\n` + ` The orchestrator must clean up stale directories before invoking AWF.` ); From 16fab427d43a20ca52c82fdd3d775779c7f80751 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:08:57 +0000 Subject: [PATCH 3/3] fix: use event-based waiting in server.websocket tests --- containers/api-proxy/server.websocket.test.js | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/containers/api-proxy/server.websocket.test.js b/containers/api-proxy/server.websocket.test.js index ad3ef1165..ed3434a77 100644 --- a/containers/api-proxy/server.websocket.test.js +++ b/containers/api-proxy/server.websocket.test.js @@ -226,17 +226,19 @@ describe('proxyWebSocket', () => { wsProxy(req, socket, Buffer.alloc(0), 'api.openai.com', { 'Authorization': 'Bearer injected' }, 'openai'); - return new Promise(resolve => setTimeout(() => { - const upgradeWrite = tlsSocket.write.mock.calls.find( - c => typeof c[0] === 'string' && c[0].startsWith('GET ') - ); - expect(upgradeWrite).toBeDefined(); - const upgradeReqStr = upgradeWrite[0]; - expect(upgradeReqStr).not.toContain('client-supplied'); - expect(upgradeReqStr).not.toContain('client-api-key'); - expect(upgradeReqStr).toContain('Bearer injected'); - resolve(); - }, 30)); + return new Promise(resolve => { + tlsSocket.once('secureConnect', () => setImmediate(() => { + const upgradeWrite = tlsSocket.write.mock.calls.find( + c => typeof c[0] === 'string' && c[0].startsWith('GET ') + ); + expect(upgradeWrite).toBeDefined(); + const upgradeReqStr = upgradeWrite[0]; + expect(upgradeReqStr).not.toContain('client-supplied'); + expect(upgradeReqStr).not.toContain('client-api-key'); + expect(upgradeReqStr).toContain('Bearer injected'); + resolve(); + })); + }); }); it('forwards the CONNECT request to the configured Squid proxy host/port', () => { @@ -267,12 +269,14 @@ describe('proxyWebSocket', () => { const headBytes = Buffer.from([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]); // WS text frame: FIN=1, opcode=1, len=5, payload='Hello' wsProxy(makeUpgradeReq(), socket, headBytes, 'api.openai.com', { 'Authorization': 'Bearer k' }, 'openai'); - return new Promise(resolve => setTimeout(() => { - const bufWrite = tlsSocket.write.mock.calls.find(c => Buffer.isBuffer(c[0])); - expect(bufWrite).toBeDefined(); - expect(bufWrite[0]).toEqual(headBytes); - resolve(); - }, 30)); + return new Promise(resolve => { + tlsSocket.once('secureConnect', () => setImmediate(() => { + const bufWrite = tlsSocket.write.mock.calls.find(c => Buffer.isBuffer(c[0])); + expect(bufWrite).toBeDefined(); + expect(bufWrite[0]).toEqual(headBytes); + resolve(); + })); + }); }); }); });