fix: complete goal resume and comment input follow-ups (#3361)

This commit is contained in:
Bohdan Triapitsyn
2026-09-05 10:53:34 +03:00
committed by GitHub
parent f3f844463b
commit 7c9fdd1ee5
9 changed files with 172 additions and 62 deletions
@@ -77,7 +77,10 @@ const createRuntimeHarness = ({ messages, messageFactory, goalOverrides = {}, ma
const fetchImpl = vi.fn(async (input, init = {}) => {
const pathname = requestPath(input);
requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') return jsonResponse(activeSession);
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') {
activeSession.metadata = JSON.parse(init.body).metadata;
return jsonResponse(activeSession);
}
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(activeSession);
if (pathname === '/session/status') return jsonResponse({});
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
@@ -98,7 +101,7 @@ const createRuntimeHarness = ({ messages, messageFactory, goalOverrides = {}, ma
idleQuietMs: 10,
maxAutoTurns,
});
return { runtime, requests, service };
return { runtime, requests, service, activeSession };
};
const runIdleTick = async (runtime) => {
@@ -420,6 +423,45 @@ describe('session goal live activity gate', () => {
runtime.stop();
});
it('allows one explicit Resume after repeated truncation, then blocks another cutoff', async () => {
const first = assistantMessage('first', { finish: 'length', time: { created: 10, completed: 11 } });
const second = assistantMessage('second', { finish: 'length', time: { created: 20, completed: 21 } });
const messages = [first, second];
const { runtime, requests, activeSession } = createRuntimeHarness({ messages });
try {
await runIdleTick(runtime);
expect(lastPatchedGoal(requests).status).toBe('blocked');
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
Object.assign(activeSession.metadata.openchamber.goal, { status: 'active', statusReason: 'resumed', turnsUsed: 0 });
await runIdleTick(runtime);
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active', statusReason: '', turnsUsed: 1 });
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
messages.push(assistantMessage('third', { finish: 'length', time: { created: 30, completed: 31 } }));
await runIdleTick(runtime);
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'blocked', statusReason: 'repeated output truncation' });
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
} finally {
runtime.stop();
}
});
it.each([
{ tokenBudget: 1, error: undefined, expectedStatus: 'budgetLimited' },
{ tokenBudget: null, error: { name: 'APIError' }, expectedStatus: 'blocked' },
])('keeps $expectedStatus protection on explicit Resume', async ({ tokenBudget, error, expectedStatus }) => {
const { runtime, requests } = createRuntimeHarness({
goalOverrides: { statusReason: 'resumed', tokenBudget },
messages: [assistantMessage('resumed', { finish: 'length', error })],
});
try {
await runIdleTick(runtime);
expect(lastPatchedGoal(requests).status).toBe(expectedStatus);
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
} finally {
runtime.stop();
}
});
it('continues after a truncated agent turn followed by a length-finished summary', async () => {
const firstLength = assistantMessage('agent-length', { finish: 'length', time: { created: 10, completed: 11 } });
const summary = assistantMessage('summary', {