From 151e74a1eb75e7555b8f3e9732d5b077ddbbd961 Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 6 Nov 2026 22:00:08 +0800 Subject: [PATCH 1/3] fix(hooks): recognize cross-realm filesystem errors --- packages/rstack/src/setup/install.ts | 5 +- .../rstack/tests/setup/install-errors.test.ts | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 packages/rstack/tests/setup/install-errors.test.ts 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', + }); + }); +}); From 701f73294ac35e6520777d2d5bb5605f525430fe Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 7 Nov 2026 17:05:23 +0800 Subject: [PATCH 2/3] fix(hooks): prefer Error.isError when available --- packages/rstack/src/setup/install.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index ad900faf..b67c0a92 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -26,6 +26,8 @@ import { createHookFiles, hookNames } from './hooks.ts'; const defaultHooksDir = '.rstack/hooks'; const gitignore = '*\n'; +const isError = (value: unknown): value is Error => + Error.isError?.(value) ?? types.isNativeError(value); type InstallHooksOptions = { cwd?: string; @@ -156,8 +158,7 @@ const claimOwner = ( // Exclusive creation makes concurrent prepare scripts agree on one owner. writeFileSync(ownerPath, `${project}\n`, { flag: 'wx' }); } catch (error) { - const code = - types.isNativeError(error) && 'code' in error ? error.code : undefined; + const code = isError(error) && 'code' in error ? error.code : undefined; if (code !== 'EEXIST') { throw error; } @@ -286,7 +287,7 @@ export const installHooks = ({ chmodSync(filePath, 0o755); } } catch (error) { - const message = types.isNativeError(error) ? error.message : String(error); + const message = isError(error) ? error.message : String(error); return fail('write-failed', `Failed to write Git hook files: ${message}`); } From cb11f3acfed515fca6f5cb0d68ce90cb8d0228c8 Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 7 Nov 2026 18:01:38 +0800 Subject: [PATCH 3/3] test: remove additional hooks error regression tests --- .../rstack/tests/setup/install-errors.test.ts | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 packages/rstack/tests/setup/install-errors.test.ts diff --git a/packages/rstack/tests/setup/install-errors.test.ts b/packages/rstack/tests/setup/install-errors.test.ts deleted file mode 100644 index 7733a4a1..00000000 --- a/packages/rstack/tests/setup/install-errors.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -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', - }); - }); -});