Le test assertait encore `agent: true` sur les quick actions frontmatter, depuis la suppression du toggle mode agent (#187, v2.53.2). Job lint CI (run 853) en échec. Commit rattaché à la livraison 2.53.2 déjà publiée : pas de nouvel incrément (SKIP_VERSION_BUMP).
161 lines
7.2 KiB
JavaScript
161 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — Frontend unit tests for #106 (assistant contextual quick actions).
|
|
*
|
|
* Covers the pure logic of frontend/js/ai-quick-actions.js:
|
|
* - detectContext precedence (selection > code > multi-doc > single-doc >
|
|
* directory > general)
|
|
* - every preset id resolves to a real action (label + prompt keys exist)
|
|
* - i18n completeness: every qa.* key referenced by the catalogue exists in
|
|
* BOTH fr.json and en.json (FR/EN contract from AGENTS.md)
|
|
*
|
|
* Usage: node tests/frontend/ai-quick-actions.test.mjs
|
|
*/
|
|
|
|
import { strict as assert } from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, "..", "..");
|
|
|
|
// Minimal browser shim: i18n.js exposes `window.t = t` at module top-level.
|
|
// Strings are not loaded here, so t() falls back to the key — enough for the
|
|
// pure-logic contract asserted below (keys existence is checked against the
|
|
// real locale JSON files instead).
|
|
globalThis.window = globalThis.window || globalThis;
|
|
|
|
const {
|
|
detectContext,
|
|
suggestionsFor,
|
|
getAction,
|
|
contextBadgeKey,
|
|
ACTION_CATALOG,
|
|
CATEGORIES,
|
|
CONTEXT_PRESETS,
|
|
} = await import(pathToFileURL(path.join(ROOT, "frontend", "js", "ai-quick-actions.js")).href);
|
|
|
|
let testCount = 0;
|
|
let passCount = 0;
|
|
|
|
async function test(name, fn) {
|
|
testCount++;
|
|
try {
|
|
await fn();
|
|
passCount++;
|
|
console.log(` ✓ ${name}`);
|
|
} catch (err) {
|
|
console.error(` ✗ ${name}\n ${err.message}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
// ── detectContext precedence ───────────────────────────────────────────────
|
|
|
|
await test("selection wins over every other context", () => {
|
|
assert.equal(detectContext({ mode: "documents", docCount: 1, currentPath: "a.md", hasSelection: true }), "selection");
|
|
assert.equal(detectContext({ mode: "documents", docCount: 3, currentPath: "x.py", hasSelection: true }), "selection");
|
|
});
|
|
|
|
await test("code file drives the code context", () => {
|
|
assert.equal(detectContext({ mode: "documents", docCount: 1, currentPath: "backend/main.py" }), "code");
|
|
assert.equal(detectContext({ mode: "documents", docCount: 2, currentPath: "script.SH" }), "code");
|
|
assert.equal(detectContext({ mode: "documents", docCount: 1, currentPath: "app.tsx" }), "code");
|
|
});
|
|
|
|
await test("2+ documents drive the multi_doc context", () => {
|
|
assert.equal(detectContext({ mode: "documents", docCount: 2, currentPath: "a.md" }), "multi_doc");
|
|
});
|
|
|
|
await test("one text document drives the single_doc context", () => {
|
|
assert.equal(detectContext({ mode: "documents", docCount: 1, currentPath: "note.md" }), "single_doc");
|
|
assert.equal(detectContext({ mode: "documents", docCount: 1, currentPath: "plan.txt" }), "single_doc");
|
|
assert.equal(detectContext({ mode: "documents", docCount: 1, currentPath: "image.png" }), "single_doc");
|
|
});
|
|
|
|
await test("directory and general fall back correctly", () => {
|
|
assert.equal(detectContext({ mode: "directory", docCount: 0 }), "directory");
|
|
assert.equal(detectContext({ mode: "general", docCount: 0 }), "general");
|
|
assert.equal(detectContext(), "general");
|
|
});
|
|
|
|
// ── presets & catalogue integrity ──────────────────────────────────────────
|
|
|
|
await test("every context has 3-4 suggested actions, all resolvable", () => {
|
|
for (const [ctx, ids] of Object.entries(CONTEXT_PRESETS)) {
|
|
assert.ok(ids.length >= 3 && ids.length <= 4, `preset ${ctx} must hold 3-4 actions`);
|
|
for (const id of ids) assert.ok(getAction(id), `preset ${ctx}: unknown action id ${id}`);
|
|
assert.equal(suggestionsFor(ctx).length, ids.length);
|
|
}
|
|
});
|
|
|
|
await test("unknown context falls back to the general preset", () => {
|
|
assert.deepEqual(suggestionsFor("nonsense").map((a) => a.id), suggestionsFor("general").map((a) => a.id));
|
|
});
|
|
|
|
await test("catalogue actions have unique ids, a known category, icon and keys", () => {
|
|
const catIds = new Set(CATEGORIES.map((c) => c.id));
|
|
const seen = new Set();
|
|
for (const a of ACTION_CATALOG) {
|
|
assert.ok(!seen.has(a.id), `duplicate action id ${a.id}`);
|
|
seen.add(a.id);
|
|
assert.ok(catIds.has(a.cat), `action ${a.id}: unknown category ${a.cat}`);
|
|
assert.ok(a.icon, `action ${a.id}: missing icon`);
|
|
assert.ok(a.labelKey && a.promptKey, `action ${a.id}: missing i18n keys`);
|
|
}
|
|
});
|
|
|
|
await test("the 4 scenarios of the design table map to the expected actions", () => {
|
|
assert.deepEqual(suggestionsFor("single_doc").map((a) => a.id),
|
|
["summarize_3", "checklist", "frontmatter", "frontmatter_update"]);
|
|
assert.deepEqual(suggestionsFor("multi_doc").map((a) => a.id), ["merge", "compare", "frictions"]);
|
|
assert.deepEqual(suggestionsFor("code").map((a) => a.id), ["explain_code", "audit_code", "test_code"]);
|
|
assert.deepEqual(suggestionsFor("selection").map((a) => a.id), ["concise", "fix_style", "explain_selection"]);
|
|
});
|
|
|
|
await test("#187: frontmatter actions need no agent-mode flag (always agent)", () => {
|
|
assert.equal(getAction("frontmatter").agent, undefined);
|
|
assert.equal(getAction("frontmatter_update").agent, undefined);
|
|
});
|
|
|
|
// ── i18n completeness (FR + EN) ────────────────────────────────────────────
|
|
|
|
const FR = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "fr.json"), "utf8"));
|
|
const EN = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "en.json"), "utf8"));
|
|
|
|
await test("every catalogue/preset key exists in FR and EN (label + prompt)", () => {
|
|
const keys = new Set();
|
|
for (const a of ACTION_CATALOG) { keys.add(a.labelKey); keys.add(a.promptKey); }
|
|
for (const id of Object.values(CONTEXT_PRESETS).flat()) {
|
|
const a = getAction(id);
|
|
keys.add(a.labelKey); keys.add(a.promptKey);
|
|
}
|
|
for (const c of CATEGORIES) keys.add(c.labelKey);
|
|
for (const k of ["qa.header_suggested", "qa.all_actions", "qa.drawer_title",
|
|
"qa.search_placeholder", "qa.no_match", "qa.empty_hint", "qa.badge_single",
|
|
"qa.badge_multi", "qa.badge_code", "qa.badge_selection", "qa.badge_directory",
|
|
"qa.badge_general"]) keys.add(k);
|
|
for (const k of keys) {
|
|
assert.ok(typeof FR[k] === "string" && FR[k].length > 0, `fr.json missing ${k}`);
|
|
assert.ok(typeof EN[k] === "string" && EN[k].length > 0, `en.json missing ${k}`);
|
|
}
|
|
});
|
|
|
|
await test("context badge keys resolve per context", () => {
|
|
assert.equal(contextBadgeKey("code"), "qa.badge_code");
|
|
assert.equal(contextBadgeKey("selection"), "qa.badge_selection");
|
|
assert.equal(contextBadgeKey("multi_doc"), "qa.badge_multi");
|
|
assert.equal(contextBadgeKey("single_doc"), "qa.badge_single");
|
|
assert.equal(contextBadgeKey("directory"), "qa.badge_directory");
|
|
assert.equal(contextBadgeKey("general"), "qa.badge_general");
|
|
});
|
|
|
|
await test("multi-doc badge carries the {count} param in both locales", () => {
|
|
assert.ok(FR["qa.badge_multi"].includes("{count}"));
|
|
assert.ok(EN["qa.badge_multi"].includes("{count}"));
|
|
});
|
|
|
|
console.log(`\n${passCount} passed, ${testCount - passCount} failed`);
|
|
if (passCount !== testCount) process.exit(1);
|