mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-13 03:58:33 +00:00
fix(watch): do not close an unassigned loop on startup watcher errors (#3121)
onWatcherError can fire while waitUntilReady is still pending, before startWatchFileLoop returns. Guard loop.close() so shutdown stays idempotent without throwing on undefined. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bcabd82310
commit
7e3ec5f34e
2 changed files with 44 additions and 6 deletions
|
|
@ -413,7 +413,7 @@ export async function watchCommandWithRunnerIdentity(
|
|||
process.once('SIGINT', stop);
|
||||
process.once('SIGTERM', stop);
|
||||
try {
|
||||
let loop: WatchFileLoop;
|
||||
let loop: WatchFileLoop | undefined;
|
||||
let fatalRefreshError: unknown;
|
||||
let configControlValid = true;
|
||||
let lastSuccessfulRefreshAt: string | undefined;
|
||||
|
|
@ -476,7 +476,7 @@ export async function watchCommandWithRunnerIdentity(
|
|||
if (shouldStopAfterWatchRefreshFailure(error, paths)) {
|
||||
fatalRefreshError = error;
|
||||
cliError(formatFatalWatchRefreshFailure(error, paths));
|
||||
void loop.close();
|
||||
void loop?.close();
|
||||
stopWatching();
|
||||
return;
|
||||
}
|
||||
|
|
@ -493,7 +493,7 @@ export async function watchCommandWithRunnerIdentity(
|
|||
`Watcher failed: ${error instanceof Error ? error.message : String(error)}. ` +
|
||||
'Watch mode is stopping.',
|
||||
);
|
||||
void loop.close();
|
||||
void loop?.close();
|
||||
stopWatching();
|
||||
},
|
||||
);
|
||||
|
|
@ -506,6 +506,7 @@ export async function watchCommandWithRunnerIdentity(
|
|||
}
|
||||
|
||||
await stopped;
|
||||
if (loop === undefined) return;
|
||||
await loop.close();
|
||||
if (fatalRefreshError !== undefined) process.exitCode = 1;
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ type MockWatcher = EventEmitter & {
|
|||
|
||||
let pendingClose: Promise<void> | undefined;
|
||||
let releaseClose: (() => void) | undefined;
|
||||
const { delayWatcherClose } = vi.hoisted(() => ({
|
||||
delayWatcherClose: { current: false },
|
||||
}));
|
||||
let lastWatcher: MockWatcher | undefined;
|
||||
|
||||
vi.mock('chokidar', () => ({
|
||||
|
|
@ -23,10 +26,9 @@ vi.mock('chokidar', () => ({
|
|||
});
|
||||
emitter.add = vi.fn();
|
||||
emitter.close = vi.fn(async () => {
|
||||
await pendingClose;
|
||||
if (delayWatcherClose.current) await pendingClose;
|
||||
});
|
||||
lastWatcher = emitter;
|
||||
queueMicrotask(() => emitter.emit('ready'));
|
||||
return emitter;
|
||||
},
|
||||
}));
|
||||
|
|
@ -51,13 +53,15 @@ afterEach(async () => {
|
|||
pendingClose = undefined;
|
||||
releaseClose = undefined;
|
||||
lastWatcher = undefined;
|
||||
delayWatcherClose.current = false;
|
||||
});
|
||||
|
||||
describe('watch loop fatal shutdown', () => {
|
||||
it('does not start another refresh while a slow watcher.close is in flight', async () => {
|
||||
const repo = await makeRepo();
|
||||
delayWatcherClose.current = true;
|
||||
const refreshCalls: string[][] = [];
|
||||
const loop: WatchFileLoop = await startWatchFileLoop(
|
||||
const loopPromise = startWatchFileLoop(
|
||||
repo,
|
||||
10,
|
||||
async (paths) => {
|
||||
|
|
@ -70,6 +74,11 @@ describe('watch loop fatal shutdown', () => {
|
|||
void loop.close();
|
||||
},
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
expect(lastWatcher).toBeDefined();
|
||||
});
|
||||
lastWatcher.emit('ready');
|
||||
const loop: WatchFileLoop = await loopPromise;
|
||||
loops.push(loop);
|
||||
|
||||
expect(refreshCalls).toEqual([[]]);
|
||||
|
|
@ -87,4 +96,32 @@ describe('watch loop fatal shutdown', () => {
|
|||
releaseClose?.();
|
||||
await loop.close();
|
||||
});
|
||||
|
||||
it('reports a watcher error during startup even if the caller has not assigned loop yet', async () => {
|
||||
const repo = await makeRepo();
|
||||
let loop: WatchFileLoop | undefined;
|
||||
const onWatcherError = vi.fn((error: unknown) => {
|
||||
void loop?.close();
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
const started = startWatchFileLoop(
|
||||
repo,
|
||||
10,
|
||||
async () => {},
|
||||
() => {},
|
||||
onWatcherError,
|
||||
).then((assigned) => {
|
||||
loop = assigned;
|
||||
return assigned;
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(lastWatcher).toBeDefined();
|
||||
});
|
||||
lastWatcher.emit('error', new Error('startup watcher failed'));
|
||||
|
||||
await expect(started).rejects.toThrow('startup watcher failed');
|
||||
expect(onWatcherError).toHaveBeenCalledOnce();
|
||||
expect(loop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue