Skip to content

Commit e7d3adb

Browse files
committed
Drain review requests before stopping question agents
1 parent 9efbe9f commit e7d3adb

3 files changed

Lines changed: 33 additions & 1 deletion

File tree

‎docs/implementation/read-only-review.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,7 @@ The review's polling-efficiency observation is tracked separately in issue #12:
100100
PR #11 round 7 confirmed cancellation tracking and described outdated snippet retries in its summary. Direct UI reassignment of owned code is already rejected; a persisted assignment change reproduced the stale-reference case, now guarded in both manager and retry UI. The summary also mentioned in-flight composer state: browser tests reproduced lost typing during submission. Action responses now capture current drafts before rendering and clear only the unchanged submitted draft/attachment, preserving edits and drafts on other items.
101101

102102
PR #11 round 8 returned no inline findings and identified one related summary gap: completed answers only warned for an older snapshot or plan revision, not a snippet invalidated by reassignment. Completed answers now show the same historical-context warning for either condition. A browser regression preserves the historical answer while checking the warning after a persisted assignment change.
103+
104+
PR #11 round 9 returned no inline findings and identified shutdown ordering in its summary. Server shutdown now stops accepting connections and drains in-flight HTTP requests before closing the question manager and database. A partial-body request regression proves a question already admitted during shutdown starts its agent and is persisted as interrupted rather than left unanswered.
105+
106+
Current PR validation: 187 unit/integration tests, 31 browser tests, typecheck, and diff checks. Earlier counts above identify the stage when each behavior was added.

‎test/browser/review.spec.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
33
import { tmpdir } from 'node:os';
44
import { join } from 'node:path';
55
import { execFileSync } from 'node:child_process';
6+
import { request as httpRequest } from 'node:http';
67
import { createDemo } from '../../scripts/demo.ts';
78
import { choiceKeys } from '../../core/approvals.ts';
9+
import { ReviewService } from '../../runner/review.ts';
810
import { startServer } from '../../web/server.ts';
911
let root: string, app: Awaited<ReturnType<typeof startServer>>;
1012
test.beforeEach(async () => { root=mkdtempSync(join(tmpdir(),'codeboost-browser-'));app=await startServer(createDemo(join(root,'demo')),0); });
@@ -258,3 +260,25 @@ test('warns on completed answers when a snippet assignment changes',async({page}
258260
await expect(page.locator('.agent-answer')).toContainText('Historical answer');
259261
await expect(page.locator('.agent-answer')).toContainText('Answer refers to earlier code or review context.');
260262
});
263+
test('drains an in-flight question request before closing its agent manager',async()=>{
264+
const config=app.service.config;await app.close();let calls=0;
265+
app=await startServer(config,0,(_prompt,signal)=>{calls++;return new Promise((_,reject)=>signal.addEventListener('abort',()=>reject(signal.reason),{once:true}));});
266+
const view=app.service.load(),body=JSON.stringify({action:'note',item:'P1',kind:'question',text:'Question during shutdown',token:view.token});
267+
const endpoint=new URL('/api/action',app.url);
268+
let response='';
269+
const completed=new Promise<void>((resolve,reject)=>{
270+
const req=httpRequest(endpoint,{method:'POST',headers:{'x-codeboost-token':app.token,'content-type':'application/json','content-length':Buffer.byteLength(body)}},res=>{
271+
res.setEncoding('utf8');res.on('data',chunk=>response+=chunk);res.on('end',resolve);
272+
});
273+
req.on('error',reject);req.write(body.slice(0,1));
274+
setTimeout(()=>req.end(body.slice(1)),50);
275+
});
276+
await new Promise(resolve=>setTimeout(resolve,10));
277+
await Promise.all([app.close(),completed]);
278+
const reopened=new ReviewService(config);
279+
try {
280+
const note=reopened.load().notes.find(note=>note.text==='Question during shutdown');
281+
expect(calls).toBe(1);expect(note?.answer?.status).toBe('failed');expect(note?.answer?.error).toMatch(/Server stopped/);
282+
expect(JSON.parse(response).notes.some((candidate:{text:string})=>candidate.text==='Question during shutdown')).toBe(true);
283+
} finally {reopened.close();app=await startServer(config,0);}
284+
});

‎web/server.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,9 @@ export async function startServer(config: ReviewConfig, port = 4318, questionAge
5656
server.requestTimeout = 15000;
5757
await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); }); }).catch(error => { service.close(); throw error; });
5858
const address = server.address(); if (!address || typeof address === 'string') throw new Error('Cannot determine local address.');
59-
return { server, service, token, url: `http://127.0.0.1:${address.port}/#${token}`, close: async () => { await questions.close(); await new Promise<void>((resolve, reject) => server.close(error => { service.close(); error ? reject(error) : resolve(); })); } };
59+
return { server, service, token, url: `http://127.0.0.1:${address.port}/#${token}`, close: async () => {
60+
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
61+
await questions.close();
62+
service.close();
63+
} };
6064
}

0 commit comments

Comments
 (0)