|
| 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 fixturePath = path.join(repoRoot, "tests", "fixtures", "v2-tools", "asset-browser-v2.json"); |
| 11 | +const toolJsPath = path.join(repoRoot, "tools", "asset-browser-v2", "index.js"); |
| 12 | +const resultsPath = path.join(repoRoot, "tmp", "pr_11_313_asset_browser_results.json"); |
| 13 | + |
| 14 | +function cloneJson(value) { |
| 15 | + return JSON.parse(JSON.stringify(value)); |
| 16 | +} |
| 17 | + |
| 18 | +function validateAssetBrowserSession(sessionContext) { |
| 19 | + if (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext)) { |
| 20 | + return { state: "INVALID", message: "Session context is invalid." }; |
| 21 | + } |
| 22 | + if (sessionContext.version !== "v2") { |
| 23 | + return { state: "INVALID", message: "Unsupported session version." }; |
| 24 | + } |
| 25 | + if (typeof sessionContext.toolId !== "string" || sessionContext.toolId.trim() !== "asset-browser-v2") { |
| 26 | + return { state: "INVALID", message: "Expected toolId 'asset-browser-v2'." }; |
| 27 | + } |
| 28 | + if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) { |
| 29 | + return { state: "INVALID", message: "Expected payloadJson object." }; |
| 30 | + } |
| 31 | + if (typeof sessionContext.payloadJson.importName === "string" || typeof sessionContext.payloadJson.importDestination === "string") { |
| 32 | + return { state: "INVALID", message: "Legacy importName/importDestination is not allowed." }; |
| 33 | + } |
| 34 | + if (!sessionContext.payloadJson.assetCatalog || typeof sessionContext.payloadJson.assetCatalog !== "object" || Array.isArray(sessionContext.payloadJson.assetCatalog)) { |
| 35 | + return { state: "INVALID", message: "Expected payloadJson.assetCatalog." }; |
| 36 | + } |
| 37 | + if (typeof sessionContext.payloadJson.assetCatalog.name !== "string" || !sessionContext.payloadJson.assetCatalog.name.trim()) { |
| 38 | + return { state: "INVALID", message: "Expected assetCatalog.name." }; |
| 39 | + } |
| 40 | + if (!Array.isArray(sessionContext.payloadJson.assetCatalog.entries)) { |
| 41 | + return { state: "INVALID", message: "Expected assetCatalog.entries[]." }; |
| 42 | + } |
| 43 | + if (sessionContext.payloadJson.assetCatalog.entries.some((entry) => |
| 44 | + !entry || |
| 45 | + typeof entry !== "object" || |
| 46 | + Array.isArray(entry) || |
| 47 | + typeof entry.id !== "string" || |
| 48 | + !entry.id.trim() || |
| 49 | + typeof entry.label !== "string" || |
| 50 | + !entry.label.trim() || |
| 51 | + typeof entry.kind !== "string" || |
| 52 | + !entry.kind.trim() || |
| 53 | + typeof entry.path !== "string" || |
| 54 | + !entry.path.trim() |
| 55 | + )) { |
| 56 | + return { state: "INVALID", message: "Each entry must include id, label, kind, and path." }; |
| 57 | + } |
| 58 | + if (sessionContext.payloadJson.assetCatalog.entries.length === 0) { |
| 59 | + return { state: "VALID_EMPTY", message: "Valid catalog with zero assets." }; |
| 60 | + } |
| 61 | + return { state: "VALID_ENTRIES", message: "Valid catalog with renderable assets." }; |
| 62 | +} |
| 63 | + |
| 64 | +function checkSyntax(filePath) { |
| 65 | + execFileSync(process.execPath, ["--check", filePath], { |
| 66 | + cwd: repoRoot, |
| 67 | + stdio: ["ignore", "pipe", "pipe"] |
| 68 | + }); |
| 69 | +} |
| 70 | + |
| 71 | +export function run() { |
| 72 | + checkSyntax(toolJsPath); |
| 73 | + const fixtureJson = JSON.parse(fs.readFileSync(fixturePath, "utf8")); |
| 74 | + const toolSource = fs.readFileSync(toolJsPath, "utf8"); |
| 75 | + const validWithEntries = validateAssetBrowserSession(fixtureJson.sessionContext); |
| 76 | + |
| 77 | + const validEmptyContext = cloneJson(fixtureJson.sessionContext); |
| 78 | + validEmptyContext.payloadJson.assetCatalog.entries = []; |
| 79 | + const validEmpty = validateAssetBrowserSession(validEmptyContext); |
| 80 | + |
| 81 | + const invalidPayloadContext = cloneJson(fixtureJson.sessionContext); |
| 82 | + delete invalidPayloadContext.payloadJson; |
| 83 | + const invalidPayload = validateAssetBrowserSession(invalidPayloadContext); |
| 84 | + |
| 85 | + const invalidLegacyHintContext = cloneJson(fixtureJson.sessionContext); |
| 86 | + invalidLegacyHintContext.payloadJson.importName = "future-import"; |
| 87 | + const invalidLegacyHint = validateAssetBrowserSession(invalidLegacyHintContext); |
| 88 | + |
| 89 | + const invalidEntryContext = cloneJson(fixtureJson.sessionContext); |
| 90 | + invalidEntryContext.payloadJson.assetCatalog.entries[0] = { |
| 91 | + id: "broken", |
| 92 | + label: "Broken", |
| 93 | + kind: "image" |
| 94 | + }; |
| 95 | + const invalidEntry = validateAssetBrowserSession(invalidEntryContext); |
| 96 | + |
| 97 | + const summary = { |
| 98 | + generatedAt: new Date().toISOString(), |
| 99 | + checks: { |
| 100 | + hasPayloadCatalogValidation: toolSource.includes("payloadJson.assetCatalog"), |
| 101 | + hasLegacyFutureHintRejection: toolSource.includes("importName/importDestination"), |
| 102 | + hasEmptyValidStateMessage: toolSource.includes("Asset catalog is valid but empty") |
| 103 | + }, |
| 104 | + cases: { |
| 105 | + validWithEntries, |
| 106 | + validEmpty, |
| 107 | + invalidPayload, |
| 108 | + invalidLegacyHint, |
| 109 | + invalidEntry |
| 110 | + } |
| 111 | + }; |
| 112 | + |
| 113 | + fs.mkdirSync(path.dirname(resultsPath), { recursive: true }); |
| 114 | + fs.writeFileSync(resultsPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8"); |
| 115 | + |
| 116 | + assert.equal(summary.checks.hasPayloadCatalogValidation, true, "Asset Browser V2 must validate payloadJson.assetCatalog."); |
| 117 | + assert.equal(summary.checks.hasLegacyFutureHintRejection, true, "Asset Browser V2 must reject legacy future hint fields."); |
| 118 | + assert.equal(summary.checks.hasEmptyValidStateMessage, true, "Asset Browser V2 must expose an explicit empty valid-catalog message."); |
| 119 | + assert.equal(validWithEntries.state, "VALID_ENTRIES"); |
| 120 | + assert.equal(validEmpty.state, "VALID_EMPTY"); |
| 121 | + assert.equal(invalidPayload.state, "INVALID"); |
| 122 | + assert.equal(invalidLegacyHint.state, "INVALID"); |
| 123 | + assert.equal(invalidEntry.state, "INVALID"); |
| 124 | + return summary; |
| 125 | +} |
| 126 | + |
| 127 | +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| 128 | + try { |
| 129 | + const summary = run(); |
| 130 | + console.log(JSON.stringify(summary, null, 2)); |
| 131 | + } catch (error) { |
| 132 | + console.error(error); |
| 133 | + process.exitCode = 1; |
| 134 | + } |
| 135 | +} |
0 commit comments