|
| 1 | +import assert from "node:assert/strict"; |
| 2 | +import fs from "node:fs"; |
| 3 | +import path from "node:path"; |
| 4 | +import { execFileSync } from "node:child_process"; |
| 5 | +import { fileURLToPath, pathToFileURL } from "node:url"; |
| 6 | + |
| 7 | +const __filename = fileURLToPath(import.meta.url); |
| 8 | +const __dirname = path.dirname(__filename); |
| 9 | +const repoRoot = path.resolve(__dirname, "..", ".."); |
| 10 | +const workspaceJsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js"); |
| 11 | +const resultsPath = path.join(repoRoot, "tmp", "v2-selection-persistence-results.json"); |
| 12 | + |
| 13 | +function readText(filePath) { |
| 14 | + return fs.readFileSync(filePath, "utf8"); |
| 15 | +} |
| 16 | + |
| 17 | +function checkSyntax(jsPath) { |
| 18 | + try { |
| 19 | + execFileSync(process.execPath, ["--check", jsPath], { |
| 20 | + cwd: repoRoot, |
| 21 | + stdio: ["ignore", "pipe", "pipe"] |
| 22 | + }); |
| 23 | + return { ok: true, error: "" }; |
| 24 | + } catch (error) { |
| 25 | + return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() }; |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +function safeParse(raw) { |
| 30 | + try { |
| 31 | + return { ok: true, value: JSON.parse(raw) }; |
| 32 | + } catch { |
| 33 | + return { ok: false, value: null }; |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +function isValidPayload(payload) { |
| 38 | + return Boolean(payload && typeof payload === "object" && !Array.isArray(payload)); |
| 39 | +} |
| 40 | + |
| 41 | +function readPersistedSelection(store) { |
| 42 | + const raw = store["v2-session-selection"]; |
| 43 | + if (!raw) return { sessionA: "", sessionB: "" }; |
| 44 | + const parsed = safeParse(raw); |
| 45 | + if (!parsed.ok || !parsed.value || typeof parsed.value !== "object" || Array.isArray(parsed.value)) { |
| 46 | + return { sessionA: "", sessionB: "" }; |
| 47 | + } |
| 48 | + return { |
| 49 | + sessionA: typeof parsed.value.sessionA === "string" ? parsed.value.sessionA.trim() : "", |
| 50 | + sessionB: typeof parsed.value.sessionB === "string" ? parsed.value.sessionB.trim() : "" |
| 51 | + }; |
| 52 | +} |
| 53 | + |
| 54 | +function resolvePersistedSelectionIds(entries, store) { |
| 55 | + if (!Array.isArray(entries) || entries.length < 2) return { leftId: "", rightId: "" }; |
| 56 | + const persisted = readPersistedSelection(store); |
| 57 | + if (!persisted.sessionA || !persisted.sessionB || persisted.sessionA === persisted.sessionB) { |
| 58 | + return { leftId: "", rightId: "" }; |
| 59 | + } |
| 60 | + const left = entries.find((entry) => entry.contextId === persisted.sessionA && isValidPayload(entry.payload)); |
| 61 | + const right = entries.find((entry) => entry.contextId === persisted.sessionB && isValidPayload(entry.payload)); |
| 62 | + if (!left || !right || left.id === right.id) return { leftId: "", rightId: "" }; |
| 63 | + return { leftId: left.id, rightId: right.id }; |
| 64 | +} |
| 65 | + |
| 66 | +function writePersistedSelection(store, leftEntry, rightEntry) { |
| 67 | + const sessionA = leftEntry && typeof leftEntry.contextId === "string" ? leftEntry.contextId : ""; |
| 68 | + const sessionB = rightEntry && typeof rightEntry.contextId === "string" ? rightEntry.contextId : ""; |
| 69 | + store["v2-session-selection"] = JSON.stringify({ sessionA, sessionB }); |
| 70 | +} |
| 71 | + |
| 72 | +function clearPersistedSelection(store) { |
| 73 | + delete store["v2-session-selection"]; |
| 74 | +} |
| 75 | + |
| 76 | +function canRunAction(leftId, rightId) { |
| 77 | + return Boolean(leftId && rightId && leftId !== rightId); |
| 78 | +} |
| 79 | + |
| 80 | +export function run() { |
| 81 | + const failures = []; |
| 82 | + const jsExists = fs.existsSync(workspaceJsPath); |
| 83 | + const js = jsExists ? readText(workspaceJsPath) : ""; |
| 84 | + const syntax = checkSyntax(workspaceJsPath); |
| 85 | + |
| 86 | + const hasStorageKey = js.includes("this.sessionSelectionStorageKey = \"v2-session-selection\";"); |
| 87 | + const hasRead = js.includes("readPersistedSessionSelection()"); |
| 88 | + const hasWrite = js.includes("writePersistedSessionSelection(leftEntry, rightEntry)"); |
| 89 | + const hasResolve = js.includes("resolvePersistedSelectionIds(entries)"); |
| 90 | + const hasClear = js.includes("clearPersistedSessionSelection()"); |
| 91 | + const hasResetHook = js.includes("this.clearPersistedSessionSelection();"); |
| 92 | + |
| 93 | + if (!jsExists) failures.push("Missing tools/workspace-v2/index.js."); |
| 94 | + if (!syntax.ok) failures.push("workspace-v2/index.js failed syntax check."); |
| 95 | + if (!hasStorageKey) failures.push("Missing v2-session-selection storage key."); |
| 96 | + if (!hasRead) failures.push("Missing readPersistedSessionSelection()."); |
| 97 | + if (!hasWrite) failures.push("Missing writePersistedSessionSelection(...)."); |
| 98 | + if (!hasResolve) failures.push("Missing resolvePersistedSelectionIds(...)."); |
| 99 | + if (!hasClear) failures.push("Missing clearPersistedSessionSelection()."); |
| 100 | + if (!hasResetHook) failures.push("Missing reset hook that clears persisted selection."); |
| 101 | + |
| 102 | + const entries = [ |
| 103 | + { id: "history:ctx-a", contextId: "ctx-a", payload: { toolId: "asset-browser-v2", payloadJson: { a: 1 } } }, |
| 104 | + { id: "history:ctx-b", contextId: "ctx-b", payload: { toolId: "asset-browser-v2", payloadJson: { b: 2 } } } |
| 105 | + ]; |
| 106 | + const store = {}; |
| 107 | + |
| 108 | + writePersistedSelection(store, entries[0], entries[1]); |
| 109 | + const persisted = readPersistedSelection(store); |
| 110 | + if (persisted.sessionA !== "ctx-a" || persisted.sessionB !== "ctx-b") { |
| 111 | + failures.push("Selections were not persisted correctly."); |
| 112 | + } |
| 113 | + |
| 114 | + const restored = resolvePersistedSelectionIds(entries, store); |
| 115 | + if (restored.leftId !== "history:ctx-a" || restored.rightId !== "history:ctx-b") { |
| 116 | + failures.push("Valid persisted selections were not restored."); |
| 117 | + } |
| 118 | + |
| 119 | + const deletedEntries = [{ id: "history:ctx-a", contextId: "ctx-a", payload: { toolId: "asset-browser-v2", payloadJson: { a: 1 } } }]; |
| 120 | + const deletedRestore = resolvePersistedSelectionIds(deletedEntries, store); |
| 121 | + if (deletedRestore.leftId || deletedRestore.rightId) { |
| 122 | + failures.push("Deleted/missing sessions should not restore."); |
| 123 | + } |
| 124 | + |
| 125 | + store["v2-session-selection"] = JSON.stringify({ sessionA: "ctx-a", sessionB: "ctx-a" }); |
| 126 | + const sameRestore = resolvePersistedSelectionIds(entries, store); |
| 127 | + if (sameRestore.leftId || sameRestore.rightId) { |
| 128 | + failures.push("Same-session restore should be rejected."); |
| 129 | + } |
| 130 | + |
| 131 | + store["v2-session-selection"] = JSON.stringify({ sessionA: "ctx-a", sessionB: "ctx-b" }); |
| 132 | + const restoredForState = resolvePersistedSelectionIds(entries, store); |
| 133 | + if (!canRunAction(restoredForState.leftId, restoredForState.rightId)) { |
| 134 | + failures.push("Buttons should reflect restored valid distinct state as enabled."); |
| 135 | + } |
| 136 | + |
| 137 | + clearPersistedSelection(store); |
| 138 | + if (Object.prototype.hasOwnProperty.call(store, "v2-session-selection")) { |
| 139 | + failures.push("Reset should clear v2-session-selection."); |
| 140 | + } |
| 141 | + |
| 142 | + fs.mkdirSync(path.dirname(resultsPath), { recursive: true }); |
| 143 | + fs.writeFileSync(resultsPath, `${JSON.stringify({ |
| 144 | + generatedAt: new Date().toISOString(), |
| 145 | + failures, |
| 146 | + checks: { |
| 147 | + jsExists, |
| 148 | + syntax, |
| 149 | + hasStorageKey, |
| 150 | + hasRead, |
| 151 | + hasWrite, |
| 152 | + hasResolve, |
| 153 | + hasClear, |
| 154 | + hasResetHook |
| 155 | + }, |
| 156 | + scenarios: { |
| 157 | + persisted, |
| 158 | + restored, |
| 159 | + deletedRestore, |
| 160 | + sameRestore, |
| 161 | + restoredForState, |
| 162 | + selectionCleared: !Object.prototype.hasOwnProperty.call(store, "v2-session-selection") |
| 163 | + } |
| 164 | + }, null, 2)}\n`, "utf8"); |
| 165 | + |
| 166 | + console.log(`v2 selection persistence results: ${resultsPath}`); |
| 167 | + assert.equal(failures.length, 0, `V2 selection persistence failures: ${failures.join(" | ")}`); |
| 168 | + return { failures }; |
| 169 | +} |
| 170 | + |
| 171 | +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| 172 | + try { |
| 173 | + const summary = run(); |
| 174 | + console.log(JSON.stringify(summary, null, 2)); |
| 175 | + } catch (error) { |
| 176 | + console.error(error); |
| 177 | + process.exitCode = 1; |
| 178 | + } |
| 179 | +} |
0 commit comments