fix(discord): keep the thread of a discarded rebuild, close one made for a closed tab, unique tmp names, wider prune keep set

This commit is contained in:
Devvyn committed 2026-10-30 11:43:09 +08:00
1 parent b68792f988
commit a698729fb6
6 files changed
+94 -10

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ function bareServer(live: string[] = [], persisted: string[] = []): CleanupServe
persistDeb: { cancelKey: () => {} },
sse: { cleanupSessionBatches: () => {} },
// A real delete also tells the Discord bridge, which frees its per-tab state and may archive the thread.
discordBridge: { sessionDeleted: async () => {} },
discordBridge: { sessionDeleted: async () => {}, forgetTab: () => {} },
broadcast: () => {},
});
return server;
+35
View File
@@ -663,6 +663,41 @@ describe('DiscordBridge', () => {
b.stop();
});
it('forgetTab drops the per-tab state but keeps the thread and its map entry', async () => {
await writeDiscordConfig(dir, { ...CFG, onSessionDelete: 'delete' });
const b = bridge();
await b.notify(MSG);
posts.length = 0;
b.forgetTab('s1');
await b.sessionRenamed('s1', 'other'); // first sight again: recorded, no PATCH
expect(posts).toHaveLength(0);
expect(await readThreadMap(dir)).toEqual({ s1: THREAD });
b.stop();
});
it('a tab closed while its thread is being created has that thread closed and no entry kept', async () => {
await writeDiscordConfig(dir, { ...CFG, onSessionDelete: 'archive' });
let release!: () => void;
const gate = new Promise<void>((r) => (release = r));
const b = bridge({}, async (url, init) => {
const path = new URL(String(url)).pathname.replace('/api/v10', '');
posts.push({ path, method: String(init.method), body: init.body ? JSON.parse(String(init.body)) : undefined });
if (path.endsWith('/threads')) {
await gate;
return jsonRes({ id: THREAD });
}
return jsonRes({ id: 'msg' });
});
const first = b.notify(MSG);
await vi.waitFor(() => expect(posts.some((p) => p.path.endsWith('/threads'))).toBe(true));
await b.sessionDeleted('s1');
release();
await first;
expect(posts).toContainEqual({ path: `/channels/${THREAD}`, method: 'PATCH', body: { archived: true } });
expect(await readThreadMap(dir)).toEqual({});
b.stop();
});
it('renames the thread only when the name really changed', async () => {
const b = bridge();
await b.notify(MSG);
+15
View File
@@ -25,6 +25,7 @@ type Internals = {
sendPushNotifications: (event: string, data: Record<string, unknown>) => Promise<void>;
broadcast: (event: string, data: unknown) => void;
cleanupSession: (id: string, killMux?: boolean, reason?: string) => Promise<void>;
discardPartiallyBuiltSession: (id: string) => Promise<void>;
};
function server(): Internals {
@@ -35,6 +36,7 @@ function server(): Internals {
sessionCreated: vi.fn(async () => undefined),
sessionRenamed: vi.fn(async () => undefined),
sessionDeleted: vi.fn(async () => undefined),
forgetTab: vi.fn(),
sessionIdle: vi.fn(async () => undefined),
stop: vi.fn(),
};
@@ -125,6 +127,19 @@ describe('broadcast -> Discord tab lifecycle', () => {
expect(s.discordBridge.sessionDeleted).toHaveBeenCalledWith('gone');
});
it('a discarded half-built session clears the per-tab state but keeps its thread', async () => {
const s = server();
s.sessions.set('rebuild', {
owner: 'u',
ralphTracker: { stopWatchingFixPlan: () => undefined, fullReset: () => undefined },
removeAllListeners: () => undefined,
stop: async () => undefined,
});
await s.discardPartiallyBuiltSession('rebuild');
expect(s.discordBridge.forgetTab).toHaveBeenCalledWith('rebuild');
expect(s.discordBridge.sessionDeleted).not.toHaveBeenCalled();
});
it('ignores an event with no session id', () => {
const s = server();
s.broadcast(SseEvent.SessionUpdated, {});