diff --git a/src/artifact-permissions.test.ts b/src/artifact-permissions.test.ts index 892767e8f..20549b9b6 100644 --- a/src/artifact-permissions.test.ts +++ b/src/artifact-permissions.test.ts @@ -39,6 +39,39 @@ describe('artifact-permissions', () => { } }); + it('logs stderr when permission repair fails', () => { + const auditDir = makeTempDir(); + let warnSpy: jest.SpyInstance | undefined; + try { + getuidSpy = jest.spyOn(process, 'getuid').mockReturnValue(1001); + warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExecaSync.mockReturnValue({ stdout: '', stderr: 'no such image: agent:latest', exitCode: 1 }); + fixArtifactPermissionsForRootless([auditDir], undefined, undefined, undefined, undefined); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('no such image: agent:latest')); + } finally { + warnSpy?.mockRestore(); + fs.rmSync(auditDir, { recursive: true, force: true }); + } + }); + + it('logs exit code without stderr when stderr is empty', () => { + const auditDir = makeTempDir(); + let warnSpy: jest.SpyInstance | undefined; + try { + getuidSpy = jest.spyOn(process, 'getuid').mockReturnValue(1001); + warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExecaSync.mockReturnValue({ stdout: '', stderr: '', exitCode: 1 }); + fixArtifactPermissionsForRootless([auditDir], undefined, undefined, undefined, undefined); + expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/failed.*exit 1/i)); + // Should NOT contain a colon suffix when stderr is empty + const warnCall = warnSpy.mock.calls.find(c => typeof c[0] === 'string' && /exit 1/.test(c[0])); + expect(warnCall?.[0]).not.toMatch(/exit 1\):/); + } finally { + warnSpy?.mockRestore(); + fs.rmSync(auditDir, { recursive: true, force: true }); + } + }); + it('runs rootless permission repair with translated mount paths', () => { const auditDir = makeTempDir(); try { diff --git a/src/artifact-permissions.ts b/src/artifact-permissions.ts index 46845f4f8..0ab545658 100644 --- a/src/artifact-permissions.ts +++ b/src/artifact-permissions.ts @@ -79,7 +79,11 @@ export function fixArtifactPermissionsForRootless( ); if (typeof result.exitCode === 'number' && result.exitCode !== 0) { - logger.warn(`Rootless artifact permission repair failed for ${dir} (exit ${result.exitCode})`); + const stderr = result.stderr?.trim(); + logger.warn( + `Rootless artifact permission repair failed for ${dir} (exit ${result.exitCode})` + + (stderr ? `: ${stderr}` : ''), + ); } } catch (error) { logger.warn(`Rootless artifact permission repair failed for ${dir}:`, error); diff --git a/src/artifact-preservation.ts b/src/artifact-preservation.ts index 00c638e8b..441cd7563 100644 --- a/src/artifact-preservation.ts +++ b/src/artifact-preservation.ts @@ -248,10 +248,13 @@ export function removeWorkDirectories(workDir: string, options: RemoveWorkDirect try { fs.rmSync(chrootHomeDir, { recursive: true, force: true }); } catch (retryError) { - logger.warn('Failed to remove chroot home directory after permission repair:', retryError); + // Non-fatal: chroot-home will be cleaned by the post-step + // (install_copilot_cli.sh's sudo cleanup) or runner infrastructure. + logger.debug(`Could not remove chroot home directory after permission repair: ${chrootHomeDir}`, retryError); } } else { - logger.warn('Failed to remove chroot home directory:', error); + // Non-fatal: same reasoning — defer to post-step cleanup. + logger.debug('Failed to remove chroot home directory:', error); } } }