feat(ms-ai-architect): v1.15.0 — playground v3 project-view integration
Erstatter v2 project-surface (screen-tabs + category-tabs + per-command paste-cards)
med v3 renderProjectView (sidebar med 17 artifacts + main-area + import-modal overlay).
renderActive() ruter project-surface til renderProjectSurfaceV3() som wrapper
renderProjectView + topbar + app-shell.
V2-surface helt fjernet:
- renderProjectSurface (152 linjer)
- renderCommandSubCard (87 linjer)
- rehydratePasteImports (15 linjer)
- ACTIONS['project-screen'], currentProjectScreen
- 5 v2-CSS-klasser: .project-tabs, .project-tab*, .sub-zone, .paste-import-row, .project-header__*, .command-cards
Zombie-handlers beholdt for test-back-compat:
currentProjectTab, ACTIONS['project-tab'], ACTIONS['parse'],
handlePasteImport, window.__handlePasteImport. Unreachable fra v3 DOM
men nødvendige for test-playground-v3.sh + test-playground-parsers.sh.
2 fingerprint-gap lukket:
- requirements.headers: utvidet med "EU AI Act — Krav" pattern
- license.headers: utvidet med "Lisens-kapabilitetsmatrise" pattern
- KNOWN_GAP_FIXTURES = {} i test-playground-fingerprints.sh
migrateDataVersion utvidet med parserFor (3. arg):
- Demo-state med kun raw_markdown auto-parses til project.artifacts[cid]
- defaultParserFor(cmdId) resolverer PARSERS[archetypeFor(cmdId)]
- 3 bootstrap-callsites oppdatert (cold-load, import, load-demo)
Ship-QA bugfixes funnet via browser-dogfood:
- components-tier4-project-view.css lagt til i <link>-kjeden (var ikke loaded
-> modal-overlay og two-column layout virket ikke)
- renderImportModal setter data-open="true" (DS-kontrakt for display: flex)
Bundler også sesjon 2-4 deliverables som ikke ble committed tidligere:
- shared/playground-design-system v0.6.0 (Tier 4 project-view CSS + 6 tokens)
- ms-ai-architect/playground/vendor/ re-sync til DS v0.6.0
- tests/test-playground-fingerprints.sh (sesjon 4 NY - 32 PASS)
- tests/test-playground-projectview.sh (sesjon 4 NY - 30 PASS)
- tests/test-playground-actions.sh (sesjon 4 NY - 19 PASS)
- tests/test-playground-migrations.sh utvidet (7 -> 16 PASS)
- tests/run-e2e.sh wirer alle 6 playground-suiter
Stats:
- bash tests/run-e2e.sh --playground: 386 PASS, 0 FAIL, 2 WARN (pre-eks)
- bash tests/run-e2e.sh (full): All E2E suites passed
- bash tests/validate-plugin.sh: 219 PASS
Screenshots regenerert til playground/screenshots/v1.15.0/ (24 PNG-er, 12
surfaces x 2 tema). Nye v3-surfaces: project-overview, project-artifact-*,
project-import-modal (viewport-only), project-search.
Docs oppdatert (3 nivåer): README.md (badge + version history),
CHANGELOG.md, CLAUDE.md (playground-seksjon + valideringstabell),
rot-README.md + rot-CLAUDE.md (marketplace-landingen + plugin-index).
.gitignore: ny pattern *.local.html + *.local.json for sesjon-state-filer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9affdca23e
commit
d8882f5220
47 changed files with 3722 additions and 409 deletions
|
|
@ -76,6 +76,10 @@ fi
|
|||
if $RUN_PLAYGROUND; then
|
||||
bash "$SCRIPT_DIR/test-playground-v3.sh" || FAILURES=$((FAILURES + 1))
|
||||
bash "$SCRIPT_DIR/test-playground-parsers.sh" || FAILURES=$((FAILURES + 1))
|
||||
bash "$SCRIPT_DIR/test-playground-migrations.sh" || FAILURES=$((FAILURES + 1))
|
||||
bash "$SCRIPT_DIR/test-playground-fingerprints.sh" || FAILURES=$((FAILURES + 1))
|
||||
bash "$SCRIPT_DIR/test-playground-projectview.sh" || FAILURES=$((FAILURES + 1))
|
||||
bash "$SCRIPT_DIR/test-playground-actions.sh" || FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
if $RUN_KB_UPDATE; then
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
// Capture playground screenshots for v1.14.0 documentation.
|
||||
// Capture playground screenshots for v1.15.0 documentation.
|
||||
//
|
||||
// Opens the single-file playground HTML via file://, drives it through:
|
||||
// - Initial onboarding (empty state)
|
||||
// - "Last inn demo-data" → project surface with all 17 reports rehydrated
|
||||
// - All 4 project screen-tabs (oversikt / rapporter / kontekst / eksport)
|
||||
// - Each rapport-tab category (regulatory / security / economy / docs / tool)
|
||||
// - Both themes (dark + light)
|
||||
// v1.15.0: v2 project-surface (renderProjectSurface med screen-tabs +
|
||||
// category-tabs) erstattet av renderProjectView (sidebar med 17 artifacts +
|
||||
// main-area med per-artifact view + import-modal). Skjermbilder oppdatert
|
||||
// til å fange v3-surfaces.
|
||||
//
|
||||
// Output: playground/screenshots/v1.14.0/<surface>-<theme>.png
|
||||
// Output: playground/screenshots/v1.15.0/<surface>-<theme>.png
|
||||
//
|
||||
// Usage:
|
||||
// cd tests/screenshot
|
||||
|
|
@ -25,7 +23,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||
const __dirname = dirname(__filename);
|
||||
const PLUGIN_ROOT = resolve(__dirname, '..', '..');
|
||||
const HTML_PATH = join(PLUGIN_ROOT, 'playground', 'ms-ai-architect-playground.html');
|
||||
const OUT_DIR = join(PLUGIN_ROOT, 'playground', 'screenshots', 'v1.14.0');
|
||||
const OUT_DIR = join(PLUGIN_ROOT, 'playground', 'screenshots', 'v1.15.0');
|
||||
const HTML_URL = 'file://' + HTML_PATH;
|
||||
|
||||
const VIEWPORT = { width: 1440, height: 900 };
|
||||
|
|
@ -49,7 +47,6 @@ async function clearState(page) {
|
|||
await page.evaluate(() => {
|
||||
try { localStorage.clear(); } catch (e) {}
|
||||
try {
|
||||
// Best-effort: clear IndexedDB databases.
|
||||
const dbs = ['ms-ai-architect-state-v1', 'ms-ai-architect-playground'];
|
||||
dbs.forEach((n) => indexedDB.deleteDatabase(n));
|
||||
} catch (e) {}
|
||||
|
|
@ -61,9 +58,9 @@ async function loadDemo(page) {
|
|||
const action = document.querySelector('[data-action="load-demo"]');
|
||||
if (action) action.click();
|
||||
});
|
||||
// Wait for project surface to render + rehydrate paste-imports.
|
||||
await page.waitForSelector('[data-surface="project"]:not([hidden])', { timeout: 5000 });
|
||||
await page.waitForTimeout(800); // settle rehydrate microtasks
|
||||
// Settle migrasjon (v2→v3 auto-parse) + render.
|
||||
await page.waitForTimeout(1200);
|
||||
}
|
||||
|
||||
async function clickAction(page, action) {
|
||||
|
|
@ -71,28 +68,46 @@ async function clickAction(page, action) {
|
|||
const el = document.querySelector('[data-action="' + a + '"]');
|
||||
if (el) el.click();
|
||||
}, action);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
async function clickProjectTab(page, tabId) {
|
||||
await page.evaluate((t) => {
|
||||
const el = document.querySelector('[data-action="project-tab"][data-tab="' + t + '"]');
|
||||
if (el) el.click();
|
||||
}, tabId);
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
async function clickProjectScreen(page, screenId) {
|
||||
await page.evaluate((s) => {
|
||||
const el = document.querySelector('[data-action="project-screen"][data-screen="' + s + '"]');
|
||||
async function selectArtifact(page, artifactId) {
|
||||
await page.evaluate((id) => {
|
||||
const el = document.querySelector('[data-action="project-select-artifact"][data-artifact-id="' + id + '"]');
|
||||
if (el) el.click();
|
||||
}, screenId);
|
||||
}, artifactId);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async function openImportModal(page, prefillCmd) {
|
||||
await page.evaluate((cid) => {
|
||||
// Foretrukket: artifact-reimport-knappen (har eksisterende markdown).
|
||||
if (cid) {
|
||||
const el = document.querySelector('[data-action="artifact-reimport"][data-command="' + cid + '"]');
|
||||
if (el) { el.click(); return; }
|
||||
}
|
||||
// Fallback: generisk "Importer rapport"-knapp.
|
||||
const open = document.querySelector('[data-action="import-open"]');
|
||||
if (open) open.click();
|
||||
}, prefillCmd);
|
||||
await page.waitForSelector('[data-import-modal]', { timeout: 3000 });
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
async function shoot(page, name) {
|
||||
async function setSearchQuery(page, query) {
|
||||
await page.evaluate((q) => {
|
||||
const input = document.querySelector('[data-project-search]');
|
||||
if (!input) return;
|
||||
input.value = q;
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}, query);
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
async function shoot(page, name, opts) {
|
||||
const path = join(OUT_DIR, name + '.png');
|
||||
await page.screenshot({ path, fullPage: FULL_PAGE });
|
||||
const useFullPage = (opts && opts.fullPage != null) ? opts.fullPage : FULL_PAGE;
|
||||
await page.screenshot({ path, fullPage: useFullPage });
|
||||
console.log(' → ' + name + '.png');
|
||||
}
|
||||
|
||||
|
|
@ -106,52 +121,58 @@ async function captureAllSurfaces(page, theme) {
|
|||
await setTheme(page, theme);
|
||||
await shoot(page, '01-onboarding-empty-' + theme);
|
||||
|
||||
// 2. Load demo → project surface (rapporter screen, regulatory tab default)
|
||||
// 2. Load demo → project-view overview (default — ingen artifact valgt)
|
||||
await loadDemo(page);
|
||||
await setTheme(page, theme);
|
||||
await shoot(page, '02-project-rapporter-regulatory-' + theme);
|
||||
await shoot(page, '02-project-overview-' + theme);
|
||||
|
||||
// 3. Project tab cycle (5 categories)
|
||||
const TABS = [
|
||||
{ id: 'security', label: 'security' },
|
||||
{ id: 'economy', label: 'economy' },
|
||||
{ id: 'documentation', label: 'documentation' },
|
||||
{ id: 'tool', label: 'tool' }
|
||||
// 3-7. 5 sample artifacts som dekker arketype-bredden
|
||||
const SAMPLE_ARTIFACTS = [
|
||||
{ id: 'classify', label: 'classify' }, // AI Act-pyramide
|
||||
{ id: 'security', label: 'security' }, // 6×5 sikkerhets-matrise
|
||||
{ id: 'ros', label: 'ros' }, // ROS matrise + radar
|
||||
{ id: 'cost', label: 'cost' }, // P10/P50/P90 distribusjon
|
||||
{ id: 'summary', label: 'summary' } // Beslutningsnotat
|
||||
];
|
||||
for (const tab of TABS) {
|
||||
await clickProjectTab(page, tab.id);
|
||||
await page.waitForTimeout(500);
|
||||
await shoot(page, '03-project-rapporter-' + tab.label + '-' + theme);
|
||||
for (let i = 0; i < SAMPLE_ARTIFACTS.length; i++) {
|
||||
const a = SAMPLE_ARTIFACTS[i];
|
||||
await selectArtifact(page, a.id);
|
||||
const num = String(3 + i).padStart(2, '0');
|
||||
await shoot(page, num + '-project-artifact-' + a.label + '-' + theme);
|
||||
}
|
||||
|
||||
// 4. Project screen-tabs (oversikt / kontekst / eksport)
|
||||
await clickProjectScreen(page, 'oversikt');
|
||||
await shoot(page, '04-project-oversikt-' + theme);
|
||||
await clickProjectScreen(page, 'kontekst');
|
||||
await shoot(page, '05-project-kontekst-' + theme);
|
||||
await clickProjectScreen(page, 'eksport');
|
||||
await shoot(page, '06-project-eksport-' + theme);
|
||||
// 8. Import-modal åpen (med prefill fra eksisterende ros-artifact)
|
||||
// Viewport-only (ikke fullPage) — modal er position:fixed; fullPage
|
||||
// skroller forbi overlay-en og kaster bort kontekst.
|
||||
await openImportModal(page, 'ros');
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.waitForTimeout(200);
|
||||
await shoot(page, '08-project-import-modal-' + theme, { fullPage: false });
|
||||
await clickAction(page, 'import-close');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Back to rapporter for nav screenshots
|
||||
await clickProjectScreen(page, 'rapporter');
|
||||
// 9. Sidebar-søk aktivt (filtrer på "ros")
|
||||
await setSearchQuery(page, 'ros');
|
||||
await shoot(page, '09-project-search-' + theme);
|
||||
await setSearchQuery(page, ''); // reset
|
||||
|
||||
// 5. Home surface
|
||||
// 10. Home surface
|
||||
await clickAction(page, 'goto-home');
|
||||
await page.waitForSelector('[data-surface="home"]:not([hidden])');
|
||||
await page.waitForTimeout(300);
|
||||
await shoot(page, '07-home-' + theme);
|
||||
await shoot(page, '10-home-' + theme);
|
||||
|
||||
// 6. Catalog surface
|
||||
// 11. Catalog surface
|
||||
await clickAction(page, 'goto-catalog');
|
||||
await page.waitForSelector('[data-surface="catalog"]:not([hidden])');
|
||||
await page.waitForTimeout(300);
|
||||
await shoot(page, '08-catalog-' + theme);
|
||||
await shoot(page, '11-catalog-' + theme);
|
||||
|
||||
// 7. Onboarding (with prefilled state from demo)
|
||||
// 12. Onboarding prefilled (post-demo med org-felter fylt)
|
||||
await clickAction(page, 'goto-onboarding');
|
||||
await page.waitForSelector('[data-surface="onboarding"]:not([hidden])');
|
||||
await page.waitForTimeout(300);
|
||||
await shoot(page, '09-onboarding-prefilled-' + theme);
|
||||
await shoot(page, '12-onboarding-prefilled-' + theme);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
|
@ -160,7 +181,7 @@ async function main() {
|
|||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext({
|
||||
viewport: VIEWPORT,
|
||||
deviceScaleFactor: 2 // crisper screenshots for retina
|
||||
deviceScaleFactor: 2
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.on('console', (msg) => {
|
||||
|
|
|
|||
248
plugins/ms-ai-architect/tests/test-playground-actions.sh
Executable file
248
plugins/ms-ai-architect/tests/test-playground-actions.sh
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
#!/bin/bash
|
||||
# test-playground-actions.sh — Playground v3 ACTIONS handler-state-effekter
|
||||
#
|
||||
# Verifiserer at de 6 pure-state-ACTIONS-handlerne (project-select-artifact,
|
||||
# project-show-overview, import-open, import-close, artifact-reimport,
|
||||
# artifact-delete) muterer state korrekt.
|
||||
#
|
||||
# Handlerne import-detect og import-save krever document/DOM og dekkes ikke
|
||||
# her — de testes implisitt via browser-walkthrough før release og via
|
||||
# manuell QA per playground/MANUAL-CHECKLIST.md.
|
||||
#
|
||||
# Bash 3.2-kompatibel. Bruker node til JS-eval. Ingen npm-deps.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh"
|
||||
|
||||
init_suite "Playground v3 — ACTIONS handler state-effekter"
|
||||
|
||||
if [ ! -f "$HTML_FILE" ]; then
|
||||
fail "HTML-fila finnes ikke: $HTML_FILE"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
pass "HTML-fil finnes: $(basename "$HTML_FILE")"
|
||||
|
||||
NODE_OUT=$(node -e '
|
||||
const fs = require("fs");
|
||||
const htmlPath = process.argv[1];
|
||||
const htmlSrc = fs.readFileSync(htmlPath, "utf8");
|
||||
|
||||
// Ekstraher ACTIONS-blokken fra "// PROJECT-VIEW V2 ACTIONS" (sesjon 3-kommentaren)
|
||||
// til starten av smart-detect-blokken. Tar med projectViewUiState + 6 handlere.
|
||||
const startMarker = "// PROJECT-VIEW V2 ACTIONS (Sesjon 3)";
|
||||
const endMarker = "// Smart-detect på textarea-input i import-modal.";
|
||||
const startIdx = htmlSrc.indexOf(startMarker);
|
||||
const endIdx = htmlSrc.indexOf(endMarker);
|
||||
if (startIdx < 0 || endIdx < 0) {
|
||||
console.error("MARKERS_MISSING start=" + startIdx + " end=" + endIdx);
|
||||
process.exit(2);
|
||||
}
|
||||
const block = htmlSrc.substring(startIdx, endIdx);
|
||||
|
||||
// Stubs som gir handlerne et minimums-miljø uten document/window.
|
||||
const stubs = `
|
||||
let __store_state = null;
|
||||
const store = {
|
||||
get state() { return __store_state; },
|
||||
save: function () {}
|
||||
};
|
||||
function setStoreState(s) { __store_state = s; }
|
||||
function findProject(id) {
|
||||
const list = (store.state && store.state.projects) || [];
|
||||
for (let i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
|
||||
return null;
|
||||
}
|
||||
let __renderCount = 0;
|
||||
function scheduleRender() { __renderCount++; }
|
||||
let __confirmAnswer = true;
|
||||
function confirm() { return __confirmAnswer; }
|
||||
function setConfirmAnswer(b) { __confirmAnswer = b; }
|
||||
function renderCount() { return __renderCount; }
|
||||
function resetRenderCount() { __renderCount = 0; }
|
||||
const ACTIONS = {};
|
||||
`;
|
||||
const wrapped = stubs + block + "\nreturn { ACTIONS, setStoreState, setConfirmAnswer, renderCount, resetRenderCount, projectViewUiState };";
|
||||
let api;
|
||||
try {
|
||||
api = (new Function(wrapped))();
|
||||
} catch (e) {
|
||||
console.error("EVAL_FAILED: " + e.message);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
function emit(ok, desc) { console.log((ok ? "PASS" : "FAIL") + "\t" + desc); }
|
||||
|
||||
function freshState(opts) {
|
||||
const o = opts || {};
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 3,
|
||||
activeProjectId: "p1",
|
||||
projects: [{
|
||||
id: "p1",
|
||||
name: "Demo",
|
||||
artifacts: {
|
||||
classify: { commandId: "classify", raw_markdown: "RAW_CLASSIFY", parsed: { risk_level: "minimal" }, verdict: "go", keyStats: [], importedAt: "x", updatedAt: "x" },
|
||||
ros: { commandId: "ros", raw_markdown: "RAW_ROS", parsed: { threats: [] }, verdict: "approved", keyStats: [], importedAt: "x", updatedAt: "x" }
|
||||
},
|
||||
reports: {
|
||||
classify: { raw_markdown: "RAW_CLASSIFY", parsed: { risk_level: "minimal" } },
|
||||
ros: { raw_markdown: "RAW_ROS", parsed: { threats: [] } }
|
||||
}
|
||||
}],
|
||||
ui: o.ui || {}
|
||||
};
|
||||
}
|
||||
|
||||
// ---- API ----
|
||||
emit(typeof api.ACTIONS === "object" && api.ACTIONS !== null, "ACTIONS-objekt eksponert");
|
||||
emit(typeof api.ACTIONS["project-select-artifact"] === "function", "project-select-artifact handler finnes");
|
||||
emit(typeof api.ACTIONS["project-show-overview"] === "function", "project-show-overview handler finnes");
|
||||
emit(typeof api.ACTIONS["import-open"] === "function", "import-open handler finnes");
|
||||
emit(typeof api.ACTIONS["import-close"] === "function", "import-close handler finnes");
|
||||
emit(typeof api.ACTIONS["artifact-reimport"] === "function", "artifact-reimport handler finnes");
|
||||
emit(typeof api.ACTIONS["artifact-delete"] === "function", "artifact-delete handler finnes");
|
||||
|
||||
// ---- project-select-artifact ----
|
||||
(function () {
|
||||
const state = freshState();
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["project-select-artifact"]({}, { dataset: { artifactId: "classify" } });
|
||||
const ok = state.ui.projectView && state.ui.projectView.selectedArtifactId === "classify"
|
||||
&& api.renderCount() === 1;
|
||||
emit(ok, "project-select-artifact setter selectedArtifactId og trigger scheduleRender (got=" + (state.ui.projectView && state.ui.projectView.selectedArtifactId) + ")");
|
||||
})();
|
||||
|
||||
// ---- project-select-artifact uten artifactId — no-op ----
|
||||
(function () {
|
||||
const state = freshState({ ui: { projectView: { selectedArtifactId: "ros", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } });
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["project-select-artifact"]({}, { dataset: {} });
|
||||
const ok = state.ui.projectView.selectedArtifactId === "ros" && api.renderCount() === 0;
|
||||
emit(ok, "project-select-artifact uten dataset.artifactId → no-op (selectedArtifactId beholdt, ingen render)");
|
||||
})();
|
||||
|
||||
// ---- project-show-overview ----
|
||||
(function () {
|
||||
const state = freshState({ ui: { projectView: { selectedArtifactId: "classify", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } });
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["project-show-overview"]({}, {});
|
||||
const ok = state.ui.projectView.selectedArtifactId === null && api.renderCount() === 1;
|
||||
emit(ok, "project-show-overview tømmer selectedArtifactId");
|
||||
})();
|
||||
|
||||
// ---- import-open med prefill-command (eksisterende artifact gir prefillMarkdown) ----
|
||||
(function () {
|
||||
const state = freshState();
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["import-open"]({}, { dataset: { prefillCommand: "ros" } });
|
||||
const m = state.ui.importModal;
|
||||
const ok = m && m.open === true && m.prefillCommandId === "ros"
|
||||
&& m.prefillMarkdown === "RAW_ROS" && api.renderCount() === 1;
|
||||
emit(ok, "import-open med prefillCommand=\"ros\" åpner modal + prefyller raw_markdown");
|
||||
})();
|
||||
|
||||
// ---- import-open uten prefill-command ----
|
||||
(function () {
|
||||
const state = freshState();
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["import-open"]({}, { dataset: {} });
|
||||
const m = state.ui.importModal;
|
||||
const ok = m && m.open === true && m.prefillCommandId === "" && m.prefillMarkdown === "";
|
||||
emit(ok, "import-open uten prefill → modal åpnet, ingen prefyll");
|
||||
})();
|
||||
|
||||
// ---- import-close ----
|
||||
(function () {
|
||||
const state = freshState({ ui: { projectView: { selectedArtifactId: null, searchQuery: "" }, importModal: { open: true, prefillCommandId: "ros", prefillMarkdown: "RAW_ROS" } } });
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["import-close"]({}, {});
|
||||
const m = state.ui.importModal;
|
||||
const ok = m.open === false && m.prefillCommandId === "" && m.prefillMarkdown === "" && api.renderCount() === 1;
|
||||
emit(ok, "import-close tilbakestiller modal-state");
|
||||
})();
|
||||
|
||||
// ---- artifact-reimport prefyller modal med eksisterende markdown ----
|
||||
(function () {
|
||||
const state = freshState();
|
||||
api.setStoreState(state);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["artifact-reimport"]({}, { dataset: { command: "classify" } });
|
||||
const m = state.ui.importModal;
|
||||
const ok = m.open === true && m.prefillCommandId === "classify" && m.prefillMarkdown === "RAW_CLASSIFY";
|
||||
emit(ok, "artifact-reimport åpner modal med prefill fra eksisterende artifact");
|
||||
})();
|
||||
|
||||
// ---- artifact-delete med confirm=false → no-op ----
|
||||
(function () {
|
||||
const state = freshState();
|
||||
api.setStoreState(state);
|
||||
api.setConfirmAnswer(false);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["artifact-delete"]({}, { dataset: { command: "classify" } });
|
||||
const ok = state.projects[0].artifacts.classify !== undefined && api.renderCount() === 0;
|
||||
emit(ok, "artifact-delete med confirm=false → artifact bevart, ingen render");
|
||||
})();
|
||||
|
||||
// ---- artifact-delete med confirm=true → sletter + clearer selectedArtifactId ----
|
||||
(function () {
|
||||
const state = freshState({ ui: { projectView: { selectedArtifactId: "classify", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } });
|
||||
api.setStoreState(state);
|
||||
api.setConfirmAnswer(true);
|
||||
api.resetRenderCount();
|
||||
api.ACTIONS["artifact-delete"]({}, { dataset: { command: "classify" } });
|
||||
const p = state.projects[0];
|
||||
const ok = p.artifacts.classify === undefined && p.reports.classify === undefined
|
||||
&& state.ui.projectView.selectedArtifactId === null && api.renderCount() === 1;
|
||||
emit(ok, "artifact-delete med confirm=true sletter artifact + reports + clearer selectedArtifactId");
|
||||
})();
|
||||
|
||||
// ---- artifact-delete bevarer andre artifacts ----
|
||||
(function () {
|
||||
const state = freshState();
|
||||
api.setStoreState(state);
|
||||
api.setConfirmAnswer(true);
|
||||
api.ACTIONS["artifact-delete"]({}, { dataset: { command: "ros" } });
|
||||
const p = state.projects[0];
|
||||
const ok = p.artifacts.classify !== undefined && p.artifacts.ros === undefined;
|
||||
emit(ok, "artifact-delete sletter bare den ene — andre artifacts bevart");
|
||||
})();
|
||||
|
||||
// ---- projectViewUiState initialiserer state-grener idempotent ----
|
||||
(function () {
|
||||
const state = { projects: [], ui: {} };
|
||||
api.setStoreState(state);
|
||||
const ui1 = api.projectViewUiState();
|
||||
const ui2 = api.projectViewUiState();
|
||||
const ok = ui1 === ui2
|
||||
&& ui1.projectView && ui1.projectView.selectedArtifactId === null && ui1.projectView.searchQuery === ""
|
||||
&& ui1.importModal && ui1.importModal.open === false;
|
||||
emit(ok, "projectViewUiState initialiserer projectView + importModal idempotent");
|
||||
})();
|
||||
' "$HTML_FILE" 2>&1) || NODE_RC=$?
|
||||
|
||||
if [ "${NODE_RC:-0}" -ne 0 ]; then
|
||||
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r status desc; do
|
||||
case "$status" in
|
||||
PASS) pass "$desc" ;;
|
||||
FAIL) fail "$desc" ;;
|
||||
WARN) warn "$desc" ;;
|
||||
esac
|
||||
done <<< "$NODE_OUT"
|
||||
|
||||
print_summary
|
||||
218
plugins/ms-ai-architect/tests/test-playground-fingerprints.sh
Executable file
218
plugins/ms-ai-architect/tests/test-playground-fingerprints.sh
Executable file
|
|
@ -0,0 +1,218 @@
|
|||
#!/bin/bash
|
||||
# test-playground-fingerprints.sh — Playground v3 inferCommandIdFromMarkdown
|
||||
#
|
||||
# Verifiserer:
|
||||
# 1. PROJECT_VIEW_V2_BEGIN/END-markørene finnes
|
||||
# 2. inferCommandIdFromMarkdown matcher hver av 17 test-fixtures mot
|
||||
# egen commandId med confidence >= 0.6 (true-positive matrise)
|
||||
# 3. False-positive immunity:
|
||||
# - tom streng → null
|
||||
# - 100 tegn lorem ipsum → null
|
||||
# - kun "# Hello"-header → null
|
||||
# - mixed content med kun classify-header → matcher classify
|
||||
# 4. Sanity-asserts på COMMAND_FINGERPRINTS-shape og fingerprintScore-API
|
||||
#
|
||||
# Bash 3.2-kompatibel. Bruker node til JS-eval; ingen npm-deps.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html"
|
||||
FIXTURE_DIR="$PLUGIN_ROOT/playground/test-fixtures"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh"
|
||||
|
||||
init_suite "Playground v3 — inferCommandIdFromMarkdown fingerprints"
|
||||
|
||||
# ---- 1. Filer eksisterer ----
|
||||
if [ ! -f "$HTML_FILE" ]; then
|
||||
fail "HTML-fila finnes ikke: $HTML_FILE"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
pass "HTML-fil finnes: $(basename "$HTML_FILE")"
|
||||
|
||||
if [ ! -d "$FIXTURE_DIR" ]; then
|
||||
fail "Fixture-mappe mangler: $FIXTURE_DIR"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
pass "Fixture-mappe finnes"
|
||||
|
||||
# ---- 2. PROJECT_VIEW_V2-markører eksisterer ----
|
||||
if grep -q "PROJECT_VIEW_V2_BEGIN" "$HTML_FILE" && grep -q "PROJECT_VIEW_V2_END" "$HTML_FILE"; then
|
||||
pass "PROJECT_VIEW_V2_BEGIN/END markører finnes"
|
||||
else
|
||||
fail "Mangler PROJECT_VIEW_V2-markører i HTML"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
|
||||
# ---- 3. Kjør hele test-matrisen i én node-prosess (effektivt) ----
|
||||
# Node-skriptet:
|
||||
# - ekstraherer PROJECT_VIEW_V2-blokken
|
||||
# - stubber dependencies
|
||||
# - leser alle 17 fixture-filer
|
||||
# - kjører true-positive + anti-match + sanity-tester
|
||||
# - skriver én linje per assert: "PASS|FAIL <description>"
|
||||
|
||||
NODE_OUT=$(node -e '
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const htmlPath = process.argv[1];
|
||||
const fixtureDir = process.argv[2];
|
||||
|
||||
const html = fs.readFileSync(htmlPath, "utf8");
|
||||
const beginMarker = "// === PROJECT_VIEW_V2_BEGIN ===";
|
||||
const endMarker = "// === PROJECT_VIEW_V2_END ===";
|
||||
const beginIdx = html.indexOf(beginMarker);
|
||||
const endIdx = html.indexOf(endMarker);
|
||||
if (beginIdx < 0 || endIdx < 0) {
|
||||
console.error("MARKER_MISSING");
|
||||
process.exit(2);
|
||||
}
|
||||
const block = html.substring(beginIdx, endIdx + endMarker.length);
|
||||
|
||||
// 17 produces_report-renderers per PROJECT_VIEW_CONFIG.renderers.
|
||||
const RENDERER_IDS = ["renderAiActPyramid","renderRequirements","renderTransparency","renderFria","renderConformity","renderDpia","renderSecurity","renderRos","renderReview","renderCost","renderLicense","renderMigrate","renderAdr","renderSummary","renderPoc","renderUtredning","renderCompare"];
|
||||
const rendererStubs = RENDERER_IDS.map(function (n) { return n + ": function () {}"; }).join(", ");
|
||||
|
||||
// CATALOG.commands: 17 produces_report=true entries med id, label, category, renderer.
|
||||
const COMMANDS = [
|
||||
{ id: "classify", category: "regulatory", label: "EU AI Act — Klassifisering", renderer: "renderAiActPyramid", produces_report: true, report_archetype: "aiact" },
|
||||
{ id: "requirements", category: "regulatory", label: "EU AI Act — Krav per risiko", renderer: "renderRequirements", produces_report: true, report_archetype: "requirements-list" },
|
||||
{ id: "transparency", category: "regulatory", label: "Transparensnotis (Art. 13/50)", renderer: "renderTransparency", produces_report: true, report_archetype: "text-document" },
|
||||
{ id: "frimpact", category: "regulatory", label: "FRIA (Art. 27)", renderer: "renderFria", produces_report: true, report_archetype: "fria" },
|
||||
{ id: "conformity", category: "regulatory", label: "Samsvarsvurdering (Art. 43)", renderer: "renderConformity", produces_report: true, report_archetype: "conformity-checklist" },
|
||||
{ id: "dpia", category: "regulatory", label: "DPIA / PVK", renderer: "renderDpia", produces_report: true, report_archetype: "matrix-risk" },
|
||||
{ id: "security", category: "security", label: "Sikkerhetsvurdering (6×5)", renderer: "renderSecurity", produces_report: true, report_archetype: "matrix-risk-6x5" },
|
||||
{ id: "ros", category: "security", label: "ROS-analyse", renderer: "renderRos", produces_report: true, report_archetype: "matrix-risk" },
|
||||
{ id: "review", category: "security", label: "Arkitekturgjennomgang", renderer: "renderReview", produces_report: true, report_archetype: "findings" },
|
||||
{ id: "cost", category: "economy", label: "Kostnadsestimat", renderer: "renderCost", produces_report: true, report_archetype: "cost-distribution" },
|
||||
{ id: "license", category: "economy", label: "Lisenskartlegging", renderer: "renderLicense", produces_report: true, report_archetype: "scenario-comparison" },
|
||||
{ id: "migrate", category: "economy", label: "Migrasjonsplan", renderer: "renderMigrate", produces_report: true, report_archetype: "phase-plan" },
|
||||
{ id: "adr", category: "documentation", label: "ADR", renderer: "renderAdr", produces_report: true, report_archetype: "adr" },
|
||||
{ id: "summary", category: "documentation", label: "Beslutningsnotat", renderer: "renderSummary", produces_report: true, report_archetype: "verdict" },
|
||||
{ id: "poc", category: "documentation", label: "POC-plan", renderer: "renderPoc", produces_report: true, report_archetype: "phase-plan" },
|
||||
{ id: "utredning", category: "documentation", label: "Utredning", renderer: "renderUtredning", produces_report: true, report_archetype: "utredning" },
|
||||
{ id: "compare", category: "documentation", label: "Plattformsammenligning", renderer: "renderCompare", produces_report: true, report_archetype: "scenario-comparison" }
|
||||
];
|
||||
|
||||
const stubs = `
|
||||
const window = {};
|
||||
function escapeHtml(s) { return String(s == null ? "" : s); }
|
||||
function escapeAttr(s) { return escapeHtml(s); }
|
||||
function renderPageShell(opts, body) { return "<header>" + (opts && opts.title || "") + "</header>" + (body || ""); }
|
||||
function renderVerdictPill(v) { return "<span class=\\"verdict-pill\\" data-verdict=\\"" + v + "\\">" + v + "</span>"; }
|
||||
function renderKeyStatsGrid(s) { return "<div class=\\"key-stats\\">" + (s && s.length || 0) + "</div>"; }
|
||||
function inferVerdict() { return "n-a"; }
|
||||
function inferKeyStats() { return []; }
|
||||
const PARSERS = {};
|
||||
const RENDERERS = { ` + rendererStubs + ` };
|
||||
const CATALOG = { commands: ` + JSON.stringify(COMMANDS) + ` };
|
||||
const ACTIONS = {};
|
||||
const store = { state: { ui: {}, projects: [], activeProjectId: null }, save: function () {} };
|
||||
function findProject() { return null; }
|
||||
function scheduleRender() {}
|
||||
`;
|
||||
|
||||
const wrapped = stubs + block + "\nreturn { inferCommandIdFromMarkdown, fingerprintScore, COMMAND_FINGERPRINTS };";
|
||||
let api;
|
||||
try {
|
||||
api = (new Function(wrapped))();
|
||||
} catch (e) {
|
||||
console.error("EVAL_FAILED: " + e.message);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
function emit(ok, desc) {
|
||||
console.log((ok ? "PASS" : "FAIL") + "\t" + desc);
|
||||
}
|
||||
|
||||
// ---- Sanity-asserts (5) ----
|
||||
emit(typeof api.inferCommandIdFromMarkdown === "function", "inferCommandIdFromMarkdown er funksjon");
|
||||
emit(typeof api.fingerprintScore === "function", "fingerprintScore er funksjon");
|
||||
emit(typeof api.COMMAND_FINGERPRINTS === "object" && api.COMMAND_FINGERPRINTS !== null, "COMMAND_FINGERPRINTS er objekt");
|
||||
const cfKeys = Object.keys(api.COMMAND_FINGERPRINTS);
|
||||
emit(cfKeys.length === 17, "COMMAND_FINGERPRINTS har 17 entries (got " + cfKeys.length + ")");
|
||||
let allShapeOk = true;
|
||||
for (const k of cfKeys) {
|
||||
const v = api.COMMAND_FINGERPRINTS[k];
|
||||
if (!v || !Array.isArray(v.headers) || !Array.isArray(v.keywords)) {
|
||||
allShapeOk = false; break;
|
||||
}
|
||||
}
|
||||
emit(allShapeOk, "Hver fingerprint har headers[] og keywords[]");
|
||||
|
||||
// ---- True-positive matrise (17 fixtures) ----
|
||||
//
|
||||
// Kjente fingerprint/fixture-gap (oppdaget av denne testen, fix i sesjon 5):
|
||||
// - requirements.md har header "# EU AI Act — Krav for høyrisiko" som ikke
|
||||
// matcher /^\s*#\s*(AI\s*Act-?krav|Krav per|Requirements)/i. Annex IV-
|
||||
// omtale i tabellen gjør at conformity vinner med 0.76.
|
||||
// - license.md har header "# Lisens-kapabilitetsmatrise" som ikke matcher
|
||||
// /^\s*#\s*(Lisens(kart)?legging|License\s*Mapping)/i.
|
||||
// v1.15.0 (sesjon 5): begge gap-er lukket — requirements.headers og
|
||||
// license.headers utvidet i COMMAND_FINGERPRINTS. KNOWN_GAP_FIXTURES tømt.
|
||||
const KNOWN_GAP_FIXTURES = {};
|
||||
|
||||
const expectedIds = ["classify","requirements","transparency","frimpact","conformity","dpia","security","ros","review","cost","license","migrate","adr","summary","poc","utredning","compare"];
|
||||
for (const cid of expectedIds) {
|
||||
const fxPath = path.join(fixtureDir, cid + ".md");
|
||||
let text = "";
|
||||
try { text = fs.readFileSync(fxPath, "utf8"); } catch (e) {
|
||||
emit(false, "fixture " + cid + ".md kunne ikke leses: " + e.message);
|
||||
continue;
|
||||
}
|
||||
const r = api.inferCommandIdFromMarkdown(text, {});
|
||||
const matchedCid = r && r.commandId;
|
||||
const conf = r && r.confidence;
|
||||
const ok = r && r.commandId === cid && r.confidence >= 0.6;
|
||||
if (ok) {
|
||||
emit(true, "fixture " + cid + ".md → " + matchedCid + " conf=" + conf.toFixed(2));
|
||||
} else if (KNOWN_GAP_FIXTURES[cid]) {
|
||||
console.log("WARN\tfixture " + cid + ".md → " + (matchedCid || "null") +
|
||||
" (KNOWN_GAP — fingerprint dekker ikke fixture-header; fix i sesjon 5)");
|
||||
} else {
|
||||
emit(false, "fixture " + cid + ".md → " + (matchedCid || "null") +
|
||||
(conf != null ? " conf=" + conf.toFixed(2) : ""));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Anti-match (4) ----
|
||||
emit(api.inferCommandIdFromMarkdown("", {}) === null,
|
||||
"tom streng → null");
|
||||
emit(api.inferCommandIdFromMarkdown(null, {}) === null,
|
||||
"null input → null");
|
||||
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim";
|
||||
emit(api.inferCommandIdFromMarkdown(lorem, {}) === null,
|
||||
"100 tegn lorem ipsum → null (got " + JSON.stringify(api.inferCommandIdFromMarkdown(lorem, {})) + ")");
|
||||
emit(api.inferCommandIdFromMarkdown("# Hello\n\nIngen relevant tekst.", {}) === null,
|
||||
"kun \"# Hello\"-header → null");
|
||||
|
||||
// ---- Mixed content: dominant header vinner ----
|
||||
const mixed = "# EU AI Act — Klassifisering\n\nDette inneholder også owasp og prompt injection-referanser men headeren er klassifisering.";
|
||||
const mixedR = api.inferCommandIdFromMarkdown(mixed, {});
|
||||
emit(mixedR !== null && mixedR.commandId === "classify",
|
||||
"mixed content med classify-header → " + (mixedR && mixedR.commandId));
|
||||
|
||||
// ---- fingerprintScore-API direkte ----
|
||||
emit(api.fingerprintScore("any text", null) === 0,
|
||||
"fingerprintScore(null spec) === 0");
|
||||
emit(api.fingerprintScore("", api.COMMAND_FINGERPRINTS.classify) === 0,
|
||||
"fingerprintScore(tom tekst) === 0");
|
||||
' "$HTML_FILE" "$FIXTURE_DIR" 2>&1) || NODE_RC=$?
|
||||
|
||||
if [ "${NODE_RC:-0}" -ne 0 ]; then
|
||||
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
|
||||
# Parse PASS/FAIL/WARN-linjer fra node-output
|
||||
while IFS=$'\t' read -r status desc; do
|
||||
case "$status" in
|
||||
PASS) pass "$desc" ;;
|
||||
FAIL) fail "$desc" ;;
|
||||
WARN) warn "$desc" ;;
|
||||
esac
|
||||
done <<< "$NODE_OUT"
|
||||
|
||||
print_summary
|
||||
|
|
@ -122,8 +122,8 @@ api.migrateDataVersion(stateB, api.defaultArchetypeFor);
|
|||
const a = JSON.stringify(stateA);
|
||||
const b = JSON.stringify(stateB);
|
||||
|
||||
if (stateA.dataVersion !== 2) {
|
||||
console.error("DATA_VERSION_NOT_BUMPED");
|
||||
if (stateA.dataVersion !== 3) {
|
||||
console.error("DATA_VERSION_NOT_BUMPED got=" + stateA.dataVersion);
|
||||
process.exit(4);
|
||||
}
|
||||
|
||||
|
|
@ -141,8 +141,22 @@ for (const p of (stateA.projects || [])) {
|
|||
if (verdictsAdded === 0) { console.error("NO_VERDICTS_ADDED"); process.exit(5); }
|
||||
if (statsAdded === 0) { console.error("NO_KEYSTATS_ADDED"); process.exit(6); }
|
||||
|
||||
// Sesjon 3: v2→v3-migrasjon må produsere project.artifacts med v3-shape.
|
||||
let artifactsBuilt = 0;
|
||||
for (const p of (stateA.projects || [])) {
|
||||
if (!p.artifacts) continue;
|
||||
for (const id of Object.keys(p.artifacts)) {
|
||||
const a = p.artifacts[id];
|
||||
if (a && a.commandId === id && typeof a.raw_markdown === "string"
|
||||
&& a.importedAt && a.updatedAt) {
|
||||
artifactsBuilt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (artifactsBuilt === 0) { console.error("NO_ARTIFACTS_BUILT"); process.exit(8); }
|
||||
|
||||
if (a === b) {
|
||||
console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded);
|
||||
console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded + " artifacts=" + artifactsBuilt);
|
||||
} else {
|
||||
console.error("NOT_IDEMPOTENT");
|
||||
process.exit(7);
|
||||
|
|
@ -155,7 +169,7 @@ else
|
|||
fail "Idempotency-test feilet: $IDEMPOTENCY_RESULT"
|
||||
fi
|
||||
|
||||
# ---- 6. dataVersion bumpes til 2 ved første kjøring ----
|
||||
# ---- 6. dataVersion bumpes til 3 ved første kjøring (v?→v3 kjede) ----
|
||||
DV_RESULT=$(node -e '
|
||||
const fs = require("fs");
|
||||
const html = fs.readFileSync(process.argv[1], "utf8");
|
||||
|
|
@ -176,10 +190,256 @@ api.migrateDataVersion(state, api.defaultArchetypeFor);
|
|||
console.log(state.dataVersion);
|
||||
' "$HTML_FILE" 2>&1) || true
|
||||
|
||||
if [ "$DV_RESULT" = "2" ]; then
|
||||
pass "dataVersion bumpes til 2"
|
||||
if [ "$DV_RESULT" = "3" ]; then
|
||||
pass "dataVersion bumpes til 3 (kjede v?→v3)"
|
||||
else
|
||||
fail "dataVersion ble ikke bumpet til 2 (got '$DV_RESULT')"
|
||||
fail "dataVersion ble ikke bumpet til 3 (got '$DV_RESULT')"
|
||||
fi
|
||||
|
||||
# ---- 7. v2→v3 produserer artifacts uten å miste reports ----
|
||||
V3_SHAPE=$(node -e '
|
||||
const fs = require("fs");
|
||||
const html = fs.readFileSync(process.argv[1], "utf8");
|
||||
const begin = html.indexOf("// === V2_FOUNDATION_BEGIN ===");
|
||||
const end = html.indexOf("// === V2_FOUNDATION_END ===");
|
||||
const block = html.substring(begin, end + 32);
|
||||
const stubs = `
|
||||
const window = {};
|
||||
function escapeHtml(s) { return String(s == null ? "" : s); }
|
||||
function escapeAttr(s) { return escapeHtml(s); }
|
||||
const CATALOG = { commands: [
|
||||
{ id: "classify", report_archetype: "aiact" }
|
||||
]};
|
||||
`;
|
||||
const api = (new Function(stubs + block + "\nreturn { migrateDataVersion, defaultArchetypeFor };"))();
|
||||
// Bygg en v2-state med ett prosjekt og én report som har parsed-data
|
||||
const state = {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 2,
|
||||
projects: [{
|
||||
id: "p1",
|
||||
name: "Test",
|
||||
createdAt: "2026-05-15T00:00:00.000Z",
|
||||
reports: {
|
||||
classify: { raw_markdown: "# klassifisering", parsed: { risk_level: "minimal", verdict: "go", keyStats: [] } }
|
||||
}
|
||||
}]
|
||||
};
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
const p = state.projects[0];
|
||||
const hasReports = !!(p.reports && p.reports.classify);
|
||||
const hasArtifacts = !!(p.artifacts && p.artifacts.classify);
|
||||
const a = p.artifacts && p.artifacts.classify;
|
||||
const shapeOk = a && a.commandId === "classify" && a.raw_markdown === "# klassifisering"
|
||||
&& a.parsed && a.parsed.risk_level === "minimal" && a.verdict === "go"
|
||||
&& Array.isArray(a.keyStats) && typeof a.importedAt === "string"
|
||||
&& typeof a.updatedAt === "string";
|
||||
console.log(JSON.stringify({ dataVersion: state.dataVersion, hasReports, hasArtifacts, shapeOk }));
|
||||
' "$HTML_FILE" 2>&1) || true
|
||||
|
||||
if echo "$V3_SHAPE" | grep -q '"dataVersion":3'; then
|
||||
pass "v2→v3 setter dataVersion=3"
|
||||
else
|
||||
fail "v2→v3 setter ikke dataVersion=3 ($V3_SHAPE)"
|
||||
fi
|
||||
if echo "$V3_SHAPE" | grep -q '"hasReports":true'; then
|
||||
pass "v2→v3 bevarer project.reports (bakover-kompat v1.15.0)"
|
||||
else
|
||||
fail "v2→v3 mistet project.reports ($V3_SHAPE)"
|
||||
fi
|
||||
if echo "$V3_SHAPE" | grep -q '"hasArtifacts":true'; then
|
||||
pass "v2→v3 bygger project.artifacts"
|
||||
else
|
||||
fail "v2→v3 bygde ikke project.artifacts ($V3_SHAPE)"
|
||||
fi
|
||||
if echo "$V3_SHAPE" | grep -q '"shapeOk":true'; then
|
||||
pass "v2→v3 artifact-shape ({commandId, raw_markdown, parsed, verdict, keyStats, importedAt, updatedAt})"
|
||||
else
|
||||
fail "v2→v3 artifact-shape mismatch ($V3_SHAPE)"
|
||||
fi
|
||||
|
||||
# ---- 8. v2→v3 kant-case-tester (sesjon 4) ----
|
||||
# Tomt prosjekt, manglende reports, blandet state, idempotens-mutasjon.
|
||||
EDGE_RESULT=$(node -e '
|
||||
const fs = require("fs");
|
||||
const html = fs.readFileSync(process.argv[1], "utf8");
|
||||
const begin = html.indexOf("// === V2_FOUNDATION_BEGIN ===");
|
||||
const end = html.indexOf("// === V2_FOUNDATION_END ===");
|
||||
const block = html.substring(begin, end + 32);
|
||||
const stubs = `
|
||||
const window = {};
|
||||
function escapeHtml(s) { return String(s == null ? "" : s); }
|
||||
function escapeAttr(s) { return escapeHtml(s); }
|
||||
const CATALOG = { commands: [
|
||||
{ id: "classify", report_archetype: "aiact" },
|
||||
{ id: "ros", report_archetype: "matrix-risk" },
|
||||
{ id: "cost", report_archetype: "cost-distribution" },
|
||||
{ id: "summary", report_archetype: "verdict" }
|
||||
]};
|
||||
`;
|
||||
const api = (new Function(stubs + block + "\nreturn { migrateDataVersion, defaultArchetypeFor };"))();
|
||||
|
||||
function emit(key, ok, info) {
|
||||
console.log("EDGE\t" + key + "\t" + (ok ? "PASS" : "FAIL") + "\t" + (info || ""));
|
||||
}
|
||||
|
||||
// Case A: tomt prosjekt (reports={}).
|
||||
(function () {
|
||||
const state = {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 2,
|
||||
projects: [{ id: "pA", name: "Empty", createdAt: "2026-05-15T00:00:00.000Z", reports: {} }]
|
||||
};
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
const p = state.projects[0];
|
||||
const ok = p.artifacts !== undefined && typeof p.artifacts === "object"
|
||||
&& Object.keys(p.artifacts).length === 0
|
||||
&& state.dataVersion === 3;
|
||||
emit("empty-project", ok, "artifacts=" + JSON.stringify(p.artifacts) + " dv=" + state.dataVersion);
|
||||
})();
|
||||
|
||||
// Case B: prosjekt uten reports-felt i det hele tatt.
|
||||
(function () {
|
||||
const state = {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 2,
|
||||
projects: [{ id: "pB", name: "NoReports", createdAt: "2026-05-15T00:00:00.000Z" }]
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
} catch (e) { threw = true; }
|
||||
const p = state.projects[0];
|
||||
const ok = !threw && p.artifacts !== undefined && typeof p.artifacts === "object"
|
||||
&& Object.keys(p.artifacts).length === 0;
|
||||
emit("no-reports-field", ok, "threw=" + threw + " artifacts=" + JSON.stringify(p.artifacts));
|
||||
})();
|
||||
|
||||
// Case C: blandet state — artifacts har 2 entries fra før, reports har 5.
|
||||
// Migrering skal legge til de 3 manglende uten å overskrive eksisterende.
|
||||
(function () {
|
||||
const preNow = "2026-04-01T00:00:00.000Z";
|
||||
const state = {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 2,
|
||||
projects: [{
|
||||
id: "pC",
|
||||
name: "Mixed",
|
||||
createdAt: preNow,
|
||||
artifacts: {
|
||||
classify: {
|
||||
commandId: "classify",
|
||||
raw_markdown: "EXISTING_CLASSIFY",
|
||||
parsed: { risk_level: "minimal" },
|
||||
verdict: "go",
|
||||
keyStats: [],
|
||||
importedAt: preNow,
|
||||
updatedAt: preNow,
|
||||
_preExisting: true
|
||||
},
|
||||
ros: {
|
||||
commandId: "ros",
|
||||
raw_markdown: "EXISTING_ROS",
|
||||
parsed: { threats: [] },
|
||||
verdict: "approved",
|
||||
keyStats: [],
|
||||
importedAt: preNow,
|
||||
updatedAt: preNow,
|
||||
_preExisting: true
|
||||
}
|
||||
},
|
||||
reports: {
|
||||
classify: { raw_markdown: "NEW_CLASSIFY", parsed: { risk_level: "high" } },
|
||||
ros: { raw_markdown: "NEW_ROS", parsed: { threats: [{ id: "T1" }] } },
|
||||
cost: { raw_markdown: "NEW_COST", parsed: { p50: 1000 } },
|
||||
summary: { raw_markdown: "NEW_SUMMARY", parsed: { verdict: "go" } }
|
||||
}
|
||||
}]
|
||||
};
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
const p = state.projects[0];
|
||||
const cls = p.artifacts.classify;
|
||||
const ros = p.artifacts.ros;
|
||||
const cost = p.artifacts.cost;
|
||||
const summary = p.artifacts.summary;
|
||||
// Eksisterende artifacts bevart med _preExisting-flag og opprinnelig raw_markdown.
|
||||
const classifyPreserved = cls && cls._preExisting === true && cls.raw_markdown === "EXISTING_CLASSIFY";
|
||||
const rosPreserved = ros && ros._preExisting === true && ros.raw_markdown === "EXISTING_ROS";
|
||||
// Nye artifacts bygget fra reports.
|
||||
const costAdded = cost && cost.commandId === "cost" && cost.raw_markdown === "NEW_COST";
|
||||
const summaryAdded = summary && summary.commandId === "summary" && summary.raw_markdown === "NEW_SUMMARY";
|
||||
const ok = classifyPreserved && rosPreserved && costAdded && summaryAdded;
|
||||
emit("mixed-state-merge", ok,
|
||||
"cls_preserved=" + !!classifyPreserved + " ros_preserved=" + !!rosPreserved +
|
||||
" cost_added=" + !!costAdded + " summary_added=" + !!summaryAdded);
|
||||
})();
|
||||
|
||||
// Case D: idempotens etter mutasjon — sett _touched=true på en artifact, kjør
|
||||
// migrasjon på nytt, sjekk at flagget er bevart (ikke overskrevet).
|
||||
(function () {
|
||||
const state = {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 2,
|
||||
projects: [{
|
||||
id: "pD",
|
||||
name: "Idempotent",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
reports: {
|
||||
classify: { raw_markdown: "# klassifisering", parsed: { risk_level: "minimal" } }
|
||||
}
|
||||
}]
|
||||
};
|
||||
// Første migrasjon bygger artifact.
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
const p = state.projects[0];
|
||||
p.artifacts.classify._touched = true;
|
||||
p.artifacts.classify.raw_markdown = "MUTATED_AFTER_MIGRATION";
|
||||
// Re-migrer — idempotent skal ikke overskrive.
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
const a = state.projects[0].artifacts.classify;
|
||||
const ok = a._touched === true && a.raw_markdown === "MUTATED_AFTER_MIGRATION";
|
||||
emit("idempotent-after-mutation", ok,
|
||||
"_touched=" + a._touched + " raw=" + JSON.stringify(a.raw_markdown));
|
||||
})();
|
||||
|
||||
// Case E: dataVersion=3 fra før — migrasjon er no-op.
|
||||
(function () {
|
||||
const state = {
|
||||
schemaVersion: 1,
|
||||
dataVersion: 3,
|
||||
projects: [{
|
||||
id: "pE",
|
||||
name: "AlreadyV3",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
artifacts: {
|
||||
cost: { commandId: "cost", raw_markdown: "EXISTING", parsed: { p50: 1 },
|
||||
verdict: "go", keyStats: [], importedAt: "x", updatedAt: "x" }
|
||||
}
|
||||
}]
|
||||
};
|
||||
const beforeJson = JSON.stringify(state);
|
||||
api.migrateDataVersion(state, api.defaultArchetypeFor);
|
||||
const afterJson = JSON.stringify(state);
|
||||
emit("already-v3-noop", beforeJson === afterJson, "diff=" + (beforeJson === afterJson ? "none" : "changed"));
|
||||
})();
|
||||
' "$HTML_FILE" 2>&1) || true
|
||||
|
||||
# Parse EDGE-resultater
|
||||
while IFS=$'\t' read -r tag key status info; do
|
||||
[ "$tag" = "EDGE" ] || continue
|
||||
case "$key" in
|
||||
empty-project) desc="v3 kant-case: tomt prosjekt (reports={}) → artifacts={} uten å feile" ;;
|
||||
no-reports-field) desc="v3 kant-case: prosjekt uten reports-felt → artifacts={} opprettet" ;;
|
||||
mixed-state-merge) desc="v3 kant-case: blandet state — eksisterende artifacts bevart, nye lagt til" ;;
|
||||
idempotent-after-mutation) desc="v3 kant-case: idempotens etter mutasjon — _touched-flag bevart" ;;
|
||||
already-v3-noop) desc="v3 kant-case: state allerede v3 → migrasjon er no-op" ;;
|
||||
*) desc="v3 kant-case: $key" ;;
|
||||
esac
|
||||
if [ "$status" = "PASS" ]; then
|
||||
pass "$desc"
|
||||
else
|
||||
fail "$desc ($info)"
|
||||
fi
|
||||
done <<< "$EDGE_RESULT"
|
||||
|
||||
print_summary
|
||||
|
|
|
|||
323
plugins/ms-ai-architect/tests/test-playground-projectview.sh
Executable file
323
plugins/ms-ai-architect/tests/test-playground-projectview.sh
Executable file
|
|
@ -0,0 +1,323 @@
|
|||
#!/bin/bash
|
||||
# test-playground-projectview.sh — Playground v3 renderProjectView integration
|
||||
#
|
||||
# Verifiserer at renderProjectView + sub-renderers produserer korrekt HTML
|
||||
# (som strenger — ingen DOM-deps) mot en inline demo-state-snippet.
|
||||
#
|
||||
# Dekker 4 view-tilstander:
|
||||
# - overview (selectedArtifactId null)
|
||||
# - artifact (filled) (selectedArtifactId = 'classify')
|
||||
# - empty (sidebar-treff) (selectedArtifactId = 'frimpact' — mangler)
|
||||
# - import-modal-open (top-state, orthogonal)
|
||||
#
|
||||
# Pluss:
|
||||
# - renderArtifactNav-søk filtrerer
|
||||
# - renderProjectOverview har 4 verdict-tiles, top-risks, next-actions, missing
|
||||
# - renderImportModal har 17 dropdown-options + prefill
|
||||
#
|
||||
# Bash 3.2-kompatibel. Bruker node til JS-eval. Ingen npm-deps.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh"
|
||||
|
||||
init_suite "Playground v3 — renderProjectView integration"
|
||||
|
||||
if [ ! -f "$HTML_FILE" ]; then
|
||||
fail "HTML-fila finnes ikke: $HTML_FILE"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
pass "HTML-fil finnes: $(basename "$HTML_FILE")"
|
||||
|
||||
if grep -q "PROJECT_VIEW_V2_BEGIN" "$HTML_FILE" && grep -q "PROJECT_VIEW_V2_END" "$HTML_FILE"; then
|
||||
pass "PROJECT_VIEW_V2_BEGIN/END markører finnes"
|
||||
else
|
||||
fail "Mangler PROJECT_VIEW_V2-markører i HTML"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
|
||||
NODE_OUT=$(node -e '
|
||||
const fs = require("fs");
|
||||
const htmlPath = process.argv[1];
|
||||
|
||||
const htmlSrc = fs.readFileSync(htmlPath, "utf8");
|
||||
const beginMarker = "// === PROJECT_VIEW_V2_BEGIN ===";
|
||||
const endMarker = "// === PROJECT_VIEW_V2_END ===";
|
||||
const beginIdx = htmlSrc.indexOf(beginMarker);
|
||||
const endIdx = htmlSrc.indexOf(endMarker);
|
||||
if (beginIdx < 0 || endIdx < 0) {
|
||||
console.error("MARKER_MISSING"); process.exit(2);
|
||||
}
|
||||
const block = htmlSrc.substring(beginIdx, endIdx + endMarker.length);
|
||||
|
||||
// 17 produces_report-renderers per PROJECT_VIEW_CONFIG.renderers.
|
||||
// Hver stub returnerer en deterministisk HTML-streng vi kan asserte mot.
|
||||
const RENDERER_IDS = ["renderAiActPyramid","renderRequirements","renderTransparency","renderFria","renderConformity","renderDpia","renderSecurity","renderRos","renderReview","renderCost","renderLicense","renderMigrate","renderAdr","renderSummary","renderPoc","renderUtredning","renderCompare"];
|
||||
const rendererStubs = RENDERER_IDS.map(function (n) {
|
||||
return n + ": function (data, slot) { if (slot) slot.innerHTML = \"<div class=\\\"stub-\" + " + JSON.stringify(n) + " + \"\\\">\" + (data && data.title || \"stub\") + \"</div>\"; }";
|
||||
}).join(", ");
|
||||
|
||||
const COMMANDS = [
|
||||
{ id: "classify", category: "regulatory", label: "EU AI Act — Klassifisering", description: "EU AI Act klass.", renderer: "renderAiActPyramid", produces_report: true, report_archetype: "aiact" },
|
||||
{ id: "requirements", category: "regulatory", label: "EU AI Act — Krav", description: "Krav per risiko", renderer: "renderRequirements", produces_report: true, report_archetype: "requirements-list" },
|
||||
{ id: "transparency", category: "regulatory", label: "Transparensnotis (Art. 13/50)", description: "Art. 13/50", renderer: "renderTransparency", produces_report: true, report_archetype: "text-document" },
|
||||
{ id: "frimpact", category: "regulatory", label: "FRIA (Art. 27)", description: "FRIA", renderer: "renderFria", produces_report: true, report_archetype: "fria" },
|
||||
{ id: "conformity", category: "regulatory", label: "Samsvarsvurdering (Art. 43)", description: "Annex IV", renderer: "renderConformity", produces_report: true, report_archetype: "conformity-checklist" },
|
||||
{ id: "dpia", category: "regulatory", label: "DPIA / PVK", description: "PVK", renderer: "renderDpia", produces_report: true, report_archetype: "matrix-risk" },
|
||||
{ id: "security", category: "security", label: "Sikkerhetsvurdering (6×5)", description: "6×5 scoring", renderer: "renderSecurity", produces_report: true, report_archetype: "matrix-risk-6x5" },
|
||||
{ id: "ros", category: "security", label: "ROS-analyse", description: "NS 5814", renderer: "renderRos", produces_report: true, report_archetype: "matrix-risk" },
|
||||
{ id: "review", category: "security", label: "Arkitekturgjennomgang", description: "Digdir/NSM", renderer: "renderReview", produces_report: true, report_archetype: "findings" },
|
||||
{ id: "cost", category: "economy", label: "Kostnadsestimat", description: "P10/P50/P90 NOK", renderer: "renderCost", produces_report: true, report_archetype: "cost-distribution" },
|
||||
{ id: "license", category: "economy", label: "Lisenskartlegging", description: "M365-lisenser", renderer: "renderLicense", produces_report: true, report_archetype: "scenario-comparison" },
|
||||
{ id: "migrate", category: "economy", label: "Migrasjonsplan", description: "Fase-plan", renderer: "renderMigrate", produces_report: true, report_archetype: "phase-plan" },
|
||||
{ id: "adr", category: "documentation", label: "ADR", description: "MADR v3.0", renderer: "renderAdr", produces_report: true, report_archetype: "adr" },
|
||||
{ id: "summary", category: "documentation", label: "Beslutningsnotat", description: "Sammendrag", renderer: "renderSummary", produces_report: true, report_archetype: "verdict" },
|
||||
{ id: "poc", category: "documentation", label: "POC-plan", description: "Suksesskriterier", renderer: "renderPoc", produces_report: true, report_archetype: "phase-plan" },
|
||||
{ id: "utredning", category: "documentation", label: "Utredning", description: "Utredningsinstr.", renderer: "renderUtredning", produces_report: true, report_archetype: "utredning" },
|
||||
{ id: "compare", category: "documentation", label: "Plattformsammenligning", description: "Plattformer", renderer: "renderCompare", produces_report: true, report_archetype: "scenario-comparison" }
|
||||
];
|
||||
|
||||
// Demo state med 5 fylte artifacts + ui i forskjellige tilstander.
|
||||
function buildDemoState(opts) {
|
||||
const o = opts || {};
|
||||
const now = "2026-05-15T10:00:00.000Z";
|
||||
return {
|
||||
dataVersion: 3,
|
||||
schemaVersion: 1,
|
||||
activeProjectId: "p1",
|
||||
projects: [{
|
||||
id: "p1",
|
||||
name: "Acme: Kunde-chatbot",
|
||||
description: "AI-system for objekt-deteksjon i sensordata.",
|
||||
createdAt: "2026-04-01T08:00:00.000Z",
|
||||
artifacts: {
|
||||
classify: {
|
||||
commandId: "classify",
|
||||
raw_markdown: "# EU AI Act — Klassifisering",
|
||||
parsed: { title: "Klassifisering", risk_level: "Høy", role: "Provider og Deployer" },
|
||||
verdict: "warning",
|
||||
keyStats: [{ label: "Risikonivå", value: "Høy" }],
|
||||
importedAt: now, updatedAt: now
|
||||
},
|
||||
ros: {
|
||||
commandId: "ros",
|
||||
raw_markdown: "# ROS-analyse",
|
||||
parsed: { title: "ROS", threats: [
|
||||
{ id: "T1", description: "Modell-bias", severity: "Høy" },
|
||||
{ id: "T2", description: "Privacy leak", severity: "Kritisk" },
|
||||
{ id: "T3", description: "Hallusinerte fakta", severity: "Medium" }
|
||||
]},
|
||||
verdict: "warning",
|
||||
keyStats: [],
|
||||
importedAt: now, updatedAt: now
|
||||
},
|
||||
security: {
|
||||
commandId: "security",
|
||||
raw_markdown: "# Sikkerhetsvurdering",
|
||||
parsed: { title: "Security", findings: [
|
||||
{ id: "S1", finding: "Manglende DLP", severity: "Kritisk" },
|
||||
{ id: "S2", finding: "Audit ufullstendig", severity: "Høy" }
|
||||
]},
|
||||
verdict: "block",
|
||||
keyStats: [],
|
||||
importedAt: now, updatedAt: now
|
||||
},
|
||||
dpia: {
|
||||
commandId: "dpia",
|
||||
raw_markdown: "# DPIA",
|
||||
parsed: { title: "DPIA", threats: [] },
|
||||
verdict: "go-with-conditions",
|
||||
keyStats: [],
|
||||
importedAt: now, updatedAt: now
|
||||
},
|
||||
cost: {
|
||||
commandId: "cost",
|
||||
raw_markdown: "# Kostnadsestimat",
|
||||
parsed: { title: "Kostnad", p10: 78000, p50: 142000, p90: 285000 },
|
||||
verdict: "approved",
|
||||
keyStats: [{ label: "P50/mnd", value: "142 000 NOK" }],
|
||||
importedAt: now, updatedAt: now
|
||||
}
|
||||
},
|
||||
reports: {}
|
||||
}],
|
||||
ui: {
|
||||
projectView: {
|
||||
selectedArtifactId: o.selectedArtifactId === undefined ? null : o.selectedArtifactId,
|
||||
searchQuery: o.searchQuery || ""
|
||||
},
|
||||
importModal: {
|
||||
open: !!o.importOpen,
|
||||
prefillCommandId: o.prefillCommandId || "",
|
||||
prefillMarkdown: o.prefillMarkdown || ""
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const stubs = `
|
||||
const window = {};
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function escapeAttr(s) { return escapeHtml(s); }
|
||||
function renderPageShell(opts, body) {
|
||||
const t = (opts && opts.title) || "";
|
||||
const e = (opts && opts.eyebrow) || "";
|
||||
const v = (opts && opts.verdict) || "";
|
||||
return "<header class=\\"page__header\\" data-eyebrow=\\"" + escapeAttr(e) + "\\" data-verdict=\\"" + escapeAttr(v) + "\\"><h1>" + escapeHtml(t) + "</h1></header>" + (body || "");
|
||||
}
|
||||
function renderVerdictPill(v) {
|
||||
return "<span class=\\"verdict-pill\\" data-verdict=\\"" + escapeAttr(String(v || "").toLowerCase()) + "\\">" + escapeHtml(String(v || "").toUpperCase()) + "</span>";
|
||||
}
|
||||
function renderKeyStatsGrid(s) { return "<div class=\\"key-stats\\">" + (Array.isArray(s) ? s.length : 0) + "</div>"; }
|
||||
function inferVerdict() { return "n-a"; }
|
||||
function inferKeyStats() { return []; }
|
||||
const PARSERS = {};
|
||||
const RENDERERS = { ` + rendererStubs + ` };
|
||||
const CATALOG = { commands: ` + JSON.stringify(COMMANDS) + ` };
|
||||
const ACTIONS = {};
|
||||
let __STORE_STATE = null;
|
||||
const store = {
|
||||
get state() { return __STORE_STATE; },
|
||||
save: function () {}
|
||||
};
|
||||
function setStoreState(s) { __STORE_STATE = s; }
|
||||
function findProject(id) {
|
||||
const list = (store.state && store.state.projects) || [];
|
||||
for (let i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
|
||||
return null;
|
||||
}
|
||||
function scheduleRender() {}
|
||||
`;
|
||||
|
||||
const wrapped = stubs + block + "\nreturn { renderProjectView, renderProjectHeader, renderArtifactNav, renderArtifactNavItem, renderProjectMain, renderProjectOverview, renderProjectArtifact, renderEmptyArtifactPrompt, renderImportModal, PROJECT_VIEW_CONFIG, setStoreState };";
|
||||
let api;
|
||||
try {
|
||||
api = (new Function(wrapped))();
|
||||
} catch (e) {
|
||||
console.error("EVAL_FAILED: " + e.message);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
function emit(ok, desc) { console.log((ok ? "PASS" : "FAIL") + "\t" + desc); }
|
||||
|
||||
// ---- API-eksponering ----
|
||||
emit(typeof api.renderProjectView === "function", "renderProjectView er funksjon");
|
||||
emit(typeof api.renderProjectOverview === "function", "renderProjectOverview er funksjon");
|
||||
emit(typeof api.renderImportModal === "function", "renderImportModal er funksjon");
|
||||
emit(typeof api.PROJECT_VIEW_CONFIG === "object" && api.PROJECT_VIEW_CONFIG !== null, "PROJECT_VIEW_CONFIG eksponert");
|
||||
|
||||
// ---- View 1: overview (selectedArtifactId null) ----
|
||||
let demo = buildDemoState({});
|
||||
api.setStoreState(demo);
|
||||
let project = demo.projects[0];
|
||||
let html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
|
||||
|
||||
emit(html.indexOf("class=\"project-view\"") !== -1 && html.indexOf("data-view=\"overview\"") !== -1,
|
||||
"overview: .project-view rot med data-view=\"overview\"");
|
||||
emit(html.indexOf("Acme: Kunde-chatbot") !== -1,
|
||||
"overview: prosjekt-navn rendres i header");
|
||||
emit(html.indexOf("data-action=\"import-open\"") !== -1,
|
||||
"overview: Importer-rapport-knapp finnes");
|
||||
// Alle 4 kategori-grupper i sidebaren
|
||||
emit(html.indexOf("Regulatorisk") !== -1 && html.indexOf("Risiko & sikkerhet") !== -1 && html.indexOf("Økonomi") !== -1 && html.indexOf("Dokumentasjon") !== -1,
|
||||
"overview: alle 4 kategori-labels rendres i nav");
|
||||
// Overview-tiles (verdict-grid)
|
||||
emit(html.indexOf("project-overview__verdict-tile") !== -1,
|
||||
"overview: project-overview__verdict-tile finnes");
|
||||
const tileCount = (html.match(/project-overview__verdict-tile/g) || []).length;
|
||||
emit(tileCount >= 4, "overview: minst 4 verdict-tiles (got " + tileCount + ")");
|
||||
// Top-risks (3 fra ros + 2 fra security = 5)
|
||||
emit(html.indexOf("top-risks") !== -1 && html.indexOf("Privacy leak") !== -1,
|
||||
"overview: top-risks-listen inkluderer ROS-trussel");
|
||||
// Next-actions / missing reports
|
||||
emit(html.indexOf("project-overview__next-actions") !== -1 || html.indexOf("empty-hint") !== -1,
|
||||
"overview: next-actions-seksjon rendres");
|
||||
emit(html.indexOf("project-overview__missing-reports") !== -1 || html.indexOf("Alle må-ha-rapporter er importert") !== -1,
|
||||
"overview: missing-reports-seksjon rendres");
|
||||
|
||||
// ---- View 2: artifact (filled) ----
|
||||
demo = buildDemoState({ selectedArtifactId: "classify" });
|
||||
api.setStoreState(demo);
|
||||
project = demo.projects[0];
|
||||
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
|
||||
emit(html.indexOf("data-view=\"artifact\"") !== -1,
|
||||
"artifact: data-view=\"artifact\"");
|
||||
emit(html.indexOf("project-view__artifact") !== -1 && html.indexOf("data-artifact=\"classify\"") !== -1,
|
||||
"artifact: .project-view__artifact med data-artifact=\"classify\"");
|
||||
emit(html.indexOf("project-view__artifact-title") !== -1 && html.indexOf("EU AI Act — Klassifisering") !== -1,
|
||||
"artifact: command-label vises i artifact-header");
|
||||
emit(html.indexOf("data-action=\"artifact-reimport\"") !== -1 && html.indexOf("data-action=\"artifact-delete\"") !== -1,
|
||||
"artifact: reimport + delete-action-knapper finnes");
|
||||
|
||||
// ---- View 3: empty (sidebar-treff uten artifact) ----
|
||||
demo = buildDemoState({ selectedArtifactId: "frimpact" });
|
||||
api.setStoreState(demo);
|
||||
project = demo.projects[0];
|
||||
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
|
||||
emit(html.indexOf("data-view=\"empty\"") !== -1,
|
||||
"empty: data-view=\"empty\"");
|
||||
emit(html.indexOf("empty-artifact-prompt") !== -1 && html.indexOf("data-command=\"frimpact\"") !== -1,
|
||||
"empty: empty-artifact-prompt for frimpact");
|
||||
emit(html.indexOf("data-prefill-command=\"frimpact\"") !== -1,
|
||||
"empty: Importer-knapp har data-prefill-command=\"frimpact\"");
|
||||
|
||||
// ---- View 4: import-modal-open ----
|
||||
demo = buildDemoState({ importOpen: true, prefillCommandId: "ros", prefillMarkdown: "# ROS-analyse\nDemo" });
|
||||
api.setStoreState(demo);
|
||||
project = demo.projects[0];
|
||||
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
|
||||
emit(html.indexOf("data-import-modal") !== -1,
|
||||
"import-modal: [data-import-modal] rendres når open=true");
|
||||
// Dropdown har 17 options + tom default = 18 <option>-tags
|
||||
const optionMatches = html.match(/<option /g) || [];
|
||||
emit(optionMatches.length >= 17,
|
||||
"import-modal: dropdown har 17+ options (got " + optionMatches.length + ")");
|
||||
emit(html.indexOf("value=\"ros\" selected") !== -1 || html.indexOf("value=\"ros\" selected") !== -1,
|
||||
"import-modal: prefillCommandId=\"ros\" gir selected option");
|
||||
emit(html.indexOf("# ROS-analyse") !== -1 || html.indexOf("ROS-analyse") !== -1,
|
||||
"import-modal: prefillMarkdown rendres i textarea");
|
||||
|
||||
// ---- Modal IKKE rendret når open=false ----
|
||||
demo = buildDemoState({});
|
||||
api.setStoreState(demo);
|
||||
project = demo.projects[0];
|
||||
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
|
||||
emit(html.indexOf("data-import-modal") === -1,
|
||||
"import-modal: ikke rendret når importModal.open=false");
|
||||
|
||||
// ---- renderArtifactNav-søk filtrerer ----
|
||||
demo = buildDemoState({ searchQuery: "ros" });
|
||||
api.setStoreState(demo);
|
||||
project = demo.projects[0];
|
||||
const navHtml = api.renderArtifactNav(project, api.PROJECT_VIEW_CONFIG, null, "ros");
|
||||
emit(navHtml.indexOf("data-artifact-id=\"ros\"") !== -1,
|
||||
"nav-filter: ROS-element finnes ved søk=\"ros\"");
|
||||
emit(navHtml.indexOf("data-artifact-id=\"cost\"") === -1,
|
||||
"nav-filter: cost-element filtrert ut ved søk=\"ros\"");
|
||||
|
||||
// ---- renderProjectView med null project ----
|
||||
emit(api.renderProjectView(null, api.PROJECT_VIEW_CONFIG).indexOf("Ingen prosjekt valgt") !== -1,
|
||||
"guard: null project → empty-hint");
|
||||
' "$HTML_FILE" 2>&1) || NODE_RC=$?
|
||||
|
||||
if [ "${NODE_RC:-0}" -ne 0 ]; then
|
||||
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
|
||||
print_summary; exit 1
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r status desc; do
|
||||
case "$status" in
|
||||
PASS) pass "$desc" ;;
|
||||
FAIL) fail "$desc" ;;
|
||||
WARN) warn "$desc" ;;
|
||||
esac
|
||||
done <<< "$NODE_OUT"
|
||||
|
||||
print_summary
|
||||
|
|
@ -223,20 +223,23 @@ else
|
|||
fi
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 13. data-report-slot per rapport-produserende command (17 stk)
|
||||
# 13. v1.15.0: artifact-slot rendres dynamisk via renderProjectArtifact
|
||||
# -------------------------------------------------------
|
||||
# v2 hadde per-command data-report-slot="..." på alle 17 cards. v3 har én
|
||||
# .project-main-zone der renderProjectArtifact mounter den valgte artefakten.
|
||||
# Sjekk i stedet at RENDERERS routing-objektet er wired for alle 17.
|
||||
REPORT_CMDS="classify requirements transparency frimpact conformity dpia security ros review cost license migrate adr summary poc utredning compare"
|
||||
slot_hits=0
|
||||
renderer_hits=0
|
||||
for c in $REPORT_CMDS; do
|
||||
if grep -qE "data-report-slot=[\"']${c}[\"']" "$HTML_FILE"; then
|
||||
pass "data-report-slot=\"${c}\" markup til stede"
|
||||
slot_hits=$((slot_hits + 1))
|
||||
# PROJECT_VIEW_CONFIG.renderers[<cid>] = RENDERERS.renderXxx
|
||||
if grep -qE "^[[:space:]]+${c}:[[:space:]]+RENDERERS\." "$HTML_FILE"; then
|
||||
pass "PROJECT_VIEW_CONFIG.renderers.${c} wired"
|
||||
renderer_hits=$((renderer_hits + 1))
|
||||
else
|
||||
warn "data-report-slot=\"${c}\" finnes ikke i statisk markup (kan rendres dynamisk)"
|
||||
fail "PROJECT_VIEW_CONFIG.renderers.${c} mangler"
|
||||
fi
|
||||
done
|
||||
# Slot rendrer dynamisk via render-funksjoner — warn kun, ingen fail
|
||||
pass "Report-slot-stikkprøve fullført ($slot_hits/17 statiske; resterende rendres dynamisk)"
|
||||
pass "v3 renderer-routing wired ($renderer_hits/17)"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 14. report_archetype-routing-felt i CATALOG-data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue