fix(ai): espaces dans les noms de fichiers et chemins des liens (BUG-042)
CI / lint (push) Successful in 1m17s
CI / security (push) Successful in 51s
CI / test (push) Successful in 2m30s
CI / build (push) Successful in 51s
CI / e2e (push) Successful in 10m29s

Les noms/paths de l'assistant peuvent contenir des espaces ; la detection et
la resolution les prennent desormais en charge de bout en bout :

- liens markdown : cible avec espaces, encadree par <...> ou avec un "titre",
  et URL percent-encodees (%20) decodees ;
- code inline : `Ma note.md` / `Mon dossier/Ma note.md` reconnus
  (_looksLikePath accepte les espaces, jeu de caracteres strict pour rejeter
  extraits de code et commandes shell) ;
- mentions brutes : liees uniquement si presentes dans l'index du vault
  (_linkifySpacePaths + _confirmPathInCache), avec acceptation du plus long
  suffixe aligne sur un mot -> le mot de prose precedent n'est pas avale ;
- _normalizeLinkPath : trim ; _renderMdLink : decodage %20.

Tests : frontend IA 57/57 (+5), 9 suites JSDOM, validate-imports 36 modules,
pytest 963 passed / 6 skipped.
This commit is contained in:
2026-09-14 13:20:09 -04:00
parent 314e37c433
commit 12a53d647e
5 changed files with 185 additions and 14 deletions
+59
View File
@@ -415,6 +415,65 @@ async function main() {
assert.equal(b._classifyPath("projets/2026"), "dir", "dir path → reveal");
});
// ── 12b. Spaces in file names / paths ──
await test("_looksLikePath accepts spaced names but rejects code snippets", () => {
const b = new BooksLM();
assert.equal(b._looksLikePath("Ma note.md"), true, "spaced filename is a path");
assert.equal(b._looksLikePath("Mon dossier/Ma note.md"), true, "spaced path is a path");
assert.equal(b._looksLikePath("const x = 1;"), false, "code snippet rejected");
assert.equal(b._looksLikePath("npm run test"), false, "shell command rejected");
});
await test("_renderMarkdown linkifies inline code containing spaces", () => {
const b = new BooksLM();
const html = b._renderMarkdown("Voir `Mon dossier/Ma note.md` pour le détail.");
assert.ok(html.includes('data-path="Mon dossier/Ma note.md"'), "spaced path kept intact");
assert.ok(html.includes('data-kind="file"'), "spaced file classified as file");
});
await test("_renderMarkdown linkifies markdown links with spaces and %20", () => {
const b = new BooksLM();
const spaced = b._renderMarkdown("[Ma note](Mon dossier/Ma note.md)");
assert.ok(spaced.includes('data-path="Mon dossier/Ma note.md"'), "raw spaced target kept");
const encoded = b._renderMarkdown("[Ma note](Mon%20dossier/Ma%20note.md)");
assert.ok(encoded.includes('data-path="Mon dossier/Ma note.md"'), "%20 decoded to space");
});
await test("bare spaced paths link only when present in the vault index", () => {
const b = new BooksLM();
// No index → no link (prose must not become a dead link).
b._pathCache = [];
const noCache = b._renderMarkdown("Ouvre Mon dossier/Ma note.md pour la suite");
assert.ok(!noCache.includes("bookslm-path-link"), "no link without index");
// Indexed path → link, and the leading prose word is not swallowed.
b._pathCache = [{ path: "Mon dossier/Ma note.md", name: "Ma note.md", type: "file" }];
const html = b._renderMarkdown("Ouvre Mon dossier/Ma note.md pour la suite");
assert.ok(html.includes('data-path="Mon dossier/Ma note.md"'), "indexed spaced path linkified");
assert.ok(html.includes(">Mon dossier/Ma note.md</a>"), "link text excludes leading word");
assert.ok(html.includes("Ouvre Mon") === false || !html.includes(">Ouvre Mon"), "prose word kept outside the link");
});
await test("clicking a spaced file link resolves against the index", async () => {
const b = new BooksLM();
b._activeVault = () => "TestVault";
const opened = [];
const revealed = [];
b._openFileLink = (p) => { opened.push(p); return Promise.resolve(); };
b._revealPath = (p) => { revealed.push(p); return Promise.resolve(); };
b._ensurePaths = async () => ([
{ path: "Mon dossier/Ma note.md", name: "Ma note.md", type: "file" },
{ path: "Mon dossier/Sous dossier", name: "Sous dossier", type: "directory" },
]);
await b._activatePath("Ma note.md", "file");
await b._activatePath("Sous dossier", "dir");
assert.deepEqual(opened, ["Mon dossier/Ma note.md"], "spaced filename resolves to its real path");
assert.deepEqual(revealed, ["Mon dossier/Sous dossier"], "spaced directory resolves too");
});
await test("_renderMarkdown keeps external links external", () => {
const b = new BooksLM();
const html = b._renderMarkdown("[site](https://example.com/page)");