ktg-plugin-marketplace/plugins/ms-ai-architect/tests/test-playground-actions.sh
Kjell Tore Guttormsen d8882f5220 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>
2026-05-16 20:58:51 +02:00

248 lines
10 KiB
Bash
Executable file

#!/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