Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c067b92
fix(core): keep no-follow reads protected where O_NOFOLLOW is missing
yiliang114 Aug 25, 2026
bdecd4c
fix(core): keep the no-follow helper mockable by fs spy suites
yiliang114 Aug 25, 2026
84df828
fix(core): distinguish inode-unverifiable refusals from ELOOP
yiliang114 Aug 25, 2026
55ce9d7
refactor(core): drop the unused flags/mode params from the no-follow …
yiliang114 Aug 25, 2026
47d0757
test(core): pin the symlink refusal for the plural session-field read
yiliang114 Aug 25, 2026
88d16cb
test(core): pin the async identity re-check of the no-follow fallback
yiliang114 Aug 25, 2026
7354a1f
fix(acp): keep no-follow helper off the core barrel
yiliang114 Aug 25, 2026
294ffd8
fix(cli): keep registration read off core barrel
yiliang114 Aug 26, 2026
d6e4a63
test: resolve no-follow core subpath in consumers
yiliang114 Aug 26, 2026
c60046d
test(core): exercise output-tail no-follow fallback
yiliang114 Aug 26, 2026
b0d5da7
test(cli): pin unverifiable registration identity
yiliang114 Aug 26, 2026
b89e739
Merge branch 'main' into fix/issue-8227-windows-nofollow
wenshao Aug 26, 2026
842bd9f
Merge remote-tracking branch 'fork114/fix/issue-8227-windows-nofollow'
yiliang114 Aug 26, 2026
3af3575
build: map noFollowOpen subpath in typecheck programs
yiliang114 Aug 26, 2026
e2b5965
test(core): pin fd close, dev check, and pre-open snapshot
yiliang114 Aug 26, 2026
a74e249
test(core): pin inode-0 degradation branches in sessionArtifacts and …
yiliang114 Aug 26, 2026
4fae763
test(core): run sync fallback identity re-checks on all platforms
yiliang114 Aug 26, 2026
3030173
build(cli): map noFollowOpen subpath in cli typecheck paths
yiliang114 Aug 26, 2026
5aec7c2
test(core): dedupe no-follow-open fallback mocks, pin best-effort close
yiliang114 Aug 26, 2026
e2c80ed
Merge branch 'main' into fix/issue-8227-windows-nofollow
Aug 27, 2026
07c31e8
Merge branch 'main' into fix/issue-8227-windows-nofollow
yiliang114 Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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>
  • Loading branch information
yiliang114 and Qwen-Coder committed Aug 25, 2026
commit bdecd4c8534ea1ce7d9d8c37c4fd8c88aa306c20
26 changes: 26 additions & 0 deletions packages/core/src/services/sessionService.rename.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,32 @@ describe('SessionService - rename and custom title', () => {
vi.spyOn(fs, 'openSync').mockReturnValue(42);
readSyncSpy = vi.spyOn(fs, 'readSync').mockReturnValue(0);
vi.spyOn(fs, 'closeSync').mockImplementation(() => undefined);
// Platforms without O_NOFOLLOW (Windows) open session files through an
// lstat -> open -> fstat identity check (openSyncNoFollow). Spy both
// stats so that fallback accepts the fabricated paths above: a regular
// (non-symlink) file whose identity trivially matches itself. On
// platforms with the flag the spies stay inert.
vi.spyOn(fs, 'lstatSync').mockImplementation(
() =>
({
dev: 1,
ino: 1,
isSymbolicLink: () => false,
isFile: () => true,
}) as unknown as fs.Stats,
);
vi.spyOn(fs, 'fstatSync').mockImplementation(
() =>
({
dev: 1,
ino: 1,
// size 0 keeps readLatestTailIfGrown's grown-tail pass inert,
// matching the pre-rerouting behavior where it never ran.
size: 0,
isSymbolicLink: () => false,
isFile: () => true,
}) as unknown as fs.Stats,
);

vi.mocked(jsonl.read).mockResolvedValue([]);
vi.mocked(jsonl.readLines).mockResolvedValue([]);
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/utils/no-follow-open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,14 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
vi.resetModules();
vi.doMock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
// The helper uses a DEFAULT import of node:fs, so the `default`
// property must carry the stubbed constants too (`...actual` alone
// would keep the real default binding with the real O_NOFOLLOW).
const modified = {
...actual,
constants: { ...actual.constants, O_NOFOLLOW: undefined },
};
return { ...modified, default: modified };
});
const mockedFs = await import('node:fs');
const { openNoFollow: openFallback, openSyncNoFollow: openSyncFallback } =
Expand Down Expand Up @@ -172,7 +176,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
vi.resetModules();
vi.doMock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
const modified = {
...actual,
constants: { ...actual.constants, O_NOFOLLOW: undefined },
fstatSync: ((fd: number) => {
Comment thread
yiliang114 marked this conversation as resolved.
Outdated
Expand All @@ -184,6 +188,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
);
}) as typeof actual.fstatSync,
};
return { ...modified, default: modified };
});

try {
Expand All @@ -210,7 +215,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
vi.resetModules();
vi.doMock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
const modified = {
...actual,
constants: { ...actual.constants, O_NOFOLLOW: undefined },
lstatSync: ((p: string) => {
Expand All @@ -222,6 +227,7 @@ describe('openNoFollow without O_NOFOLLOW (Windows flag set)', () => {
);
}) as typeof actual.lstatSync,
};
return { ...modified, default: modified };
});

try {
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/utils/no-follow-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@
* Every refusal is reported as an error with `code: 'ELOOP'` — the same
* code POSIX `O_NOFOLLOW` produces — so existing `ELOOP` handling in
* callers applies to the fallback path unchanged.
*
* `node:fs` is bound through the DEFAULT import (not a namespace import)
* so suites that spy the fs object — the way `sessionService.rename.test.ts`
* spies `openSync`/`readSync` for its fabricated session paths — intercept
* this helper's calls too: vitest hands namespace imports their own copy of
* an externalized CJS module, which escapes those spies (#8227).
*/

import * as fs from 'node:fs';
import fs from 'node:fs';
Comment thread
yiliang114 marked this conversation as resolved.
import type { FileHandle } from 'node:fs/promises';

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