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>
445 lines
16 KiB
Bash
Executable file
445 lines
16 KiB
Bash
Executable file
#!/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, ">").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 !== 3) {
|
|
console.error("DATA_VERSION_NOT_BUMPED got=" + stateA.dataVersion);
|
|
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); }
|
|
|
|
// 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 + " artifacts=" + artifactsBuilt);
|
|
} 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 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");
|
|
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" = "3" ]; then
|
|
pass "dataVersion bumpes til 3 (kjede v?→v3)"
|
|
else
|
|
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
|