mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix(cases): bounded path probe landing fixes (#516)
- hooks-config: a probe the bulk cap refused gets ONE bounded re-probe past the cap (probeBeforeTouching), and whatever is still unknown is skipped. The per-spawn hook and statusLine helpers used to fall back to an unbounded lstat/readFile there, which on a dead workspace never settled and could take the last threadpool workers (and hang the boot hook sweep). New test: cap engaged, stat/lstat/readFile hanging on two more paths; both helpers return. - describeUnknownPath()/unknownPathReason(): POST /api/sessions, quick-start and GET /api/cases/:name now say a folder was not checked (other mounts are still not answering) instead of blaming a healthy folder at the stall ceiling. errorCodes unchanged. - #535 x #516: Create in a custom folder probes the parent through the bounded probe before realpath/stat/lstat/readdir touch it; an unknown parent is 422 OPERATION_FAILED (UNREACHABLE) within the probe timeout. New test. - Docs: MAX_STALLED default is 2 (follows UV_THREADPOOL_SIZE), CaseInfo .unreachable covers a refused probe, the boot sweep skips an unanswering workspace, a CLAUDE.md gotcha for bounded probes, verbs.md documents the 422 (plugin mirror synced), api-reference documents the custom-folder 422. - Tests: the launcher case-lookup describe is no longer nested in the Grok block, and the cap-below-ceiling test no longer depends on an inherited UV_THREADPOOL_SIZE / CODEMAN_PATH_PROBE_MAX_STALLED. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,18 +22,26 @@ const probe = vi.hoisted(() => {
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
||||
const stat = ((path: string, ...rest: unknown[]) => {
|
||||
for (const dead of probe.dead) {
|
||||
if (String(path) === dead || String(path).startsWith(dead + '/')) {
|
||||
return new Promise((resolve, reject) => {
|
||||
probe.releases.push(() => reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })));
|
||||
void resolve;
|
||||
});
|
||||
/** A call on a dead path never settles (a hard mount), until afterEach releases it. */
|
||||
const hangOnDead = <F extends (...a: never[]) => unknown>(real: F): F =>
|
||||
((path: string, ...rest: unknown[]) => {
|
||||
for (const dead of probe.dead) {
|
||||
if (String(path) === dead || String(path).startsWith(dead + '/')) {
|
||||
return new Promise((resolve, reject) => {
|
||||
probe.releases.push(() => reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })));
|
||||
void resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return (actual.stat as (...a: unknown[]) => unknown)(path, ...rest);
|
||||
}) as typeof actual.stat;
|
||||
return { ...actual, stat, default: { ...actual, stat } };
|
||||
return (real as unknown as (...a: unknown[]) => unknown)(path, ...rest);
|
||||
}) as unknown as F;
|
||||
const hung = {
|
||||
stat: hangOnDead(actual.stat),
|
||||
lstat: hangOnDead(actual.lstat),
|
||||
readFile: hangOnDead(actual.readFile),
|
||||
realpath: hangOnDead(actual.realpath),
|
||||
};
|
||||
return { ...actual, ...hung, default: { ...actual, ...hung } };
|
||||
});
|
||||
|
||||
import { applyWorkspaceHooks, resolveStatusLineCliCommand, stripCaseEnvKeys } from '../src/hooks-config.js';
|
||||
@@ -149,6 +157,21 @@ describe('workspace helpers while other mounts are unreachable', () => {
|
||||
expect(await resolveStatusLineCliCommand(workspace, true)).toMatch(/statusline-exporter\.sh$/);
|
||||
});
|
||||
|
||||
it('skips, within the probe timeout, dead workspaces whose probes the bulk cap refused', async () => {
|
||||
// Two unrelated stalls engage the bulk cap, so probes of these two paths are
|
||||
// refused without a stat. Their lstat and readFile hang too: a helper that
|
||||
// touched them directly would never return and would hold a threadpool worker.
|
||||
await stallUnrelatedMounts(MAX_STALLED_PATH_PROBES);
|
||||
probe.dead.add('/mnt/dead-nas-c');
|
||||
probe.dead.add('/mnt/dead-nas-d');
|
||||
const within = <T>(work: Promise<T>) =>
|
||||
Promise.race([work, new Promise<'hung'>((resolve) => setTimeout(() => resolve('hung'), 2_000))]);
|
||||
|
||||
expect(await within(applyWorkspaceHooks('/mnt/dead-nas-c/project', true))).toBeUndefined();
|
||||
expect(await within(resolveStatusLineCliCommand('/mnt/dead-nas-d/project', true))).toBeUndefined();
|
||||
expect(existsSync('/mnt/dead-nas-c/project')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not inject the exporter into a workspace on the dead mount', async () => {
|
||||
probe.dead.add('/mnt/dead-nas-y');
|
||||
expect(await probePath('/mnt/dead-nas-y/project')).toBe('unknown');
|
||||
|
||||
Reference in New Issue
Block a user