Files
notebook-ai-extension/src/__tests__/model_selection.spec.ts
T
dd5667cea4 feat: persist user (provider, model) selection per notebook in localStorage
When the user picks a provider+model in the inline prompt, remember
that choice across panel close/reopen AND across JupyterLab restarts,
keyed by notebookPath. If the stored provider (or the model under it)
is no longer present in the current providers payload, fall back to
the default (first provider / default[pid] model) - never silently
apply a stale value that would result in an empty or disabled select.

- New module src/components/model_selection.ts with
  loadModelSelection / saveModelSelection / clearModelSelection.
  Best-effort: localStorage may be disabled (private mode), quota
  may be exhausted, value may be corrupt - all failure modes return
  null / no-op rather than throw.
- IOpenCodeInlinePromptOptions gains notebookPath?: string.
- Constructor resolves the initial selection: prefer stored+valid,
  else first provider / default[pid]. providerSelect.change and
  modelSelect.change listeners call _persistSelection() to keep
  storage in sync.
- _rebuildModelSelect now accepts a preferredModelId; if it exists
  in the current provider's models it's used, else the default.
- OpenCodeCellActions._showPrompt passes notebookPath =
  this._context?.notebookPath so the prompt can key the storage.

Tests
-----
- 10 unit tests in model_selection.spec.ts (load/save/clear +
  malformed JSON + per-notebook independence).
- 4 prompt behavior tests in opencode_cell_actions.spec.ts
  (valid stored applied, provider removed -> default, model removed
  -> default, change -> saved).
- 72 frontend jest (was 58), 52 backend pytest (unchanged),
  build green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 17:38:53 +08:00

100 lines
2.9 KiB
TypeScript

/**
* Tests for the localStorage-backed model selection persistence helper.
* jsdom provides a real (per-test-class) localStorage.
*/
import {
clearModelSelection,
loadModelSelection,
saveModelSelection
} from '../components/model_selection';
describe('loadModelSelection', () => {
beforeEach(() => {
localStorage.clear();
});
it('returns null when no entry exists', () => {
expect(loadModelSelection('foo.ipynb')).toBeNull();
});
it('returns null for an empty notebookPath (defensive)', () => {
expect(loadModelSelection('')).toBeNull();
});
it('returns the parsed entry when storage is valid', () => {
localStorage.setItem(
'opencode_bridge:model-selection:foo.ipynb',
JSON.stringify({ providerId: 'anthropic', modelId: 'claude-sonnet-4-20250514' })
);
expect(loadModelSelection('foo.ipynb')).toEqual({
providerId: 'anthropic',
modelId: 'claude-sonnet-4-20250514'
});
});
it('returns null for malformed JSON (does not throw)', () => {
localStorage.setItem(
'opencode_bridge:model-selection:foo.ipynb',
'{not json'
);
expect(loadModelSelection('foo.ipynb')).toBeNull();
});
it('returns null when the entry is missing required fields', () => {
localStorage.setItem(
'opencode_bridge:model-selection:foo.ipynb',
JSON.stringify({ providerId: 'anthropic' })
);
expect(loadModelSelection('foo.ipynb')).toBeNull();
});
it('keys entries independently per notebookPath', () => {
saveModelSelection('a.ipynb', { providerId: 'p1', modelId: 'm1' });
saveModelSelection('b.ipynb', { providerId: 'p2', modelId: 'm2' });
expect(loadModelSelection('a.ipynb')).toEqual({
providerId: 'p1',
modelId: 'm1'
});
expect(loadModelSelection('b.ipynb')).toEqual({
providerId: 'p2',
modelId: 'm2'
});
});
});
describe('saveModelSelection + clearModelSelection', () => {
beforeEach(() => {
localStorage.clear();
});
it('writes a JSON entry that can be re-read', () => {
saveModelSelection('foo.ipynb', { providerId: 'p1', modelId: 'm1' });
expect(loadModelSelection('foo.ipynb')).toEqual({
providerId: 'p1',
modelId: 'm1'
});
});
it('overwrites an existing entry', () => {
saveModelSelection('foo.ipynb', { providerId: 'p1', modelId: 'm1' });
saveModelSelection('foo.ipynb', { providerId: 'p2', modelId: 'm2' });
expect(loadModelSelection('foo.ipynb')).toEqual({
providerId: 'p2',
modelId: 'm2'
});
});
it('clearModelSelection removes the entry', () => {
saveModelSelection('foo.ipynb', { providerId: 'p1', modelId: 'm1' });
clearModelSelection('foo.ipynb');
expect(loadModelSelection('foo.ipynb')).toBeNull();
});
it('no-op for an empty notebookPath (defensive)', () => {
expect(() =>
saveModelSelection('', { providerId: 'p1', modelId: 'm1' })
).not.toThrow();
expect(() => clearModelSelection('')).not.toThrow();
});
});