refactor: dedup setSessionId calls in session-switch path; add test coverage

Code review of 893f049 flagged 6 findings. This commit addresses them all:

#1 (real issue, production code): setSessionId was being called
TWICE per session switch - once explicitly from the cell-action
callback, and again from the prompt's _handleSwitchSession (which
also assigned _sessionId, called _resetStreamPointers, and set
_sessionSelect.value directly). The explicit setSessionId in the
cell-action callbacks (onCreateSession, onSwitchSession) is now
removed; the prompt's handlers do all of that bookkeeping through
a single setSessionId() call, which already implements the
dropdown + reset-stream-pointers logic.

#2 / #4 (test coverage): the existing switching test only checked
that the right HTTP call was made. New assertions: the prompt's
session-messages mock IS called again (history actually reloaded)
and the rendered history reflects the new session's content
(verifies the user sees the right messages, not stale DOM).

#3 (test coverage): new test for the error path. bind_existing
returns ok:false -> the prompt rolls the dropdown back to the
previous active id, leaves _sessionId unchanged, and shows a
Notification.error. This locks in the rollback behavior so a
future refactor can't silently break it.

#5 (test cleanup): mockedCallOpenCodeSessionMessages (the spy used
to verify history reload) is now declared at the top of the spec
file alongside the other mocked* variables, with a meaningful
name (mockedSessionMessages) instead of an inline jest.mocked()
that was hard to find.

#6 (source-doc): added a TODO(frontend) comment on
callOpenCodeSetActiveSession explaining why it is currently
unused (kept for a future 'switch between already-bound sessions'
flow that would avoid the bind round-trip) and warning a future
maintainer not to delete the corresponding server route without
removing the client function (or vice versa).

Tests: 77 jest (was 76), 73 pytest, build green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-28 17:00:29 +08:00
co-authored by Claude
parent 893f049eb7
commit 4ff5d7021c
4 changed files with 76 additions and 16 deletions
+58 -3
View File
@@ -128,7 +128,7 @@ jest.mock('../api/opencode_client', () => ({
callOpenCodeSetActiveSession: jest.fn().mockResolvedValue({
ok: true,
notebookPath: '',
activeSessionId: 'sess-B',
activeSessionId: '',
}),
callOpenCodeBindExistingSession: jest.fn().mockResolvedValue({
ok: true,
@@ -151,6 +151,7 @@ import {
callOpenCodeCreateNotebookSession,
callOpenCodeSetActiveSession,
callOpenCodeBindExistingSession,
callOpenCodeSessionMessages,
subscribeOpenCodeEvents
} from '../api/opencode_client';
import { Notification } from '@jupyterlab/apputils';
@@ -174,6 +175,9 @@ const mockedSetActiveSession = callOpenCodeSetActiveSession as jest.MockedFuncti
const mockedBindExistingSession = callOpenCodeBindExistingSession as jest.MockedFunction<
typeof callOpenCodeBindExistingSession
>;
const mockedSessionMessages = callOpenCodeSessionMessages as jest.MockedFunction<
typeof callOpenCodeSessionMessages
>;
const mockedNotification = Notification as jest.Mocked<typeof Notification>;
function makeFakeOutputs(items: any[]): any {
@@ -1115,7 +1119,7 @@ describe('OpenCodeInlinePrompt', () => {
expect((prompt as any)._sessionId).toBe('fresh-sid');
});
it('switching session calls onSwitchSession and reloads history', async () => {
it('switching session: bind_existing is called, SSE is closed, history is reloaded', async () => {
mockedListAllSessions.mockResolvedValue({
ok: true,
sessions: [
@@ -1133,8 +1137,13 @@ describe('OpenCodeInlinePrompt', () => {
(actions.node.querySelector('button') as HTMLButtonElement).click();
await new Promise(r => setTimeout(r, 5));
const prompt = (actions as any)._prompt;
// Mark sess-A as currently active.
// Mark sess-A as currently active (so we can verify the switch).
prompt.setSessionId('sess-A');
// Reset session-messages mock so we can assert it's called again.
mockedSessionMessages.mockClear();
mockedSessionMessages.mockResolvedValue({
messages: [{ role: 'user', content: 'from sess-B' }]
});
const select = prompt.node.querySelector(
'.opencode-session-select'
) as HTMLSelectElement;
@@ -1146,7 +1155,53 @@ describe('OpenCodeInlinePrompt', () => {
// because the session is not yet bound.
expect(mockedBindExistingSession).toHaveBeenCalled();
expect(mockedSetActiveSession).not.toHaveBeenCalled();
// Prompt's internal _sessionId is now the new one.
expect((prompt as any)._sessionId).toBe('sess-B');
// The prompt's history was reloaded for the new session.
expect(mockedSessionMessages).toHaveBeenCalledWith(
'foo.ipynb', expect.anything()
);
// The new history is rendered (the user sees sess-B's messages,
// not sess-A's stale DOM).
const userMsg = prompt.node.querySelector(
'.opencode-msg-user'
) as HTMLElement;
expect(userMsg.textContent).toBe('from sess-B');
});
it('switching session rolls back the dropdown + shows a Notification.error when bind fails', async () => {
mockedListAllSessions.mockResolvedValue({
ok: true,
sessions: [
{ id: 'sess-A', title: 'first' },
{ id: 'sess-B', title: 'second' },
]
});
// bind returns ok:false -> the prompt's _handleSwitchSession
// must roll back the dropdown to the previous active id.
mockedBindExistingSession.mockResolvedValue({
ok: false, notebookPath: 'foo.ipynb', error: 'upstream down',
});
const cell = makeFakeCell('x = 1', [], 'foo.ipynb');
const actions = new OpenCodeCellActions(cell);
Object.defineProperty(actions, 'parent', { value: cell, configurable: true });
(actions as any).onAfterAttach({} as any);
(actions.node.querySelector('button') as HTMLButtonElement).click();
await new Promise(r => setTimeout(r, 5));
const prompt = (actions as any)._prompt;
prompt.setSessionId('sess-A');
const select = prompt.node.querySelector(
'.opencode-session-select'
) as HTMLSelectElement;
select.value = 'sess-B';
select.dispatchEvent(new Event('change'));
await new Promise(r => setTimeout(r, 5));
// Dropdown rolled back to the previous active id (sess-A).
expect(select.value).toBe('sess-A');
// Prompt's _sessionId is unchanged.
expect((prompt as any)._sessionId).toBe('sess-A');
// User sees a notification.
expect(mockedNotification.error).toHaveBeenCalledWith('切换会话失败');
});
it('applyEvent: streaming shows raw text in real-time, then renders to HTML on session.idle', () => {