mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
fix(discord): say when the agent has exited, keep replies in order, settle a stalled response, prune dead tabs
- a reply to a tab whose agent has exited is answered in the thread, with no tick - inbound messages run on one chain per thread, so a reply with a file cannot be overtaken - postJson settles when a response stalls after its headers - the thread map is pruned against the live sessions once after boot
This commit is contained in:
1 parent
72a59de96c
commit
b68792f988
6 files changed
+108
-4
No files matched your search
@@ -71,6 +71,21 @@ describe('runNotify', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('postJson settles when the server sends headers and then stalls the body', async () => {
|
||||
const server = createServer((_req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': '500' });
|
||||
res.write('{"success":');
|
||||
});
|
||||
await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
|
||||
try {
|
||||
const { port } = server.address() as AddressInfo;
|
||||
expect((await postJson(`http://127.0.0.1:${port}`, '/x', {}, 100)).status).toBe(0);
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('treats a disabled bridge (409) as success that says so', async () => {
|
||||
const r = await runNotify({ message: 'x' }, ENV, '/work', fake(409));
|
||||
expect(r.code).toBe(0);
|
||||
|
||||
@@ -551,6 +551,17 @@ describe('DiscordBridge', () => {
|
||||
b.stop();
|
||||
});
|
||||
|
||||
it('prune drops thread entries for tabs that no longer exist, and keeps the live ones', async () => {
|
||||
const b = bridge();
|
||||
await b.notify(MSG);
|
||||
await b.notify({ ...MSG, sessionId: 's2', sessionName: 'w2' });
|
||||
await b.prune(['s2']);
|
||||
expect(Object.keys(await readThreadMap(dir))).toEqual(['s2']);
|
||||
await b.prune([]); // nothing restored: leave the map alone
|
||||
expect(Object.keys(await readThreadMap(dir))).toEqual(['s2']);
|
||||
b.stop();
|
||||
});
|
||||
|
||||
describe('sendTest', () => {
|
||||
it('posts with the config it is given and leaves the live config alone', async () => {
|
||||
const auth: string[] = [];
|
||||
@@ -734,7 +745,8 @@ describe('DiscordBridge files and startup hint', () => {
|
||||
function make(
|
||||
cfg: Partial<DiscordConfig> = {},
|
||||
dirFor: (id: string) => string | undefined = () => work,
|
||||
awaitingChoice?: (id: string) => boolean
|
||||
awaitingChoice?: (id: string) => boolean,
|
||||
agentExited?: (id: string) => boolean
|
||||
) {
|
||||
const b = new DiscordBridge({
|
||||
configDir: dir,
|
||||
@@ -751,6 +763,7 @@ describe('DiscordBridge files and startup hint', () => {
|
||||
sendInput: async (id, text, fromUser) => (sent.push([id, text, fromUser]), true),
|
||||
sessionDir: dirFor,
|
||||
awaitingChoice,
|
||||
agentExited,
|
||||
socketFactory: () => new FakeSocket(),
|
||||
now: () => t,
|
||||
sleep: async () => undefined,
|
||||
@@ -834,6 +847,26 @@ describe('DiscordBridge files and startup hint', () => {
|
||||
b.stop();
|
||||
});
|
||||
|
||||
it('says the agent has exited and types nothing when the tab reports it', async () => {
|
||||
const b = await make({}, undefined, undefined, () => true);
|
||||
await b.onGatewayMessage(reply({ content: 'hello', attachments: [] }));
|
||||
expect(sent).toHaveLength(0);
|
||||
expect(String(posted.at(-1)?.body?.content)).toMatch(/exited/);
|
||||
expect(posted.some((p) => p.path.includes('reactions'))).toBe(false);
|
||||
b.stop();
|
||||
});
|
||||
|
||||
it('types a reply that waits on a download before a later text reply', async () => {
|
||||
const b = await make();
|
||||
const slow = b.onGatewayMessage(reply({ content: 'first' }));
|
||||
await new Promise((r) => setTimeout(r, 0)); // the first reply is under way (gate passed) before the clock moves
|
||||
t += 5_000;
|
||||
const fast = b.onGatewayMessage(reply({ id: '555555555555555556', content: 'second', attachments: [] }));
|
||||
await Promise.all([slow, fast]);
|
||||
expect(sent.map((s) => s[1].split(' ')[0])).toEqual(['first', 'second\r']);
|
||||
b.stop();
|
||||
});
|
||||
|
||||
it('types a lone digit without Enter while a dialog is waiting, and with Enter otherwise', async () => {
|
||||
let waiting = true;
|
||||
const b = await make({}, undefined, () => waiting);
|
||||
|
||||
@@ -43,6 +43,20 @@ function server(): Internals {
|
||||
|
||||
beforeEach(() => sendNotification.mockClear());
|
||||
|
||||
describe('bridge deps', () => {
|
||||
it('agentExited is true only for a session whose pane exit was observed', () => {
|
||||
const s = new WebServer(0, false, true, '127.0.0.1', 'box') as unknown as Internals & {
|
||||
discordBridge: { deps: { agentExited: (id: string) => boolean } };
|
||||
};
|
||||
s.sessions.set('dead', { paneExit: { observedAt: 1 } });
|
||||
s.sessions.set('alive', {});
|
||||
const { agentExited } = s.discordBridge.deps;
|
||||
expect(agentExited('dead')).toBe(true);
|
||||
expect(agentExited('alive')).toBe(false);
|
||||
expect(agentExited('missing')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPushNotifications -> Discord', () => {
|
||||
it('posts a permission prompt with zero push subscriptions, with the rendered screen as the tail', async () => {
|
||||
const s = server();
|
||||
|
||||
Reference in new issue
Block a user