mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +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:
+58
-58
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user