#!/bin/bash # test-playground-migrations.sh — Playground v3 dataVersion v1->v2 idempotency # # Verifiserer: # 1. fixture (playground/test-fixtures/state-v1-snapshot.json) eksisterer + er v1 # 2. V2_FOUNDATION-blokken kan ekstraheres fra HTML-fila # 3. migrateDataVersion(fixture) gir resultat A # 4. migrateDataVersion(A) gir resultat B # 5. JSON.stringify(A) === JSON.stringify(B) -> idempotency # 6. A.dataVersion === 2 -> migrasjonen ble utført # # Bash 3.2-kompatibel. Bruker node til JS-eval; ingen npm-deps. # Designet for å integreres i tests/run-e2e.sh --playground. set -euo pipefail PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html" FIXTURE_FILE="$PLUGIN_ROOT/playground/test-fixtures/state-v1-snapshot.json" # shellcheck disable=SC1091 source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh" init_suite "Playground v3 — dataVersion v1->v2 migration idempotency" # ---- 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 [ ! -f "$FIXTURE_FILE" ]; then fail "Fixture mangler: $FIXTURE_FILE" print_summary; exit 1 fi pass "Fixture finnes: $(basename "$FIXTURE_FILE")" # ---- 2. Fixture er v1 (mangler dataVersion eller dataVersion < 2) ---- if node -e " const f = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); if (f.dataVersion === 2) { console.error('fixture er allerede v2'); process.exit(1); } if (typeof f.schemaVersion !== 'number') { console.error('fixture mangler schemaVersion'); process.exit(1); } if (!Array.isArray(f.projects) || f.projects.length === 0) { console.error('fixture har ingen projects'); process.exit(1); } " "$FIXTURE_FILE" 2>/dev/null; then pass "Fixture er v1-state (dataVersion ikke satt)" else fail "Fixture er ikke gyldig v1-state" print_summary; exit 1 fi # ---- 3. V2_FOUNDATION-markører eksisterer i HTML ---- if grep -q "V2_FOUNDATION_BEGIN" "$HTML_FILE" && grep -q "V2_FOUNDATION_END" "$HTML_FILE"; then pass "V2_FOUNDATION_BEGIN/END markører finnes" else fail "Mangler V2_FOUNDATION-markører i HTML" print_summary; exit 1 fi # ---- 4. migrateDataVersion-funksjonen er definert ---- if grep -q "function migrateDataVersion" "$HTML_FILE"; then pass "migrateDataVersion-funksjonen finnes" else fail "migrateDataVersion-funksjonen mangler" print_summary; exit 1 fi # ---- 5. Eval idempotency-test i node ---- # Strategi: ekstraher V2_FOUNDATION-blokken via sed, wrap med stubs (window, # CATALOG-mock som returnerer archetype basert på fixture command-IDs), eval, # kjør migrasjon to ganger på fixture-deep-copy, sammenlign JSON.stringify. IDEMPOTENCY_RESULT=$(node -e ' const fs = require("fs"); const path = require("path"); const htmlPath = process.argv[1]; const fixturePath = process.argv[2]; const html = fs.readFileSync(htmlPath, "utf8"); const beginMarker = "// === V2_FOUNDATION_BEGIN ==="; const endMarker = "// === V2_FOUNDATION_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); // Stub-ene som blokken trenger const stubs = ` const window = {}; function escapeHtml(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escapeAttr(s) { return escapeHtml(s); } // Mock CATALOG: kun de command-id-ene fixture-en bruker. 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 wrapped = stubs + block + "\nreturn { migrateDataVersion, defaultArchetypeFor };"; let api; try { api = (new Function(wrapped))(); } catch (e) { console.error("EVAL_FAILED:", e.message); process.exit(3); } const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8")); // Deep clones så vi sammenligner uavhengige objekter const stateA = JSON.parse(JSON.stringify(fixture)); api.migrateDataVersion(stateA, api.defaultArchetypeFor); const stateB = JSON.parse(JSON.stringify(stateA)); 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"); process.exit(4); } // Sjekk at minst én rapport fikk verdict + keyStats utledet let verdictsAdded = 0, statsAdded = 0; for (const p of (stateA.projects || [])) { for (const id of Object.keys(p.reports || {})) { const r = p.reports[id]; if (r && r.parsed) { if (r.parsed.verdict != null) verdictsAdded++; if (Array.isArray(r.parsed.keyStats)) statsAdded++; } } } if (verdictsAdded === 0) { console.error("NO_VERDICTS_ADDED"); process.exit(5); } if (statsAdded === 0) { console.error("NO_KEYSTATS_ADDED"); process.exit(6); } if (a === b) { console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded); } else { console.error("NOT_IDEMPOTENT"); process.exit(7); } ' "$HTML_FILE" "$FIXTURE_FILE" 2>&1) || RC=$? if [ "${RC:-0}" -eq 0 ] && echo "$IDEMPOTENCY_RESULT" | grep -q "^IDEMPOTENT"; then pass "migrateDataVersion er idempotent ($IDEMPOTENCY_RESULT)" else fail "Idempotency-test feilet: $IDEMPOTENCY_RESULT" fi # ---- 6. dataVersion bumpes til 2 ved første kjøring ---- DV_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" } ]}; `; const api = (new Function(stubs + block + "\nreturn { migrateDataVersion, defaultArchetypeFor };"))(); const state = { schemaVersion: 1, projects: [] }; api.migrateDataVersion(state, api.defaultArchetypeFor); console.log(state.dataVersion); ' "$HTML_FILE" 2>&1) || true if [ "$DV_RESULT" = "2" ]; then pass "dataVersion bumpes til 2" else fail "dataVersion ble ikke bumpet til 2 (got '$DV_RESULT')" fi print_summary