mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(rename): apply the server's confirmed name locally instead of waiting on SSE
Renaming a tab appeared to do nothing: the new name only showed after a full page reload. The PUT always succeeded; what was broken is how the tab strip learns the result. `finishRename()` re-renders the strip from the client-side `app.sessions` map, and nothing wrote the new name into that map, so the rename depended on the `session:updated` SSE frame to carry its own write back. On a page whose stream has gone quiet without erroring, that frame never lands and the re-render repaints the stale label. - `_applyLocalSessionName()` writes the confirmed name into `this.sessions` and refreshes cached subagent parent names, mirroring `_onSessionUpdated`. - `_putSessionName()` returns the stored name or null. `_apiPut` turns a network error into a null Response and an API failure into a non-ok status, so a rejected rename previously read as success and silently dropped the edit (the old try/catch could never fire). - Both surfaces use them: `startInlineRename()`'s `finishRename` and `saveSessionName()`. Two regression tests: the commit applies the name with no SSE frame dispatched, and a 500 restores the old label, leaves the map untouched, and toasts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -358,6 +358,83 @@ describe('Inline rename input', () => {
|
||||
expect(result.editingAfter).toBe(null);
|
||||
});
|
||||
|
||||
it('Commit writes the confirmed name into app.sessions WITHOUT any session:updated frame', async () => {
|
||||
await resetState();
|
||||
expect(await startRename('no-sse', 'w9-case')).toBe(true);
|
||||
|
||||
// finishRename() re-renders the tab strip from app.sessions, so the rename
|
||||
// used to depend on the session:updated SSE frame to carry its own write
|
||||
// back. On a page whose stream has gone quiet without erroring, the PUT
|
||||
// stored the new name, the re-render repainted the stale one, and the tab
|
||||
// only showed it after a full reload. No SSE is dispatched here at all.
|
||||
const result = await page.evaluate(async () => {
|
||||
const app = (
|
||||
window as unknown as {
|
||||
app: { sessions: Map<string, { id: string; name: string }> };
|
||||
}
|
||||
).app;
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = (async () =>
|
||||
new Response('{"success":true,"data":{"name":"w9-case: fresh"}}', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})) as typeof window.fetch;
|
||||
|
||||
const inputEl = document.querySelector('input.tab-rename-input') as HTMLInputElement;
|
||||
inputEl.value = 'fresh';
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
|
||||
window.fetch = origFetch;
|
||||
return { mapName: app.sessions.get('no-sse')?.name ?? null };
|
||||
});
|
||||
|
||||
expect(result.mapName).toBe('w9-case: fresh');
|
||||
});
|
||||
|
||||
it('A rejected rename restores the old label and leaves app.sessions untouched', async () => {
|
||||
await resetState();
|
||||
expect(await startRename('rename-500', 'w9-case')).toBe(true);
|
||||
|
||||
// _apiPut turns a network error into a null Response and an API-level
|
||||
// failure arrives as a non-ok status, neither of which throws, so a
|
||||
// rejected rename has to be detected from the response, or it reports
|
||||
// success and silently discards the user's edit.
|
||||
const result = await page.evaluate(async () => {
|
||||
const app = (
|
||||
window as unknown as {
|
||||
app: { sessions: Map<string, { id: string; name: string }>; showToast: (m: string, k: string) => void };
|
||||
}
|
||||
).app;
|
||||
const toasts: string[] = [];
|
||||
const origToast = app.showToast;
|
||||
app.showToast = (msg: string) => void toasts.push(msg);
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = (async () =>
|
||||
new Response('{"success":false,"error":"boom","errorCode":"INTERNAL"}', {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})) as typeof window.fetch;
|
||||
|
||||
const inputEl = document.querySelector('input.tab-rename-input') as HTMLInputElement;
|
||||
inputEl.value = 'never-stored';
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
|
||||
window.fetch = origFetch;
|
||||
app.showToast = origToast;
|
||||
return {
|
||||
mapName: app.sessions.get('rename-500')?.name ?? null,
|
||||
label: document.querySelector('.tab-name[data-session-id="rename-500"]')?.textContent ?? null,
|
||||
toasts,
|
||||
};
|
||||
});
|
||||
|
||||
expect(result.mapName).toBe('w9-case');
|
||||
expect(result.label).toBe('w9-case');
|
||||
expect(result.toasts).toContain('Failed to rename');
|
||||
});
|
||||
|
||||
it('Re-entry: starting rename while one is active aborts the previous one', async () => {
|
||||
await resetState();
|
||||
expect(await startRename('first-id', 'First')).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user