Files
ObsiGate/tests/frontend/ai-quick-actions.test.mjs
T
bruno 39bad990c0
CI / lint (push) Successful in 3m1s
CI / security (push) Failing after 2m14s
CI / test (push) Successful in 4m58s
CI / build (push) Successful in 3m17s
CI / e2e (push) Failing after 16m56s
test: ai-quick-actions suit #187 - plus de flag agent sur les actions
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).
2026-10-07 08:29:57 -04:00

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);