diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index bd0bb407..ad900faf 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -7,6 +7,7 @@ import { writeFileSync, } from 'node:fs'; import path from 'node:path'; +import { types } from 'node:util'; import { displayPath, fail, @@ -156,7 +157,7 @@ const claimOwner = ( writeFileSync(ownerPath, `${project}\n`, { flag: 'wx' }); } catch (error) { const code = - error instanceof Error && 'code' in error ? error.code : undefined; + types.isNativeError(error) && 'code' in error ? error.code : undefined; if (code !== 'EEXIST') { throw error; } @@ -285,7 +286,7 @@ export const installHooks = ({ chmodSync(filePath, 0o755); } } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = types.isNativeError(error) ? error.message : String(error); return fail('write-failed', `Failed to write Git hook files: ${message}`); } diff --git a/packages/rstack/tests/setup/install-errors.test.ts b/packages/rstack/tests/setup/install-errors.test.ts new file mode 100644 index 00000000..7733a4a1 --- /dev/null +++ b/packages/rstack/tests/setup/install-errors.test.ts @@ -0,0 +1,54 @@ +import { existsSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { runInNewContext } from 'node:vm'; +import { afterEach, expect, rs, test } from 'rstack/test'; +import { installHooks } from '../../src/setup/install.ts'; +import { git, hooksPath, withRepository } from './helpers.ts'; + +rs.mock('node:fs', () => { + const actual = rs.requireActual('node:fs'); + return { ...actual, writeFileSync: rs.fn(actual.writeFileSync) }; +}); + +afterEach(() => { + rs.mocked(writeFileSync).mockReset(); +}); + +test('handles cross-realm owner file conflicts', () => { + withRepository((cwd) => { + const error: unknown = runInNewContext( + 'Object.assign(new Error("owner file exists"), { code: "EEXIST" })', + ); + expect(error instanceof Error).toBe(false); + rs.mocked(writeFileSync).mockImplementationOnce(() => { + throw error; + }); + + expect(installHooks({ cwd })).toMatchObject({ + status: 'skipped', + reason: 'hooks-directory-conflict', + }); + expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(false); + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); + }); +}); + +test('preserves the message of cross-realm filesystem errors', () => { + withRepository((cwd) => { + const error: unknown = runInNewContext( + 'Object.assign(new Error("permission denied"), { code: "EACCES" })', + ); + expect(error instanceof Error).toBe(false); + rs.mocked(writeFileSync).mockImplementationOnce(() => { + throw error; + }); + + expect(installHooks({ cwd })).toEqual({ + status: 'failed', + reason: 'write-failed', + message: 'Failed to write Git hook files: permission denied', + }); + }); +});