fix(ai): contexte applicatif de l'assistant, dossier vide et liens fiables (#88, BUG-041, BUG-042)
- BUG-041 : le contexte d'un dossier vide ne renvoie plus 404 ; il degrade vers le prompt General enrichi d'un bloc « Dossier vide ». - BUG-042 : liens de fichiers deterministes (nom -> presse-papiers, dossier -> arborescence, chemin -> viewer) et resolution du chemin contre l'index du vault (exact -> suffixe -> basename) avant ouverture, fin des 'File not found'. - #88 : app_context (documents ouverts, repertoire/vault courants, recherche + resultats affiches) et fichiers recemment modifies injectes dans le prompt General. Tests : pytest 963 passed / 6 skipped, ruff 0, mypy 0, frontend 52/52 + validate-imports 36 modules.
This commit is contained in:
+41
-14
@@ -408,6 +408,13 @@ async function main() {
|
||||
assert.ok(html.includes('data-path="docs/guide.md"'), "markdown link linkified");
|
||||
});
|
||||
|
||||
await test("_classifyPath treats a bare filename as a name, not a file", () => {
|
||||
const b = new BooksLM();
|
||||
assert.equal(b._classifyPath("readme.md"), "name", "bare filename → copy");
|
||||
assert.equal(b._classifyPath("notes/a.md"), "file", "dir+file → open");
|
||||
assert.equal(b._classifyPath("projets/2026"), "dir", "dir path → reveal");
|
||||
});
|
||||
|
||||
await test("_renderMarkdown keeps external links external", () => {
|
||||
const b = new BooksLM();
|
||||
const html = b._renderMarkdown("[site](https://example.com/page)");
|
||||
@@ -415,25 +422,45 @@ async function main() {
|
||||
assert.ok(!html.includes("bookslm-path-link"), "external link not treated as a vault path");
|
||||
});
|
||||
|
||||
await test("clicking a path link opens a file or reveals a directory", () => {
|
||||
await test("_activatePath opens files, reveals dirs, copies unknown names", async () => {
|
||||
const b = new BooksLM();
|
||||
b._activeVault = () => "TestVault";
|
||||
const opened = [];
|
||||
const revealed = [];
|
||||
const copied = [];
|
||||
b._openFileLink = (p) => { opened.push(p); return Promise.resolve(); };
|
||||
b._revealPath = (p) => { revealed.push(p); return Promise.resolve(); };
|
||||
b._copyLinkName = (p) => { copied.push(p); };
|
||||
|
||||
// Resolved file → open the canonical path (not the raw mention).
|
||||
b._resolveExistingPath = async () => ({ path: "notes/a.md", name: "a.md", type: "file" });
|
||||
await b._activatePath("a.md", "file");
|
||||
assert.deepEqual(opened, ["notes/a.md"]);
|
||||
|
||||
// Resolved directory → reveal.
|
||||
b._resolveExistingPath = async () => ({ path: "projets/2026", name: "2026", type: "directory" });
|
||||
await b._activatePath("projets/2026", "dir");
|
||||
assert.deepEqual(revealed, ["projets/2026"]);
|
||||
|
||||
// Unresolvable file mention → copy the name instead of a dead link.
|
||||
b._resolveExistingPath = async () => null;
|
||||
await b._activatePath("bidule.md", "file");
|
||||
assert.deepEqual(copied, ["bidule.md"]);
|
||||
});
|
||||
|
||||
await test("clicking a bare-name link copies the filename to the clipboard", async () => {
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
const opened = [];
|
||||
const revealed = [];
|
||||
b._openFileLink = (p) => { opened.push(p); return Promise.resolve(); };
|
||||
b._revealPath = (p) => { revealed.push(p); return Promise.resolve(); };
|
||||
const copied = [];
|
||||
b._copyLinkName = (p) => { copied.push(p); };
|
||||
|
||||
const container = b._panel.querySelector(".bookslm-messages");
|
||||
container.innerHTML = b._renderMarkdown("`notes/a.md` et `dossier/sous-dossier`");
|
||||
const fileLink = container.querySelector('a[data-kind="file"]');
|
||||
const dirLink = container.querySelector('a[data-kind="dir"]');
|
||||
assert.ok(fileLink, "file link rendered");
|
||||
assert.ok(dirLink, "dir link rendered");
|
||||
fileLink.click();
|
||||
dirLink.click();
|
||||
assert.deepEqual(opened, ["notes/a.md"]);
|
||||
assert.deepEqual(revealed, ["dossier/sous-dossier"]);
|
||||
container.innerHTML = b._renderMarkdown("Voir `readme.md` pour tout.");
|
||||
const nameLink = container.querySelector('a[data-kind="name"]');
|
||||
assert.ok(nameLink, "bare filename rendered as a name link");
|
||||
nameLink.click();
|
||||
assert.deepEqual(copied, ["readme.md"]);
|
||||
b._panel.remove();
|
||||
});
|
||||
|
||||
|
||||
+35
-3
@@ -527,15 +527,17 @@ class TestBooksLMChatEndpoint:
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_chat_empty_directory(self, bookslm_client):
|
||||
"""Chat endpoint returns 404 for empty directory."""
|
||||
def test_chat_empty_directory_does_not_block(self, bookslm_client):
|
||||
"""Chat on an empty directory must not 404 — it answers without context."""
|
||||
token, _ = _login_bookslm(bookslm_client)
|
||||
resp = bookslm_client.post(
|
||||
"/api/ai/bookslm/chat",
|
||||
json={"vault": "TestVault", "directory": "nonexistent", "message": "Hello"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
# 200 (stream opened) or 500 (no AI key) — never 404/422.
|
||||
assert resp.status_code in (200, 500), f"unexpected {resp.status_code}: {resp.text[:200]}"
|
||||
assert resp.status_code != 404
|
||||
|
||||
def test_chat_request_accepts_provider_and_model(self, bookslm_client):
|
||||
"""The chat endpoint schema accepts provider + model fields without rejecting.
|
||||
@@ -641,6 +643,36 @@ class TestGeneralPrompt:
|
||||
assert "Beta" in prompt
|
||||
assert "obsigate-action" in prompt
|
||||
|
||||
def test_general_prompt_includes_app_context(self):
|
||||
"""The General assistant is told what the user is currently doing."""
|
||||
from backend.bookslm import build_general_system_prompt
|
||||
|
||||
prompt = build_general_system_prompt(
|
||||
["Alpha"],
|
||||
app_context={
|
||||
"vault": "Alpha",
|
||||
"directory": "notes",
|
||||
"current_path": "notes/a.md",
|
||||
"open_documents": [{"vault": "Alpha", "path": "notes/a.md"}],
|
||||
"search_query": "auth",
|
||||
"search_total": 3,
|
||||
"search_results": [{"vault": "Alpha", "path": "notes/auth.md"}],
|
||||
},
|
||||
recent_files=[{"vault": "Alpha", "path": "notes/recent.md"}],
|
||||
)
|
||||
|
||||
assert "Contexte applicatif actuel" in prompt
|
||||
assert "notes/a.md" in prompt
|
||||
assert "notes/recent.md" in prompt
|
||||
assert "auth" in prompt
|
||||
|
||||
def test_general_prompt_without_app_context_is_unchanged(self):
|
||||
"""No context → no app-context block (backwards compatible)."""
|
||||
from backend.bookslm import build_general_system_prompt
|
||||
|
||||
prompt = build_general_system_prompt(["Alpha"])
|
||||
assert "Contexte applicatif actuel" not in prompt
|
||||
|
||||
def test_documents_prompt_scope(self):
|
||||
from backend.bookslm import build_system_prompt
|
||||
|
||||
|
||||
Reference in New Issue
Block a user