import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const contractPath = path.join(root, "docs/web/deployment-contract.json"); const contract = JSON.parse(fs.readFileSync(contractPath, "utf8")); const guide = fs.readFileSync(path.join(root, "docs/web/DEPLOYMENT.md"), "utf8"); const vite = fs.readFileSync(path.join(root, "web/app/vite.config.ts"), "utf8"); const releaseCreator = fs.readFileSync(path.join(root, "tools/web/create-offline-release.mjs"), "utf8"); assert.equal(contract.schemaVersion, 1); assert.equal(contract.product, "Web Blender Modeler V1"); assert.equal(contract.browserScope, "Chromium"); assert.deepEqual(contract.methods, ["GET", "HEAD"]); assert.equal(contract.transport.production, "https"); assert.equal(contract.transport.localDevelopment, "http://127.0.0.1"); assert.equal(contract.transport.fileProtocolSupported, false); assert.equal(contract.transport.sameOriginRuntimeAssets, true); assert.deepEqual(contract.responseHeaders.allResponses, { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Resource-Policy": "same-origin", }); const routes = Object.fromEntries(contract.responseHeaders.routes.map((route) => [route.id, route])); assert.deepEqual(routes["entry-document"], { id: "entry-document", patterns: ["/", "/index.html"], cacheControl: "no-cache" }); assert.deepEqual(routes["runtime-manifest"], { id: "runtime-manifest", patterns: ["/engine-manifest.json"], cacheControl: "no-cache" }); assert.deepEqual(routes["content-hashed-assets"], { id: "content-hashed-assets", patterns: ["/assets/*"], cacheControl: "public, max-age=31536000, immutable" }); assert.deepEqual(routes["stable-engine-assets"], { id: "stable-engine-assets", patterns: ["/vendor/blender/*"], cacheControl: "no-cache" }); assert.deepEqual(routes.fallback, { id: "fallback", patterns: ["*"], cacheControl: "no-cache" }); for (const [extension, mime] of Object.entries({ ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".wasm": "application/wasm", ".json": "application/json; charset=utf-8", ".png": "image/png", ".wav": "audio/wav", ".blend": "application/octet-stream", ".nvdb": "application/x-nanovdb", })) assert.equal(contract.mimeTypes[extension], mime, `${extension} MIME drifted`); assert.deepEqual(contract.rangeRequests.extensions, [".wasm", ".blend", ".nvdb"]); assert.equal(contract.rangeRequests.unit, "bytes"); assert.equal(contract.rangeRequests.singleRangeOnly, true); assert.equal(contract.rangeRequests.etag, "strong-sha256"); assert.equal(contract.rangeRequests.satisfiedStatus, 206); assert.equal(contract.rangeRequests.unsatisfiedStatus, 416); assert.deepEqual(contract.rangeRequests.requiredSatisfiedHeaders, ["Accept-Ranges", "Content-Range", "Content-Length", "ETag"]); assert.equal(contract.rangeRequests.ifRangeMismatch, "return-full-200"); for (const value of Object.values(contract.responseHeaders.allResponses)) assert.ok(guide.includes(`\`${value}\``)); for (const status of ["206", "416", "304", "200"]) assert.ok(guide.includes(`\`${status}\``)); for (const route of contract.responseHeaders.routes) assert.ok(guide.includes(route.cacheControl)); for (const section of ["Empty-directory install runbook", "Transport preflight", "Upgrade runbook", "Rollback runbook", "Failure diagnostics", "Fresh-directory rehearsal"]) assert.ok(guide.includes(`## ${section}`)); for (const token of ["BLENDER_WEB_INSTALL_ROOT", "window.isSecureContext === true", "window.crossOriginIsolated === true"]) assert.ok(guide.includes(token)); assert.match(vite, /deployment-contract\.json/); assert.match(vite, /responseHeaders\.allResponses/); assert.match(releaseCreator, /deployment-contract\.json/); assert.match(releaseCreator, /DEPLOYMENT\.md/); assert.doesNotMatch(releaseCreator, /Serve app\/ from any local static HTTP server/); process.stdout.write(`deployment-contract-ok headers=3 routes=${contract.responseHeaders.routes.length} mime=${Object.keys(contract.mimeTypes).length} rangeExtensions=${contract.rangeRequests.extensions.length}\n`);