fix(ai): la liste des fournisseurs de l'assistant suit la configuration (BUG-043)
Le picker lit /api/ai/status une seule fois, à sa construction, et le panneau de l'assistant est un singleton monté pour toute la session : ajouter ou supprimer une clé API dans la configuration du projet laissait la liste des fournisseurs figée jusqu'à un rechargement de page. - ai.js : nouveau refreshAIPickers() qui reconstruit chaque picker monté dans son emplacement .ai-picker-slot (constante PICKER_SLOT_CLASS) ; le slot est conservé même sans fournisseur configuré, donc le premier fournisseur ajouté s'y monte aussi ; une sélection persistée dont le fournisseur n'est plus configuré est purgée de obsigate_ai_picker (retour au défaut, plus de modèle fantôme dans le déclencheur). - bookslm.js : l'emplacement .bookslm-picker-host porte la classe ai-picker-slot et reste dans la barre (replaceChildren au lieu de replaceWith). - config.js : refreshAIPickers() après saveAIKeys() et deleteAIKey(). - tests : +4 tests JSDOM (ajout/retrait dans la barre, premier montage dans un slot vide, purge de la sélection orpheline, câblage save/delete). Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped, ruff 0, et contrôle navigateur (Playwright) sur l'instance de test — ajout de nvidia visible sans rechargement, retrait effectif + sélection réinitialisée.
This commit is contained in:
@@ -42,6 +42,19 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
`e`+accent correspond à une entrée d'index précomposée) et un chemin
|
||||
**préfixé par le nom du vault** (`TestVault/Recettes/Pizza Maison.md`) est
|
||||
résolu puis ouvert dans ce vault, le préfixe étant retiré.
|
||||
- **BUG-043 — Liste des fournisseurs de l'assistant non synchronisée avec la
|
||||
configuration** : ajouter ou supprimer une clé API dans la configuration du
|
||||
projet met désormais à jour **immédiatement** le menu Fournisseur de la barre
|
||||
latérale de l'assistant, sans recharger la page. Le picker ne lisait
|
||||
`/api/ai/status` qu'une fois, à sa construction, et le panneau de l'assistant
|
||||
est un singleton monté pour toute la session : la liste restait figée. Le
|
||||
nouveau `refreshAIPickers()` (`frontend/js/ai.js`) reconstruit chaque picker
|
||||
monté dans son emplacement `.ai-picker-slot` et est appelé après
|
||||
l'enregistrement (`saveAIKeys`) et la suppression (`deleteAIKey`) d'une clé
|
||||
(`frontend/js/config.js`). Un emplacement est conservé même sans fournisseur
|
||||
configuré, donc le **premier** fournisseur ajouté s'y monte aussi. Une
|
||||
sélection dont le fournisseur n'est plus configuré est purgée de
|
||||
`obsigate_ai_picker` (retour au défaut au lieu d'un modèle fantôme).
|
||||
|
||||
### Sécurité
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| *BUG-040* | [🔵 MINEUR] Extraction PDF intégrale (100 ko) au scan de démarrage | 🔴 ouvert | P2 | ⚙️ backend | IA | `backend/indexer.py:465` | Démarrer sur un vault contenant de nombreux PDF | Analyser les PDF en tâche de fond / à la demande (lazy) | Ralentit fortement le démarrage et le rebuild d'index |
|
||||
| *BUG-041* | [🟡 IMPORTANT] Assistant IA : échec sur un répertoire vide (« Aucun fichier markdown trouvé dans ce dossier ») au lieu de répondre | 🟢 corrigé | P1 | 📱 frontend + ⚙️ backend | IA | `backend/bookslm_routes.py`, `backend/bookslm.py`, `frontend/js/bookslm.js` | Ouvrir l'assistant sur un dossier vide puis envoyer une question | `_resolve_system_prompt` dégrade vers le prompt Général + bloc « Dossier vide » (plus de 404) ; contexte applicatif `app_context` enrichi (documents ouverts, répertoire, recherche, fichiers récents) | Le 404 bloquait toute la requête. Feature #88, fiche `docs/features/ai-app-context.md`. Tests : `tests/test_bookslm.py` (+3), `tests/frontend/ai.test.mjs` |
|
||||
| *BUG-042* | [🟡 IMPORTANT] Assistant IA : liens de fichiers non fiables (« File not found: ») — pas de règle déterministe nom / dossier / chemin | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Cliquer les liens de fichiers/dossiers dans une réponse de l'assistant (noms avec espaces et/ou accents, chemin préfixé par le nom du vault) | `_classifyPath` distingue `name` (copie presse-papiers) / `dir` (révélation arborescence) / `file` (ouverture) ; `_activatePath()` résout le chemin contre l'index du vault (exact → suffixe → basename unique) avant d'agir ; espaces + accents pris en charge (classes Unicode `\p{L}\p{N}\p{M}`, comparaison normalisée NFC, markdown `<…>`/`%20`, code inline, mentions brutes confirmées par l'index) ; `_splitVaultPrefix` retire un préfixe `Vault/…` et ouvre dans ce vault (`_fetchPathsForVault`) | Les liens morts ouvraient un fichier inexistant. Feature #88. Tests : `tests/frontend/ai.test.mjs` (+11) |
|
||||
| *BUG-043* | [🟡 IMPORTANT] Assistant IA : la liste des fournisseurs de la barre latérale ne suit pas les ajouts/retraits de clés API dans la configuration du projet | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/js/bookslm.js`, `frontend/js/config.js` | Ajouter (ou supprimer) une clé de fournisseur AI dans la configuration puis observer le menu Fournisseur de l'assistant sans recharger la page | Le picker lit `/api/ai/status` **une seule fois**, à sa construction, et le panneau de l'assistant est un singleton monté pour toute la session → liste figée. Nouveau `refreshAIPickers()` (exporté par `ai.js`) qui reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` (conservé même sans fournisseur configuré, donc un premier fournisseur s'y monte aussi) ; appelé après `saveAIKeys()` et `deleteAIKey()` (`config.js`) ; une sélection dont le fournisseur n'est plus configuré est purgée de `obsigate_ai_picker` (retour au défaut + modèle effacé au lieu d'un nom fantôme) | Il fallait recharger la page pour voir un nouveau fournisseur (ou en voir disparaître un). Feature #82. Tests : `tests/frontend/ai.test.mjs` (+4) |
|
||||
|
||||
### TODOs techniques (améliorations / nouvelles tâches)
|
||||
|
||||
@@ -191,6 +192,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| 2026-09-14 | BUG-042 (complément) | Correction | `frontend/js/bookslm.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-app-context.md` | Prise en charge des **espaces** dans les noms de fichiers et chemins des liens de l'assistant : cibles markdown avec espaces / `<…>` / `%20` décodés, code inline reconnu (`_looksLikePath` élargi, rejette toujours les extraits de code), mentions brutes liées uniquement si présentes dans l'index du vault (`_linkifySpacePaths` + `_confirmPathInCache`, plus long suffixe aligné sur un mot pour ne pas avaler le mot de prose précédent). Vérifié : tests frontend 57/57 (IA) + 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-14 | BUG-041, BUG-042, #88 | Correction + feature | `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py`, `frontend/locales/{fr,en}.json`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `docs/features/ai-app-context.md` | BUG-041 : contexte de dossier vide → dégradation gracieuse vers le prompt Général + bloc « Dossier vide » (fin du 404). BUG-042 : liens de fichiers déterministes (nom → presse-papiers, dossier → arborescence, chemin → viewer) + résolution du chemin contre l'index du vault avant ouverture. #88 : `app_context` (documents ouverts, répertoire/vault courants, recherche + résultats affichés) et fichiers récemment modifiés injectés dans le prompt Général. Vérifié : pytest 963 passed / 6 skipped, ruff 0, mypy 0, tests frontend 52/52 + validate-imports 36 modules. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-14 | BUG-042 (accents + préfixe vault) | Correction | `frontend/js/bookslm.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-app-context.md` | **Caractères accentués** : classes de caractères Unicode (`\p{L}\p{N}\p{M}`) pour `PATH_WITH_DIR_RE` et `PATH_NAME_RE` (`_looksLikePath`), comparaison normalisée `NFC` via `_normKey()` → une mention décomposée (`e` + accent combinant, style macOS) correspond à une entrée d'index précomposée, et les liens sans espace mais accentués sont enfin produits. **Préfixe de vault** : `_splitVaultPrefix()` retire un premier segment égal à un vault connu (`TestVault/Recettes/Pizza Maison.md`) ; `_fetchPathsForVault()` interroge l'index de ce vault sans écraser le cache du vault actif ; `_openFileLink`/`_revealPath` reçoivent le vault cible ; repli « retirer le premier segment » si le préfixe est inconnu. Vérifié : tests frontend 62/62 (IA) + 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped, et vérification contre l'instance live (données réelles accentuées/espacées : `Recettes/Préparation.md`, `Recettes/Pâtes carbonara.md`, `Recettes/Pizza Maison.md`) — 8/8 contrôles. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-14 | BUG-043, #82 | Correction | `frontend/js/ai.js`, `frontend/js/bookslm.js`, `frontend/js/config.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-provider-picker.md` | La liste des fournisseurs de la barre latérale de l'assistant suit désormais la configuration du projet : nouveau `refreshAIPickers()` (`ai.js`) qui reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` (host conservé dans la barre de l'assistant, montage par `replaceChildren`), appelé après `saveAIKeys()` et `deleteAIKey()` (`config.js`) ; slot préservé même sans fournisseur configuré (le premier fournisseur ajouté s'y monte) ; sélection persistée d'un fournisseur non configuré purgée de `obsigate_ai_picker` (retour au défaut, modèle effacé). Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM vertes, validate-imports 36 modules, pytest 963 passed / 6 skipped, ruff OK, et vérification en navigateur (Playwright) sur l'instance de test : ajout de `nvidia` → fournisseur visible sans rechargement, retrait → disparition + sélection réinitialisée (`{"provider":null,"model":null}`), un seul montage du picker dans son host. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@
|
||||
| 79 | Assistant IA — Outils (function calling) & serveur MCP | 2.3.0 | [features/ai-tools-mcp.md](./features/ai-tools-mcp.md) |
|
||||
| 80 | Assistant IA — Rendu Markdown, liens fichiers/paths & sessions | 2.3.0 | [features/ai-assistant-ux.md](./features/ai-assistant-ux.md) |
|
||||
| 81 | Assistant IA — Commandes (`@`, `/`), skills, images & capacités des modèles | 2.3.0 | [features/ai-assistant-commands.md](./features/ai-assistant-commands.md) |
|
||||
| 82 | Assistant IA — Section « Fournisseur & modèle » compacte (recherche modèle + info capacités) | 2.3.0 | [features/ai-provider-picker.md](./features/ai-provider-picker.md) |
|
||||
| 82 | Assistant IA — Section « Fournisseur & modèle » compacte (recherche modèle + info capacités, BUG-043) | 2.3.0 | [features/ai-provider-picker.md](./features/ai-provider-picker.md) |
|
||||
| 69 | Éditeur mobile natif — Interface tactile optimisée | 2.3.0 | [features/mobile-editor.md](./features/mobile-editor.md) |
|
||||
| 70 | Recherche sémantique — Embeddings vectoriels (hybride TF-IDF + RRF) | 2.3.0 | [features/semantic-search.md](./features/semantic-search.md) |
|
||||
| 77 | Application Desktop native — Tauri | 🔵 en cours | [features/desktop-tauri.md](./features/desktop-tauri.md) |
|
||||
|
||||
@@ -95,6 +95,23 @@
|
||||
barre latérale désormais que le bouton ⓘ est en tête de groupe.
|
||||
- [x] Test JSDOM : l'ordre des trois contrôles visibles est vérifié (`tests/frontend/ai.test.mjs`).
|
||||
|
||||
## K. Synchronisation de la liste des fournisseurs (BUG-043) — ✅ livré
|
||||
- [x] `frontend/js/ai.js` : nouveau `refreshAIPickers()` — reconstruit chaque picker
|
||||
monté dans son emplacement `.ai-picker-slot` (constante `PICKER_SLOT_CLASS`).
|
||||
Le slot est conservé même quand aucun picker ne peut être construit (aucun
|
||||
fournisseur configuré), donc le **premier** fournisseur ajouté s'y monte aussi.
|
||||
- [x] `frontend/js/bookslm.js` : l'emplacement du picker (`span.bookslm-picker-host`)
|
||||
porte la classe `ai-picker-slot` et **reste dans la barre** — le picker est monté
|
||||
à l'intérieur au lieu de remplacer le host (`replaceChildren`).
|
||||
- [x] `frontend/js/config.js` : `refreshAIPickers()` appelé après l'enregistrement
|
||||
(`saveAIKeys()`) et la suppression (`deleteAIKey()`) d'une clé API.
|
||||
- [x] Une sélection persistée dont le fournisseur n'est plus configuré est purgée de
|
||||
`obsigate_ai_picker` (fournisseur **et** modèle effacés → retour au défaut, plus
|
||||
de nom de modèle fantôme dans le déclencheur).
|
||||
- [x] Tests JSDOM : ajout/retrait d'un fournisseur dans la barre de l'assistant,
|
||||
montage du premier fournisseur dans un slot vide, purge de la sélection orpheline,
|
||||
et garde-fou sur le câblage `saveAIKeys`/`deleteAIKey` (`tests/frontend/ai.test.mjs`).
|
||||
|
||||
## I. Points d'attention
|
||||
- La table de capacités reste **statique** (`backend/model_capabilities.py`) : un modèle inconnu
|
||||
retombe sur le défaut du fournisseur.
|
||||
|
||||
+46
-2
@@ -434,8 +434,16 @@ async function _buildPickerUI() {
|
||||
_updateCaps();
|
||||
});
|
||||
|
||||
// Load models for the initial provider selection
|
||||
if (pickerState.provider && providerNames.includes(pickerState.provider)) {
|
||||
// Load models for the initial provider selection. A provider that is no
|
||||
// longer configured (its API key was removed) is dropped from the stored
|
||||
// selection so the toolbar never advertises a stale provider/model.
|
||||
if (pickerState.provider && !providerNames.includes(pickerState.provider)) {
|
||||
pickerState.provider = null;
|
||||
pickerState.model = null;
|
||||
delete pickerState.capabilities;
|
||||
_writePicker(pickerState);
|
||||
}
|
||||
if (pickerState.provider) {
|
||||
_loadModels(pickerState.provider);
|
||||
} else {
|
||||
_syncTrigger();
|
||||
@@ -450,6 +458,40 @@ async function _buildPickerUI() {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// Mount point that survives even when no picker can be built (no configured
|
||||
// provider). Keeping it lets a later refresh mount the picker at the same spot.
|
||||
const PICKER_SLOT_CLASS = 'ai-picker-slot';
|
||||
|
||||
/**
|
||||
* Rebuild every mounted provider/model picker so the provider list matches the
|
||||
* current AI configuration of the project.
|
||||
*
|
||||
* A picker reads `/api/ai/status` once, when it is built — and the assistant
|
||||
* panel is a singleton mounted for the whole session. Without this refresh,
|
||||
* adding or removing a provider key in the project config left a stale
|
||||
* provider list in the assistant sidebar until a full page reload.
|
||||
*/
|
||||
async function refreshAIPickers() {
|
||||
const mounts = Array.from(document.querySelectorAll(`.${PICKER_SLOT_CLASS}, .ai-picker`));
|
||||
let mounted = 0;
|
||||
for (const mount of mounts) {
|
||||
// A picker living inside a slot is detached once the slot is refilled.
|
||||
if (!mount.isConnected) continue;
|
||||
const fresh = await _buildPickerUI();
|
||||
if (fresh) {
|
||||
if (mount.classList.contains(PICKER_SLOT_CLASS)) mount.replaceChildren(fresh);
|
||||
else mount.replaceWith(fresh);
|
||||
mounted += 1;
|
||||
} else if (!mount.classList.contains(PICKER_SLOT_CLASS)) {
|
||||
// Every provider was removed: keep an inert mount point in place.
|
||||
const slot = document.createElement('span');
|
||||
slot.className = PICKER_SLOT_CLASS;
|
||||
mount.replaceWith(slot);
|
||||
}
|
||||
}
|
||||
return mounted;
|
||||
}
|
||||
|
||||
// ── Get selected text from CodeMirror ──
|
||||
function getSelection(editorView) {
|
||||
if (!editorView) return '';
|
||||
@@ -978,5 +1020,7 @@ export {
|
||||
getModelCapabilities,
|
||||
renderCapabilityList,
|
||||
_buildPickerUI as buildAIPickerUI,
|
||||
refreshAIPickers,
|
||||
PICKER_SLOT_CLASS,
|
||||
_promptRewriteInstruction,
|
||||
};
|
||||
|
||||
@@ -827,7 +827,7 @@ class BooksLM {
|
||||
</div>
|
||||
<div class="bookslm-history-menu hidden"></div>
|
||||
<div class="bookslm-toolbar">
|
||||
<span class="bookslm-picker-host"></span>
|
||||
<span class="bookslm-picker-host ai-picker-slot"></span>
|
||||
</div>
|
||||
<div class="bookslm-status"></div>
|
||||
<div class="bookslm-suggestions"></div>
|
||||
@@ -854,7 +854,9 @@ class BooksLM {
|
||||
const pickerHost = panel.querySelector('.bookslm-picker-host');
|
||||
buildAIPickerUI().then((picker) => {
|
||||
if (!picker || !pickerHost) return;
|
||||
pickerHost.replaceWith(picker);
|
||||
// Mount inside the host (kept as an `ai-picker-slot`) so a provider
|
||||
// change in the config can rebuild the picker in place.
|
||||
pickerHost.replaceChildren(picker);
|
||||
if (typeof safeCreateIcons === 'function') safeCreateIcons();
|
||||
}).catch(() => { /* ignore */ });
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext,
|
||||
import { escapeHtml, safeCreateIcons } from './utils.js';
|
||||
import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js';
|
||||
import { t, setLocale, getLocale } from './i18n.js';
|
||||
import { getModelCapabilities, renderCapabilityList } from './ai.js';
|
||||
import { getModelCapabilities, renderCapabilityList, refreshAIPickers } from './ai.js';
|
||||
|
||||
let _recentTimestampTimer = null;
|
||||
let _recentFilesCache = [];
|
||||
@@ -1511,6 +1511,9 @@ async function saveAIKeys() {
|
||||
Object.keys(AI_KEY_MAP).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
|
||||
loadAIKeys();
|
||||
loadAIDefaults();
|
||||
// The assistant sidebar lists the providers it found when the picker was
|
||||
// built: rebuild it so a newly configured provider shows up immediately.
|
||||
refreshAIPickers();
|
||||
} catch(e) { showToast("Erreur: " + e.message, "error"); }
|
||||
}
|
||||
|
||||
@@ -1523,6 +1526,8 @@ async function deleteAIKey(inputId) {
|
||||
await api("/api/config/ai-keys/" + envName, { method: "DELETE" });
|
||||
showToast("Cl\u00e9 " + provider + " supprim\u00e9e", "success");
|
||||
loadAIKeys();
|
||||
// Drop the removed provider from the assistant sidebar's provider list.
|
||||
refreshAIPickers();
|
||||
} catch(e) { showToast("Erreur: " + e.message, "error"); }
|
||||
}
|
||||
|
||||
|
||||
@@ -1173,6 +1173,112 @@ async function main() {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 42c. BUG-043: the provider list follows the project AI configuration ──
|
||||
const providerOptions = (root) =>
|
||||
Array.from(root.querySelectorAll(".ai-picker-select option"))
|
||||
.map((o) => o.value)
|
||||
.filter(Boolean);
|
||||
|
||||
const providerStatusMock = (getProviders, models = ["deepseek-chat"]) => async (url) => {
|
||||
const u = String(url);
|
||||
if (u.includes("/api/ai/status")) {
|
||||
return { ok: true, status: 200, json: async () => ({ configured: true, providers: getProviders() }) };
|
||||
}
|
||||
if (u.includes("/api/config/ai-models")) {
|
||||
return { ok: true, status: 200, json: async () => ({ models, source: "api" }) };
|
||||
}
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
};
|
||||
|
||||
await test("assistant sidebar lists a provider added or removed in the project config", async () => {
|
||||
localStorage.clear();
|
||||
let providers = { deepseek: { available: true, model: "deepseek-chat" } };
|
||||
globalThis.fetch = providerStatusMock(() => providers);
|
||||
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
await sleep(30);
|
||||
const host = b._panel.querySelector(".bookslm-picker-host");
|
||||
assert.ok(host, "picker host lives in the toolbar");
|
||||
assert.deepEqual(providerOptions(host), ["deepseek"], "only the configured provider is listed");
|
||||
|
||||
// Admin saves a second provider key in the project config.
|
||||
providers = {
|
||||
deepseek: { available: true, model: "deepseek-chat" },
|
||||
nvidia: { available: true, model: "meta/llama-3.1-405b-instruct" },
|
||||
};
|
||||
await ai.refreshAIPickers();
|
||||
await sleep(10);
|
||||
assert.deepEqual(providerOptions(host), ["deepseek", "nvidia"], "added provider shows up without a reload");
|
||||
assert.ok(host.querySelector(".ai-picker"), "picker rebuilt inside its host slot");
|
||||
assert.equal(b._panel.querySelectorAll(".ai-picker").length, 1, "no duplicate picker mount");
|
||||
|
||||
// Admin deletes the key again.
|
||||
providers = { deepseek: { available: true, model: "deepseek-chat" } };
|
||||
await ai.refreshAIPickers();
|
||||
await sleep(10);
|
||||
assert.deepEqual(providerOptions(host), ["deepseek"], "removed provider disappears");
|
||||
|
||||
b._panel.remove();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
await test("refreshAIPickers mounts the first provider into an empty slot", async () => {
|
||||
localStorage.clear();
|
||||
let providers = {};
|
||||
globalThis.fetch = providerStatusMock(() => providers);
|
||||
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
await sleep(30);
|
||||
const host = b._panel.querySelector(".bookslm-picker-host");
|
||||
assert.equal(host.querySelector(".ai-picker"), null, "no picker without a configured provider");
|
||||
|
||||
providers = { deepseek: { available: true, model: "deepseek-chat" } };
|
||||
await ai.refreshAIPickers();
|
||||
await sleep(10);
|
||||
assert.ok(host.querySelector(".ai-picker"), "picker mounted into the kept slot");
|
||||
assert.deepEqual(providerOptions(host), ["deepseek"]);
|
||||
|
||||
b._panel.remove();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
await test("a provider whose key was deleted is dropped from the stored selection", async () => {
|
||||
localStorage.clear();
|
||||
globalThis.fetch = providerStatusMock(() => ({ deepseek: { available: true } }));
|
||||
|
||||
localStorage.setItem(
|
||||
"obsigate_ai_picker",
|
||||
JSON.stringify({ provider: "nvidia", model: "meta/llama-3.1-405b-instruct" })
|
||||
);
|
||||
const slot = document.createElement("span");
|
||||
slot.className = "ai-picker-slot";
|
||||
document.body.appendChild(slot);
|
||||
slot.replaceChildren(await ai.buildAIPickerUI());
|
||||
await sleep(20);
|
||||
|
||||
assert.equal(slot.querySelector(".ai-picker-select").value, "", "provider falls back to the default");
|
||||
assert.equal(slot.querySelector(".ai-picker-model").value, "", "no stale model kept");
|
||||
const stored = JSON.parse(localStorage.getItem("obsigate_ai_picker"));
|
||||
assert.equal(stored.provider, null, "unconfigured provider cleared from storage");
|
||||
assert.equal(stored.model, null, "its model cleared from storage");
|
||||
|
||||
slot.remove();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
await test("config save/delete rebuilds the assistant provider picker", async () => {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const src = readFileSync(path.join(JS_DIR, "config.js"), "utf-8");
|
||||
const save = src.slice(src.indexOf("async function saveAIKeys()"), src.indexOf("async function deleteAIKey("));
|
||||
const del = src.slice(src.indexOf("async function deleteAIKey("), src.indexOf("async function testAIKeys("));
|
||||
assert.ok(save.includes("refreshAIPickers()"), "saving a provider key refreshes the picker");
|
||||
assert.ok(del.includes("refreshAIPickers()"), "deleting a provider key refreshes the picker");
|
||||
});
|
||||
|
||||
// ── 42b. Assistant activity indicator (#82) ──
|
||||
await test("activity indicator reflects the request workflow", () => {
|
||||
const b = new BooksLM();
|
||||
|
||||
Reference in New Issue
Block a user