fix: address PR review cleanup

This commit is contained in:
weiyf 2026-07-06 15:51:15 +08:00
parent 60b00c3d74
commit 9f49eea84e
12 changed files with 184 additions and 96 deletions

View file

@ -19,14 +19,14 @@
*
* False-positive suppression:
* - Skips calls whose receiver is a known non-tree-sitter library (`JSON`,
* `URL`, `marked`, `Number`).
* `URL`, `marked`, `Number`, `path`).
* - Skips calls whose first argument is a string-literal (grammar-load smoke
* tests like `_testParser.parse('service X { rpc Y (R) returns (R); }')`).
* - Skips test files (`.test.ts`/`.test.tsx`/`.spec.ts`).
* - Skips the `safe-parse.ts` helper itself.
*/
const SKIPPED_RECEIVERS = new Set(['JSON', 'URL', 'marked', 'Number', 'Math']);
const SKIPPED_RECEIVERS = new Set(['JSON', 'URL', 'marked', 'Number', 'Math', 'path']);
export default {
meta: {
@ -74,7 +74,7 @@ export default {
// Receiver-text-shape skip: anything matching well-known JS APIs that
// happen to have a `.parse(<expr>)` shape but aren't tree-sitter.
if (
/^(JSON|URL|marked|Number|Math|Date|globalThis\.JSON)\b/.test(receiverText) ||
/^(JSON|URL|marked|Number|Math|Date|path|globalThis\.JSON)\b/.test(receiverText) ||
/\bjson\.parse\b/i.test(receiverText)
) {
return;

View file

@ -68,9 +68,13 @@ async function initWatchConfig(): Promise<void> {
const configPath = getAutoSyncConfigPath();
try {
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, defaultSyncConfig(path.resolve(path.dirname(configPath), 'repo')), {
flag: 'wx',
});
await fs.writeFile(
configPath,
defaultSyncConfig(path.resolve(path.dirname(configPath), 'repo')),
{
flag: 'wx',
},
);
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
process.stderr.write(`[auto-sync] Config already exists: ${configPath}\n`);

View file

@ -209,7 +209,9 @@ export function parseAutoSyncConfig(content: string, configPath: string): AutoSy
export function validateAutoSyncRemoteUrl(remoteUrl: string): void {
const match = /^git@([^:\s/]+):([^\s]+)$/.exec(remoteUrl.trim());
if (!match) {
throw new Error('must use git@github.com:owner/repo.git, git@gitlab.com:group/repo.git, or git@gitee.com:owner/repo.git');
throw new Error(
'must use git@github.com:owner/repo.git, git@gitlab.com:group/repo.git, or git@gitee.com:owner/repo.git',
);
}
const host = match[1].toLowerCase();
const repoPath = match[2];
@ -223,7 +225,8 @@ export function validateAutoSyncRemoteUrl(remoteUrl: string): void {
export function validateAutoSyncBranchName(branch: string): void {
if (!branch.trim()) throw new Error('must not be empty');
if (/[\s\0-\x1f\x7f]/.test(branch)) throw new Error('must not contain whitespace or control characters');
if (/[\s\0-\x1f\x7f]/.test(branch))
throw new Error('must not contain whitespace or control characters');
if (/[~^:?*[\\]/.test(branch)) throw new Error('contains characters not allowed in a git ref');
if (branch.startsWith('-')) throw new Error('must not start with "-"');
if (branch.includes('..')) throw new Error('must not contain ".."');

View file

@ -65,7 +65,11 @@ export async function resolveConfiguredCloneRoot(localPath: string): Promise<Aut
await fs.mkdir(root, { recursive: true });
await assertDirectoryOwnerAndPermissions(root);
const realRoot = await fs.realpath(root);
assertContainedOrSame(root, realRoot, 'Configured clone root realpath escaped its normalized path');
assertContainedOrSame(
root,
realRoot,
'Configured clone root realpath escaped its normalized path',
);
assertNotDangerousRoot(realRoot);
assertNotGitNexusInternalRoot(realRoot);
@ -90,7 +94,10 @@ export function normalizeConfiguredCloneRoot(localPath: string): string {
return resolved;
}
export async function quarantineAutoSyncPartial(targetDir: string, quarantineRoot: string): Promise<string> {
export async function quarantineAutoSyncPartial(
targetDir: string,
quarantineRoot: string,
): Promise<string> {
await fs.mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
const base = path.basename(targetDir);
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
@ -119,7 +126,8 @@ function assertNotDangerousRoot(root: string): void {
throw new Error(`Refusing unsafe auto-sync clone root under ${dangerousRoot}: ${root}`);
}
}
if (path.parse(root).root === root) throw new Error(`Refusing filesystem root as clone root: ${root}`);
if (path.parse(root).root === root)
throw new Error(`Refusing filesystem root as clone root: ${root}`);
}
function assertNotGitNexusInternalRoot(root: string): void {
@ -151,7 +159,8 @@ async function assertNoSymlinkPath(root: string): Promise<void> {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') break;
throw err;
}
if (stat.isSymbolicLink()) throw new Error(`Refusing symlink in auto-sync clone root path: ${current}`);
if (stat.isSymbolicLink())
throw new Error(`Refusing symlink in auto-sync clone root path: ${current}`);
}
}

View file

@ -83,7 +83,10 @@ export async function runAutoSyncOnce(
const groupsToSync = new Set<string>();
const result: AutoSyncRunResult = { synced: 0, analyzed: 0, skippedAnalysis: 0, failed: 0 };
const commitInfoEntries: ProjectCommitInfoEntry[] = [];
const actualConcurrency = resolveActualConcurrency(config.maxConcurrency, deps.getAvailableMemoryGB());
const actualConcurrency = resolveActualConcurrency(
config.maxConcurrency,
deps.getAvailableMemoryGB(),
);
logger.info(
`[auto-sync] Starting sync loop with max_concurrency=${actualConcurrency} analyze_failure_threshold=${config.analyzeFailureThreshold}.`,
);
@ -182,7 +185,9 @@ export async function runAutoSyncOnce(
lastSyncTime,
};
} catch (err: unknown) {
logger.error(`[auto-sync] Repository sync failed for ${item.remoteUrl}: ${(err as Error).message}`);
logger.error(
`[auto-sync] Repository sync failed for ${item.remoteUrl}: ${(err as Error).message}`,
);
return {
kind: 'failed' as const,
project: item.project,
@ -258,7 +263,9 @@ export async function runAutoSyncOnce(
groupMembershipOk = true;
} catch (err: unknown) {
result.failed += 1;
logger.error(`[auto-sync] Group update failed for ${repoResult.project.groupName}: ${(err as Error).message}`);
logger.error(
`[auto-sync] Group update failed for ${repoResult.project.groupName}: ${(err as Error).message}`,
);
}
if (groupMembershipOk && repoResult.analyzeStatus === 'success') {
groupsToSync.add(repoResult.project.groupName);
@ -321,7 +328,10 @@ export function resolveActualConcurrency(configured: number, availableMemoryGB:
return Math.max(1, Math.min(configured, memoryLimit));
}
async function buildWorkItems(config: AutoSyncConfig, deps: AutoSyncRunDeps): Promise<AutoSyncWorkItem[]> {
async function buildWorkItems(
config: AutoSyncConfig,
deps: AutoSyncRunDeps,
): Promise<AutoSyncWorkItem[]> {
const items: AutoSyncWorkItem[] = [];
const targetOwners = new Map<string, string>();
for (const project of config.projects) {
@ -332,7 +342,9 @@ async function buildWorkItems(config: AutoSyncConfig, deps: AutoSyncRunDeps): Pr
const targetDir = getConfiguredRepoPath({ localPath: cloneRoot.root }, repoName);
const previous = targetOwners.get(targetDir);
if (previous !== undefined) {
throw new Error(`Duplicate auto-sync targetDir ${targetDir} for ${previous} and ${remoteUrl}`);
throw new Error(
`Duplicate auto-sync targetDir ${targetDir} for ${previous} and ${remoteUrl}`,
);
}
targetOwners.set(targetDir, remoteUrl);
} catch (err: unknown) {

View file

@ -48,15 +48,17 @@ export function getAutoSyncWatchPaths(gitnexusDir = getGlobalDir()): AutoSyncWat
};
}
export async function startAutoSyncWatch(options: {
setIntervalFn?: typeof setInterval;
clearIntervalFn?: typeof clearInterval;
runOnce?: typeof runAutoSyncOnce;
stderr?: Pick<NodeJS.WriteStream, 'write'>;
keepAlive?: boolean;
paths?: AutoSyncWatchPaths;
deps?: Partial<AutoSyncWatchControlDeps>;
} = {}): Promise<AutoSyncStartHandle | null> {
export async function startAutoSyncWatch(
options: {
setIntervalFn?: typeof setInterval;
clearIntervalFn?: typeof clearInterval;
runOnce?: typeof runAutoSyncOnce;
stderr?: Pick<NodeJS.WriteStream, 'write'>;
keepAlive?: boolean;
paths?: AutoSyncWatchPaths;
deps?: Partial<AutoSyncWatchControlDeps>;
} = {},
): Promise<AutoSyncStartHandle | null> {
const stderr = options.stderr ?? process.stderr;
const paths = options.paths ?? getAutoSyncWatchPaths();
const deps = resolveWatchDeps(options.deps);
@ -199,13 +201,15 @@ async function acquireWatchLock(
}
}
export async function stopAutoSyncWatch(options: {
paths?: AutoSyncWatchPaths;
stderr?: Pick<NodeJS.WriteStream, 'write'>;
deps?: Partial<AutoSyncWatchControlDeps>;
timeoutMs?: number;
pollMs?: number;
} = {}): Promise<boolean> {
export async function stopAutoSyncWatch(
options: {
paths?: AutoSyncWatchPaths;
stderr?: Pick<NodeJS.WriteStream, 'write'>;
deps?: Partial<AutoSyncWatchControlDeps>;
timeoutMs?: number;
pollMs?: number;
} = {},
): Promise<boolean> {
const stderr = options.stderr ?? process.stderr;
const paths = options.paths ?? getAutoSyncWatchPaths();
const deps = resolveWatchDeps(options.deps);
@ -428,14 +432,20 @@ async function readStatusFile(statusPath: string): Promise<WatchStatusRecord | u
}
}
async function writeWatchStatus(paths: AutoSyncWatchPaths, record: WatchStatusRecord): Promise<void> {
async function writeWatchStatus(
paths: AutoSyncWatchPaths,
record: WatchStatusRecord,
): Promise<void> {
await fs.mkdir(path.dirname(paths.statusPath), { recursive: true });
const tmpPath = `${paths.statusPath}.tmp.${process.pid}.${Date.now()}`;
await fs.writeFile(tmpPath, `${JSON.stringify(record, null, 2)}\n`, 'utf-8');
await fs.rename(tmpPath, paths.statusPath);
}
async function cleanupWatchFiles(paths: AutoSyncWatchPaths, lockHandle?: fs.FileHandle): Promise<void> {
async function cleanupWatchFiles(
paths: AutoSyncWatchPaths,
lockHandle?: fs.FileHandle,
): Promise<void> {
await lockHandle?.close().catch(() => {});
await removeIfExists(paths.pidPath);
await removeIfExists(paths.lockPath);

View file

@ -106,7 +106,12 @@ export interface ProjectCommitInfoEntry {
branch?: string;
codeCommitId?: string;
analyzedCommitId?: string;
status: AutoSyncAnalyzeStatus | 'sync_failed' | 'branch_skipped' | 'branch_unavailable' | 'sync_timeout';
status:
| AutoSyncAnalyzeStatus
| 'sync_failed'
| 'branch_skipped'
| 'branch_unavailable'
| 'sync_timeout';
analyzeConsecutiveFailures?: number;
analyzeFailureThreshold?: number;
lastAnalyzeError?: string;

View file

@ -790,11 +790,7 @@ export function buildGitEnv(
// host-scoped Authorization header (GitHub PAT for github.com, else the
// server's AZURE_DEVOPS_PAT for Azure hosts) via the GIT_CONFIG_* protocol —
// never in argv. See resolveGitCredential / buildExtraHeaderKey.
function runGit(
args: string[],
cwd?: string,
options?: RunGitOptions,
): Promise<void> {
function runGit(args: string[], cwd?: string, options?: RunGitOptions): Promise<void> {
return new Promise((resolve, reject) => {
const spawnGit = options?.spawnForTest ?? spawn;
const proc = spawnGit('git', args, {

View file

@ -13,7 +13,11 @@ import {
startAutoSyncWatch,
stopAutoSyncWatch,
} from '../../src/core/auto-sync/index.js';
import type { AutoSyncConfig, AutoSyncRunDeps, AutoSyncWatchPaths } from '../../src/core/auto-sync/index.js';
import type {
AutoSyncConfig,
AutoSyncRunDeps,
AutoSyncWatchPaths,
} from '../../src/core/auto-sync/index.js';
const config: AutoSyncConfig = {
configPath: '/tmp/.gitnexus/watch_config.yml',
@ -44,7 +48,11 @@ function withCloneRoot(deps: Partial<AutoSyncRunDeps>): Partial<AutoSyncRunDeps>
};
}
async function writeWatchOwner(paths: AutoSyncWatchPaths, pid: number, ownerId = `owner-${pid}`): Promise<string> {
async function writeWatchOwner(
paths: AutoSyncWatchPaths,
pid: number,
ownerId = `owner-${pid}`,
): Promise<string> {
await fs.mkdir(path.dirname(paths.pidPath), { recursive: true });
await fs.writeFile(paths.pidPath, `${pid}\n`);
await fs.writeFile(
@ -473,7 +481,9 @@ describe('auto-sync runner', () => {
}),
);
expect(errorLogger).toHaveBeenCalledWith(
expect.stringContaining('Repository sync failed for git@gitee.com:qts_server/failing_sync.git'),
expect.stringContaining(
'Repository sync failed for git@gitee.com:qts_server/failing_sync.git',
),
);
expect(errorLogger).toHaveBeenCalledWith(
expect.stringContaining('Analysis failed for /tmp/repos/qts_account'),
@ -540,7 +550,9 @@ describe('auto-sync runner', () => {
return remoteUrl.includes('/one.git') ? '/tmp/repos/one' : '/tmp/repos/two';
}),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn((repoPath) => (repoPath.endsWith('/one') ? 'one-commit' : 'two-commit')),
getCurrentCommit: vi.fn((repoPath) =>
repoPath.endsWith('/one') ? 'one-commit' : 'two-commit',
),
runFullAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'repo'),
loadState: vi.fn(async () => ({})),
@ -792,17 +804,10 @@ describe('auto-sync runner', () => {
await fs.mkdir(groupDir, { recursive: true });
await fs.writeFile(
path.join(groupDir, 'group.yaml'),
[
'version: 1',
'name: back_end',
'repos:',
' hr/hiring/backend: qts_account',
].join('\n'),
['version: 1', 'name: back_end', 'repos:', ' hr/hiring/backend: qts_account'].join('\n'),
);
await expect(addRepoToGroup({ groupName: 'back_end' }, 'qts_account')).resolves.toBe(
false,
);
await expect(addRepoToGroup({ groupName: 'back_end' }, 'qts_account')).resolves.toBe(false);
await expect(fs.readFile(path.join(groupDir, 'group.yaml'), 'utf-8')).resolves.toContain(
'hr/hiring/backend: qts_account',
@ -854,7 +859,9 @@ describe('auto-sync starter', () => {
expect(setIntervalFn).toHaveBeenCalledWith(expect.any(Function), 300_000);
expect(timer.unref).toHaveBeenCalled();
await vi.waitFor(() => {
expect(stderr.write).toHaveBeenCalledWith(expect.stringContaining('[auto-sync] Watch loop started at '));
expect(stderr.write).toHaveBeenCalledWith(
expect.stringContaining('[auto-sync] Watch loop started at '),
);
expect(stderr.write).toHaveBeenCalledWith(
'[auto-sync] Watch loop finished: synced=0 analyzed=0 skipped=0 failed=0.\n',
);
@ -935,7 +942,9 @@ describe('auto-sync starter', () => {
});
expect(handle).toBeNull();
expect(stderr.write).toHaveBeenCalledWith('[auto-sync] Watch is already running with pid 12345.\n');
expect(stderr.write).toHaveBeenCalledWith(
'[auto-sync] Watch is already running with pid 12345.\n',
);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
@ -1014,7 +1023,9 @@ describe('auto-sync starter', () => {
});
expect(handle).toBeNull();
expect(stderr.write).toHaveBeenCalledWith('[auto-sync] Watch is already running with pid 12345.\n');
expect(stderr.write).toHaveBeenCalledWith(
'[auto-sync] Watch is already running with pid 12345.\n',
);
expect(await fs.readFile(paths.lockPath, 'utf-8')).toContain('starting-owner');
await expect(fs.access(paths.pidPath)).rejects.toThrow();
} finally {
@ -1073,7 +1084,11 @@ describe('auto-sync starter', () => {
stopAutoSyncWatch({
paths,
stderr: { write: vi.fn() },
deps: { isProcessAlive: vi.fn(() => false), killProcess: vi.fn(), sleep: vi.fn(async () => {}) },
deps: {
isProcessAlive: vi.fn(() => false),
killProcess: vi.fn(),
sleep: vi.fn(async () => {}),
},
}),
).resolves.toBe(false);
@ -1146,7 +1161,9 @@ describe('auto-sync starter', () => {
}),
).resolves.toBe(false);
await expect(readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) })).resolves.toMatchObject({
await expect(
readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) }),
).resolves.toMatchObject({
state: 'stopping',
pid: 12345,
message: expect.stringContaining('did not exit'),
@ -1182,7 +1199,9 @@ describe('auto-sync starter', () => {
).resolves.toBe(false);
expect(killProcess).not.toHaveBeenCalled();
await expect(readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) })).resolves.toMatchObject({
await expect(
readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) }),
).resolves.toMatchObject({
state: 'error',
pid: 12345,
message: expect.stringContaining('owner'),

View file

@ -176,14 +176,14 @@ describe('auto-sync', () => {
it('hard-fails unsafe configured clone roots', async () => {
await expect(resolveConfiguredCloneRoot('/')).rejects.toThrow('unsafe auto-sync clone root');
await expect(resolveConfiguredCloneRoot(os.homedir())).rejects.toThrow('unsafe auto-sync clone root');
await expect(resolveConfiguredCloneRoot(path.join(await fs.realpath(os.tmpdir()), 'repos'))).rejects.toThrow(
await expect(resolveConfiguredCloneRoot(os.homedir())).rejects.toThrow(
'unsafe auto-sync clone root',
);
await expect(
resolveConfiguredCloneRoot(path.join(await fs.realpath(os.tmpdir()), 'repos')),
).rejects.toThrow('unsafe auto-sync clone root');
const root = path.join(tempDir, 'repos');
await expect(resolveConfiguredCloneRoot(`${root}/../repos`)).rejects.toThrow(
'normalized',
);
await expect(resolveConfiguredCloneRoot(`${root}/../repos`)).rejects.toThrow('normalized');
});
it('rejects GitNexus internal directory descendants as clone roots', async () => {
@ -258,7 +258,9 @@ describe('auto-sync', () => {
expect(extractRepoNameFromRemoteUrl('git@gitee.com:qts_server/qts_account.git')).toBe(
'qts_account',
);
expect(extractRepoNameFromRemoteUrl('git@gitlab.com:team/subgroup/repo-name.git')).toBe('repo-name');
expect(extractRepoNameFromRemoteUrl('git@gitlab.com:team/subgroup/repo-name.git')).toBe(
'repo-name',
);
});
it('rejects unsafe repository names without sanitizing them', () => {
@ -271,11 +273,19 @@ describe('auto-sync', () => {
it('allows only github, gitlab, and gitee SSH SCP remote URLs', () => {
expect(() => validateAutoSyncRemoteUrl('git@github.com:im-fan/multica.git')).not.toThrow();
expect(() => validateAutoSyncRemoteUrl('git@gitlab.com:group/subgroup/repo.git')).not.toThrow();
expect(() => validateAutoSyncRemoteUrl('git@gitee.com:qts-ops/qts-code-engineering.git')).not.toThrow();
expect(() => validateAutoSyncRemoteUrl('https://github.com/owner/repo.git')).toThrow('must use');
expect(() => validateAutoSyncRemoteUrl('ssh://git@github.com/owner/repo.git')).toThrow('must use');
expect(() =>
validateAutoSyncRemoteUrl('git@gitee.com:qts-ops/qts-code-engineering.git'),
).not.toThrow();
expect(() => validateAutoSyncRemoteUrl('https://github.com/owner/repo.git')).toThrow(
'must use',
);
expect(() => validateAutoSyncRemoteUrl('ssh://git@github.com/owner/repo.git')).toThrow(
'must use',
);
expect(() => validateAutoSyncRemoteUrl('user@github.com:owner/repo.git')).toThrow('must use');
expect(() => validateAutoSyncRemoteUrl('git@example.com:owner/repo.git')).toThrow('host must be');
expect(() => validateAutoSyncRemoteUrl('git@example.com:owner/repo.git')).toThrow(
'host must be',
);
});
it('parses repo git timeout durations', () => {
@ -318,9 +328,7 @@ describe('auto-sync', () => {
previousStatus: 'failed',
}),
).toBe(true);
expect(shouldAnalyzeCommit({ currentCommit: 'def', previousAnalyzedCommit: 'abc' })).toBe(
true,
);
expect(shouldAnalyzeCommit({ currentCommit: 'def', previousAnalyzedCommit: 'abc' })).toBe(true);
});
it('saves state atomically and reloads it', async () => {
@ -345,13 +353,13 @@ describe('auto-sync', () => {
);
await expect(loadAutoSyncState(statePath)).resolves.toEqual({
'/tmp/repos/qts_account|master': {
codeCommitId: 'abc',
analyzedCommitId: 'abc',
lastAnalyzeStatus: 'success',
analyzeConsecutiveFailures: 2,
lastAnalyzeError: 'old error',
lastSyncTime: '2026-06-30T00:00:00.000Z',
},
codeCommitId: 'abc',
analyzedCommitId: 'abc',
lastAnalyzeStatus: 'success',
analyzeConsecutiveFailures: 2,
lastAnalyzeError: 'old error',
lastSyncTime: '2026-06-30T00:00:00.000Z',
},
});
});

View file

@ -672,7 +672,9 @@ describe('git-clone', () => {
try {
await new Promise<void>((resolve, reject) => {
const proc = spawn('git', ['init'], { cwd: root, stdio: 'ignore' });
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`git init ${code}`))));
proc.on('close', (code) =>
code === 0 ? resolve() : reject(new Error(`git init ${code}`)),
);
proc.on('error', reject);
});
await fs.rename(path.join(root, '.git'), path.join(target, '.git')).catch(async () => {
@ -719,9 +721,9 @@ describe('git-clone', () => {
).rejects.toThrow('git clone failed');
const entries = await fs.readdir(quarantineRoot);
expect(entries.some((entry) => entry.startsWith('auto-sync-') && entry.endsWith('-repo'))).toBe(
true,
);
expect(
entries.some((entry) => entry.startsWith('auto-sync-') && entry.endsWith('-repo')),
).toBe(true);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
@ -881,9 +883,9 @@ describe('git-clone', () => {
describe('extractWebRepoName — API clone compatibility', () => {
it('sanitizes repo names with spaces and unsafe directory characters at the web boundary', () => {
expect(
extractWebRepoName('https://dev.azure.com/org/project/_git/My Repo With Spaces'),
).toBe('My_Repo_With_Spaces');
expect(extractWebRepoName('https://dev.azure.com/org/project/_git/My Repo With Spaces')).toBe(
'My_Repo_With_Spaces',
);
expect(extractWebRepoName('https://example.com/team/repo$name.git')).toBe('repo_name');
});
@ -1102,9 +1104,11 @@ describe('git-clone', () => {
await vi.advanceTimersByTimeAsync(25);
let settled = false;
promise.catch(() => {}).finally(() => {
settled = true;
});
promise
.catch(() => {})
.finally(() => {
settled = true;
});
await vi.runAllTicks();
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
expect(settled).toBe(false);

View file

@ -411,14 +411,32 @@ describe('windowsHide regression', () => {
/**
* Count spawn-family invocations. The regex matches ``spawn(``,
* ``spawnSync(``, ``execFile(``, ``execFileSync(``,
* ``execFileAsync(``, ``execSync(`` as function calls — not
* destructures (``const { spawn } = ...``), not method calls
* (``.exec(``), not bare ``exec()`` (which collides with regex
* ``.exec()``; we explicitly drop it).
* ``execFileAsync(``, ``execSync(`` and simple local aliases that
* point at one of those functions as function calls — not destructures
* (``const { spawn } = ...``), not method calls (``.exec(``), not bare
* ``exec()`` (which collides with regex ``.exec()``; we explicitly
* drop it).
*/
function countSpawnCalls(codeSource: string): number {
const re =
/(^|[^a-zA-Z0-9_$.])(spawn|spawnSync|execFile|execFileSync|execFileAsync|execSync)\s*\(/gm;
const spawnFunctions = [
'spawn',
'spawnSync',
'execFile',
'execFileSync',
'execFileAsync',
'execSync',
];
const spawnNames = new Set(spawnFunctions);
const aliasRe = new RegExp(
`\\bconst\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*[^;\\n]*\\b(?:${spawnFunctions.join('|')})\\b`,
'g',
);
let aliasMatch: RegExpExecArray | null;
while ((aliasMatch = aliasRe.exec(codeSource)) !== null) {
spawnNames.add(aliasMatch[1]);
}
const re = new RegExp(`(^|[^a-zA-Z0-9_$.])(${[...spawnNames].join('|')})\\s*\\(`, 'gm');
let count = 0;
while (re.exec(codeSource) !== null) {
count++;