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:
Codeman maintainer
2026-10-05 20:00:30 +02:00
parent aca23aa404
commit 88f5a43a9f
17 changed files with 335 additions and 101 deletions
+58 -58
View File
@@ -1227,66 +1227,66 @@ describe('Grok quick start', () => {
expect(names).toEqual(['w1-grok-case', 'w2-grok-case', 'w3-grok-case']);
expect(selected).toEqual(['sess-gk-0']);
});
});
describe('case lookup before a local launch', () => {
function loadLaunchHarness(caseAnswer: Record<string, unknown>) {
const elements: Record<string, any> = {
quickStartCase: { value: 'nas-case' },
shellCount: { value: '1' },
tabCount: { value: '1' },
};
const requests: Array<{ url: string; method?: string }> = [];
const written: string[] = [];
const CodemanApp = function CodemanApp(this: any) {};
const context = vm.createContext({
CodemanApp,
localStorage: { getItem: () => null, setItem: () => {} },
document: { getElementById: (id: string) => elements[id] ?? null },
fetch: async (url: string, init?: { method?: string }) => {
requests.push({ url, method: init?.method });
if (url === '/api/cases/nas-case') return { json: async () => caseAnswer };
if (url === '/api/cases' && init?.method === 'POST') {
return {
json: async () => ({
success: true,
data: { case: { name: 'nas-case', path: '/home/u/codeman-cases/nas-case' } },
}),
};
}
// Anything past the case lookup is out of scope here: stop the launch.
throw new Error(`stop: ${url}`);
},
console,
});
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
const app = new (CodemanApp as any)();
app.terminal = { clear: () => {}, writeln: (line: string) => written.push(line), focus: () => {} };
app.sessions = new Map();
app.cases = [];
app.getTerminalDimensions = () => null;
app._readTabCount = () => 1;
app.loadAppSettingsFromStorage = () => ({});
app.getCaseSettings = () => ({});
return { app, requests, written };
}
describe('case lookup before a local launch', () => {
function loadLaunchHarness(caseAnswer: Record<string, unknown>) {
const elements: Record<string, any> = {
quickStartCase: { value: 'nas-case' },
shellCount: { value: '1' },
tabCount: { value: '1' },
};
const requests: Array<{ url: string; method?: string }> = [];
const written: string[] = [];
const CodemanApp = function CodemanApp(this: any) {};
const context = vm.createContext({
CodemanApp,
localStorage: { getItem: () => null, setItem: () => {} },
document: { getElementById: (id: string) => elements[id] ?? null },
fetch: async (url: string, init?: { method?: string }) => {
requests.push({ url, method: init?.method });
if (url === '/api/cases/nas-case') return { json: async () => caseAnswer };
if (url === '/api/cases' && init?.method === 'POST') {
return {
json: async () => ({
success: true,
data: { case: { name: 'nas-case', path: '/home/u/codeman-cases/nas-case' } },
}),
};
}
// Anything past the case lookup is out of scope here: stop the launch.
throw new Error(`stop: ${url}`);
},
console,
});
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
const app = new (CodemanApp as any)();
app.terminal = { clear: () => {}, writeln: (line: string) => written.push(line), focus: () => {} };
app.sessions = new Map();
app.cases = [];
app.getTerminalDimensions = () => null;
app._readTabCount = () => 1;
app.loadAppSettingsFromStorage = () => ({});
app.getCaseSettings = () => ({});
return { app, requests, written };
}
const unreachable = { success: false, error: 'Case folder is not responding', errorCode: 'OPERATION_FAILED' };
const missing = { success: false, error: 'Case not found', errorCode: 'NOT_FOUND' };
const unreachable = { success: false, error: 'Case folder is not responding', errorCode: 'OPERATION_FAILED' };
const missing = { success: false, error: 'Case not found', errorCode: 'NOT_FOUND' };
for (const launcher of ['runClaude', 'runShell'] as const) {
it(`${launcher} never creates a case when the lookup could not tell whether it exists`, async () => {
const { app, requests, written } = loadLaunchHarness(unreachable);
await app[launcher]();
expect(requests.some((r) => r.url === '/api/cases' && r.method === 'POST')).toBe(false);
expect(written.join('\n')).toContain('Case folder is not responding');
});
for (const launcher of ['runClaude', 'runShell'] as const) {
it(`${launcher} never creates a case when the lookup could not tell whether it exists`, async () => {
const { app, requests, written } = loadLaunchHarness(unreachable);
await app[launcher]();
expect(requests.some((r) => r.url === '/api/cases' && r.method === 'POST')).toBe(false);
expect(written.join('\n')).toContain('Case folder is not responding');
});
it(`${launcher} creates the case when the lookup says it does not exist`, async () => {
const { app, requests } = loadLaunchHarness(missing);
await app[launcher]();
expect(requests.some((r) => r.url === '/api/cases' && r.method === 'POST')).toBe(true);
});
}
});
it(`${launcher} creates the case when the lookup says it does not exist`, async () => {
const { app, requests } = loadLaunchHarness(missing);
await app[launcher]();
expect(requests.some((r) => r.url === '/api/cases' && r.method === 'POST')).toBe(true);
});
}
});