Skip to content

Commit bdecd4c

Browse files
yiliang114Qwen-Coder
andcommitted
fix(core): keep the no-follow helper mockable by fs spy suites
openSyncNoFollow bound node:fs through a namespace import, which vitest resolves to its own copy of the externalized CJS module. Suites that spy the fs object — sessionService.rename.test.ts stubs openSync/readSync for fabricated session paths — never intercept that copy, so the open threw on the mocked paths and the catch-all reported "no title" (10 of 19 tests red, the CI Test failure). Take the fs binding through the default import the way the callers' suites spy it, teach the doMock factories to carry the stub on the default binding too, and stub lstatSync/fstatSync in the rename suite so the Windows lstat -> open -> fstat fallback accepts the fabricated paths as well. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
1 parent c067b92 commit bdecd4c

3 files changed

Lines changed: 42 additions & 4 deletions

File tree

‎packages/core/src/services/sessionService.rename.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,32 @@ describe('SessionService - rename and custom title', () => {
8383
vi.spyOn(fs, 'openSync').mockReturnValue(42);
8484
readSyncSpy = vi.spyOn(fs, 'readSync').mockReturnValue(0);
8585
vi.spyOn(fs, 'closeSync').mockImplementation(() => undefined);
86+
// Platforms without O_NOFOLLOW (Windows) open session files through an
87+
// lstat -> open -> fstat identity check (openSyncNoFollow). Spy both
88+
// stats so that fallback accepts the fabricated paths above: a regular
89+
// (non-symlink) file whose identity trivially matches itself. On
90+
// platforms with the flag the spies stay inert.
91+
vi.spyOn(fs, 'lstatSync').mockImplementation(
92+
() =>
93+
({
94+
dev: 1,
95+
ino: 1,
96+
isSymbolicLink: () => false,
97+
isFile: () => true,
98+
}) as unknown as fs.Stats,
99+
);
100+
vi.spyOn(fs, 'fstatSync').mockImplementation(
101+
() =>
102+
({
103+
dev: 1,
104+
ino: 1,
105+
// size 0 keeps readLatestTailIfGrown's grown-tail pass inert,
106+
// matching the pre-rerouting behavior where it never ran.
107+
size: 0,
108+
isSymbolicLink: () => false,
109+
isFile: () => true,
110+
}) as unknown as fs.Stats,
111+
);
86112

87113
vi.mocked(jsonl.read).mockResolvedValue([]);
88114
vi.mocked(jsonl.readLines).mockResolvedValue([]);

‎packages/core/src/utils/no-follow-open.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,14 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
108108
vi.resetModules();
109109
vi.doMock('node:fs', async (importOriginal) => {
110110
const actual = await importOriginal<typeof import('node:fs')>();
111-
return {
111+
// The helper uses a DEFAULT import of node:fs, so the `default`
112+
// property must carry the stubbed constants too (`...actual` alone
113+
// would keep the real default binding with the real O_NOFOLLOW).
114+
const modified = {
112115
...actual,
113116
constants: { ...actual.constants, O_NOFOLLOW: undefined },
114117
};
118+
return { ...modified, default: modified };
115119
});
116120
const mockedFs = await import('node:fs');
117121
const { openNoFollow: openFallback, openSyncNoFollow: openSyncFallback } =
@@ -172,7 +176,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
172176
vi.resetModules();
173177
vi.doMock('node:fs', async (importOriginal) => {
174178
const actual = await importOriginal<typeof import('node:fs')>();
175-
return {
179+
const modified = {
176180
...actual,
177181
constants: { ...actual.constants, O_NOFOLLOW: undefined },
178182
fstatSync: ((fd: number) => {
@@ -184,6 +188,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
184188
);
185189
}) as typeof actual.fstatSync,
186190
};
191+
return { ...modified, default: modified };
187192
});
188193

189194
try {
@@ -210,7 +215,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
210215
vi.resetModules();
211216
vi.doMock('node:fs', async (importOriginal) => {
212217
const actual = await importOriginal<typeof import('node:fs')>();
213-
return {
218+
const modified = {
214219
...actual,
215220
constants: { ...actual.constants, O_NOFOLLOW: undefined },
216221
lstatSync: ((p: string) => {
@@ -222,6 +227,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
222227
);
223228
}) as typeof actual.lstatSync,
224229
};
230+
return { ...modified, default: modified };
225231
});
226232

227233
try {

‎packages/core/src/utils/no-follow-open.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@
3232
* Every refusal is reported as an error with `code: 'ELOOP'` — the same
3333
* code POSIX `O_NOFOLLOW` produces — so existing `ELOOP` handling in
3434
* callers applies to the fallback path unchanged.
35+
*
36+
* `node:fs` is bound through the DEFAULT import (not a namespace import)
37+
* so suites that spy the fs object — the way `sessionService.rename.test.ts`
38+
* spies `openSync`/`readSync` for its fabricated session paths — intercept
39+
* this helper's calls too: vitest hands namespace imports their own copy of
40+
* an externalized CJS module, which escapes those spies (#8227).
3541
*/
3642

37-
import * as fs from 'node:fs';
43+
import fs from 'node:fs';
3844
import type { FileHandle } from 'node:fs/promises';
3945

4046
import { hasVerifiableInode } from './file-identity.js';

0 commit comments

Comments
 (0)