From c2d9b3c2e991597aa21c8ed4288c228b76a52294 Mon Sep 17 00:00:00 2001 From: Leiter Consulting <152733483+LeiterConsulting@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:40:03 -0400 Subject: [PATCH] Distinguish unavailable delivery checks from application failures --- docs/architecture/content-publication.md | 2 + docs/audits/2026-10-07-browser-recovery.json | 208 +++++++++++++++++++ docs/browser-verification-recovery.md | 11 + docs/releases/2026/10/2026-10-07.md | 6 + scripts/diagnose-content.cjs | 26 ++- tests/diagnose-content.test.cjs | 83 ++++++++ 6 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 docs/audits/2026-10-07-browser-recovery.json create mode 100644 docs/browser-verification-recovery.md create mode 100644 tests/diagnose-content.test.cjs diff --git a/docs/architecture/content-publication.md b/docs/architecture/content-publication.md index a92c19e..59064c7 100644 --- a/docs/architecture/content-publication.md +++ b/docs/architecture/content-publication.md @@ -31,6 +31,8 @@ Preserve exact GitHub/Sites source-tree, saved-version, successful-deployment an Run `npm run content:diagnose -- https://versioncompass.com/` from the matching source checkout. It checks actual database health, manifest parity, bundle integrity, bounded queries, rejected writes and fallback assets. Failures include specific repair recommendations; its success does not substitute for browser/export evidence. +Unavailable responses or interrupted body reads are recorded as `blocked` with the request method, path, phase and error name. A returned bad status, malformed JSON or integrity mismatch is `failed`. Both prevent certification and exit nonzero. Progress goes to stderr; the final JSON remains on stdout. A timeout alone does not establish a defective database, missing adapter or accepted write. Retry the exact check after evidence of recovery and correlate Worker request logs before proposing an application repair. Browser-service availability, browser capabilities and actual report verification are separate observations; see [browser recovery](../browser-verification-recovery.md). + ## Further increments Use the [evidence-based delivery policy](../soak-policy.md): advance as soon as applicable material checks pass, with no mandatory 72-hour delay. The observation window may run alongside delivery. The [supported publisher integration](publisher-integration.md) separates verified staging from activation and isolates the prepared active reader while its browser/export checks are unavailable. Reuse the Site and established tasks and preserve their schedules. Privacy/audience expansion, destructive schema changes, commercial commitments and uncertain facts remain held for review. diff --git a/docs/audits/2026-10-07-browser-recovery.json b/docs/audits/2026-10-07-browser-recovery.json new file mode 100644 index 0000000..9605af0 --- /dev/null +++ b/docs/audits/2026-10-07-browser-recovery.json @@ -0,0 +1,208 @@ +{ + "schemaVersion": 1, + "recordedAt": "2026-10-07T15:37:16.521Z", + "scope": "Maintenance diagnostic classification and browser recovery; no product, report, public delivery or storage implementation change", + "baseline": { + "githubCommit": "c10be2019cd0cde6c63125b9b45836461140b2fb", + "sitesCommit": "e9ba955eb8d591e9810c9fe8a9b879947830af87", + "matchingTree": "ea2bc5e1ba728afbfffbec79cbbbb08e488db7e2", + "sitesVersion": 122, + "contentRevision": "content-6dcf491dfeb864140adc2937", + "engineRevision": "engine-c5ff569aec1b2f034270" + }, + "browserRecovery": { + "outcome": "passed", + "evidence": "Supported cloud browser inventory, current Chrome tab binding, navigation to the previously timed-out Splunk compliance page, and US Public Sector tab interaction returned visible page state and screenshot.", + "qualification": "Navigation and tab interaction only. No full fresh compliance-matrix claim review, affected-candidate report journey, native PDF, independent saved HTML rendering or narrow-screen emulation is claimed." + }, + "rootCause": { + "outcome": "not-established", + "evidence": "Prior audit records browser-service timeouts, including reset. The current browser works. No internal browser-service incident log was available. A timeout alone does not establish bot detection, lost user permission or an application defect." + }, + "liveDeliveryRetry": { + "evaluatedAt": "2026-10-07T15:31:52.523Z", + "target": "https://versioncompass.com", + "expectedRevision": "content-6dcf491dfeb864140adc2937", + "status": "issues-detected", + "checks": { + "database-health": { + "outcome": "failed", + "evidence": "The operation was aborted due to timeout", + "checkedAt": "2026-10-07T15:31:12.265Z" + }, + "live-manifest": { + "outcome": "failed", + "evidence": "The operation was aborted due to timeout", + "checkedAt": "2026-10-07T15:31:22.268Z" + }, + "bundle-integrity": { + "outcome": "passed", + "evidence": { + "digest": "2ac4b475d0e9d1fa3fbb71cacdf98d617e485a2ac56dfa3251da7ee3e33a444d", + "recordCount": 668 + }, + "checkedAt": "2026-10-07T15:31:27.722Z" + }, + "bounded-record-query": { + "outcome": "passed", + "evidence": "SOAR filter, revision and maximum page size verified", + "checkedAt": "2026-10-07T15:31:35.282Z" + }, + "read-only-api": { + "outcome": "passed", + "evidence": "POST rejected with 405", + "checkedAt": "2026-10-07T15:31:42.222Z" + }, + "bundled-fallback-assets": { + "outcome": "passed", + "evidence": "All seven complete fallback adapters served", + "checkedAt": "2026-10-07T15:31:52.523Z" + } + }, + "findings": [ + { + "observed": "database-health: The operation was aborted due to timeout", + "impact": "Database delivery cannot be certified", + "recommendedChange": "Check D1 binding and applied schema; preserve complete bundled fallback while repairing delivery.", + "reversible": true, + "checksPassed": false, + "resolved": false + }, + { + "observed": "live-manifest: The operation was aborted due to timeout", + "impact": "Database delivery cannot be certified", + "recommendedChange": "Reconcile the published source/build and cache before publishing the exact validated revision.", + "reversible": true, + "checksPassed": false, + "resolved": false + } + ], + "limitations": [ + "Browser journeys, actual HTML reopening and native PDF rendering require separate evidence." + ] + }, + "workerLogs": { + "outcome": "inspected", + "evidence": "Recent production log inspection returned expected 400 maximum-limit rejections, 404 icon/robots requests and successful 200 bundle reads; those entries had outcome ok. No crash was found in the inspected entries. Logs are sampled/recent and do not prove absence of all application errors." + }, + "repair": { + "unavailableResponse": "blocked with method/path/phase/error name; certification remains nonzero", + "returnedInvalidResponse": "failed with the existing concrete repair recommendation", + "progress": "running and completed checks on stderr; final JSON on stdout", + "automaticRetries": false + }, + "validation": { + "generationGates": "all four passed", + "tests": { + "passed": 161, + "failed": 0 + }, + "build": { + "assets": 53, + "revision": "content-6dcf491dfeb864140adc2937", + "engineRevision": "engine-c5ff569aec1b2f034270", + "workerSha256": "38895fba2ddee9cfc21a674f45b26c561cbc41b9b7d2133563d85dedfd4bd1df", + "identicalToDeployedSourceBuild": true + }, + "browserExportScope": { + "unaffected": true, + "rationale": "Only the maintenance script, its regression tests and operational documentation/audit/release note changed. The rebuilt Worker and embedded visitor assets exactly match the build from deployed Sites source e9ba955. No new PDF/HTML/viewport claim is required to certify this diagnostic-only repair." + } + }, + "remaining": { + "machineDataLakeCandidate": "PR 24 remains independently held; this repair does not publish its four new rows or certify its missing affected-report checks", + "ownerVerification": "Existing owner-reported baseline export/narrow checks remain closed in their original scope", + "browserCapabilities": "Current browser still advertises no native PDF or viewport-emulation API", + "schedules": "unchanged", + "activeDelivery": "unchanged; off" + }, + "publication": { + "status": "validated-maintenance-repair", + "qualification": "GitHub integration and Sites synchronization are reported separately after their actual outcomes." + }, + "classifiedLiveDeliveryRetry": { + "evaluatedAt": "2026-10-07T15:36:38.335Z", + "target": "https://versioncompass.com", + "expectedRevision": "content-6dcf491dfeb864140adc2937", + "status": "issues-detected", + "checks": { + "database-health": { + "outcome": "blocked", + "failureType": "transport-unavailable", + "evidence": "The operation was aborted due to timeout", + "checkedAt": "2026-10-07T15:36:16.110Z", + "request": { + "path": "/api/content/health", + "method": "GET", + "phase": "response" + }, + "errorName": "TimeoutError", + "elapsedMs": 10015 + }, + "live-manifest": { + "outcome": "passed", + "evidence": { + "schemaVersion": 1, + "revision": "content-6dcf491dfeb864140adc2937", + "digest": "2ac4b475d0e9d1fa3fbb71cacdf98d617e485a2ac56dfa3251da7ee3e33a444d", + "engineRevision": "engine-c5ff569aec1b2f034270", + "datasetHashes": { + "platform": "64bb4fd3e6862400dfc7148d59cc6c327d6031d6101685daaf9dfd3e07899b10", + "products": "ad329377961c7471bf61993428fcb68b234ab43178cbbc1668924c4829757cea", + "guidance": "8c09fe6020e9f3e31a5b97469408e482232cbbaaa494bd77138e7f84665e273c", + "environment": "9f13ac92dedcb8a3f4d5d0de59926f7568ef67e92239c2d095edcde9cf22a0fb", + "editions": "3d71261084a1b730c55f179589478e8f6299389a3029b2311181b1f98847eb06", + "forwarders": "a33bf7da9957714e2b3e43ef6cadd0da893277ef9e8aed4b7ec9396963e7338a", + "soar": "55c75eb62e2e02f276a31bb84205a6164e51b240f0742479f09eeac82e3f0150" + }, + "recordCount": 668, + "provenance": "Canonical content/datasets JSON in the public VersionCompass repository", + "verificationPolicy": "Migration and delivery do not create verification dates", + "phase": "content-model-and-database-delivery" + }, + "checkedAt": "2026-10-07T15:36:23.154Z", + "elapsedMs": 7042 + }, + "bundle-integrity": { + "outcome": "passed", + "evidence": { + "digest": "2ac4b475d0e9d1fa3fbb71cacdf98d617e485a2ac56dfa3251da7ee3e33a444d", + "recordCount": 668 + }, + "checkedAt": "2026-10-07T15:36:32.918Z", + "elapsedMs": 9763 + }, + "bounded-record-query": { + "outcome": "passed", + "evidence": "SOAR filter, revision and maximum page size verified", + "checkedAt": "2026-10-07T15:36:33.732Z", + "elapsedMs": 814 + }, + "read-only-api": { + "outcome": "passed", + "evidence": "POST rejected with 405", + "checkedAt": "2026-10-07T15:36:37.664Z", + "elapsedMs": 3932 + }, + "bundled-fallback-assets": { + "outcome": "passed", + "evidence": "All seven complete fallback adapters served", + "checkedAt": "2026-10-07T15:36:38.334Z", + "elapsedMs": 669 + } + }, + "findings": [ + { + "observed": "database-health: The operation was aborted due to timeout", + "impact": "Delivery check incomplete; the cause is not established", + "recommendedChange": "Retry this exact check after transport recovery and inspect matching Worker request logs; do not infer an application defect from an unavailable response.", + "reversible": true, + "checksPassed": false, + "resolved": false + } + ], + "limitations": [ + "Browser journeys, actual HTML reopening and native PDF rendering require separate evidence." + ] + } +} diff --git a/docs/browser-verification-recovery.md b/docs/browser-verification-recovery.md new file mode 100644 index 0000000..a178075 --- /dev/null +++ b/docs/browser-verification-recovery.md @@ -0,0 +1,11 @@ +# Browser verification recovery + +Record the failing operation and its actual error before choosing a repair. Browser-service timeouts, page navigation failures, restricted file URLs and absent viewport/PDF capabilities are different conditions. Do not turn any of them into passed report verification or infer a site crash from a missing response. + +1. Check the supported browser inventory once. If it responds, bind a current tab and repeat the exact failed navigation once. Verify the visible page and required interaction. Do not repeatedly reset or retry an unavailable service. +2. For Sites preview, follow the installed Sites skill's supported preview/browser workflow. Do not install a substitute browser, change working site code or relax security settings to compensate for missing managed capabilities. +3. Run the live delivery diagnostic from the source matching the deployed revision. Preserve each result independently. It now reports running/completed checks, request-level unavailability as blocked, and returned verification failures as failed. Neither is a pass. Correlate the method/path and time with Worker logs; expected 400/405 rejections are successful guard checks, not application crashes. +4. Resume only checks the current surface supports. Navigation recovery does not establish native PDF generation, downloaded-HTML rendering, viewport emulation or a changed candidate's report parity. Accept existing owner-reported verification within its recorded scope; do not reopen those baseline blockers or generalize them to future content/runtime changes. +5. Keep a content candidate held until its applicable affected-report checks pass. Preserve the deployed complete revision, privacy, public audience and rollback boundary. A missing browser does not start a mandatory waiting period or justify duplicate timers. Keep established maintenance schedules. + +The October 7 diagnostic recovery is recorded in [the audit](audits/2026-10-07-browser-recovery.json). It identifies observations and remaining unknowns separately; the internal cause of the earlier browser-service timeout is not available in the application logs. diff --git a/docs/releases/2026/10/2026-10-07.md b/docs/releases/2026/10/2026-10-07.md index 577f2fc..9271cff 100644 --- a/docs/releases/2026/10/2026-10-07.md +++ b/docs/releases/2026/10/2026-10-07.md @@ -51,3 +51,9 @@ The October 7 ES Editions review found no supported factual correction. All 20 m This is an operational no-change entry. It does not advance canonical source-review or lifecycle dates, the material content cycle, or any entitlement, compatibility, availability or authorization claim. The live canonical route, legacy preview alias, Cisco theme, selected history, fixed share/export actions, five evidence questions and the surrounding Cloud 10.6 / ES 8.7 report were checked in a rendered browser. The current report's read-only WebMCP output agreed with the visible route and cited managed pairing. Owner-reported HTML reopen, native PDF inspection and narrow-screen results recorded earlier today remain applicable to the unchanged deployed report baseline; no new export artifact or assistant-performed native export is claimed here. The exact source checks, qualifications, validation boundaries and publication baseline are recorded in the [October 7 ES Editions audit](../../../audits/2026-10-07-es-editions-watch.json). + +## Browser and delivery diagnostic recovery + +The cloud browser inventory, navigation to the previously timed-out compliance page and interaction with its US Public Sector tab now succeed. This establishes current navigation recovery, not the internal cause of the prior timeout or a fresh full report/export certification. Public delivery retries verified the immutable 668-record bundle, bounded queries, rejection of POST with 405 and all seven fallback assets; health and manifest calls still timed out on that attempt. + +The maintenance diagnostic now distinguishes unavailable responses/body reads (`blocked`) from returned status, JSON and integrity failures (`failed`), preserves the nonzero certification gate, and emits progress separately from its final JSON. Timeouts recommend an exact retry and Worker-log correlation instead of asserting that a database or write guard needs repair. Regression checks cover successful delivery, response/body timeouts, malformed JSON and accepted versus unavailable public writes. Product claims, factual dates, the deployed dataset/engine identity, interface and schedules are unaffected. The [recovery audit](../../../audits/2026-10-07-browser-recovery.json) records scope and actual validation separately. diff --git a/scripts/diagnose-content.cjs b/scripts/diagnose-content.cjs index db5d905..4b7ab28 100644 --- a/scripts/diagnose-content.cjs +++ b/scripts/diagnose-content.cjs @@ -4,12 +4,30 @@ const expected = JSON.parse(fs.readFileSync('content/publication.json', 'utf8')) const base = new URL(process.argv[2] || 'http://127.0.0.1:4173/'); if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password) throw Error('Use an HTTP(S) site URL without credentials'); const checks = {}, findings = []; +function transportError(error, path, method, phase) { + return Object.assign(new Error(error.message), { failureType: 'transport-unavailable', request: { path, method, phase }, errorName: error.name }); +} async function check(name, action, recommendation) { + const started = Date.now(); + console.error(JSON.stringify({ check: name, outcome: 'running', checkedAt: new Date().toISOString() })); try { checks[name] = { outcome: 'passed', evidence: await action(), checkedAt: new Date().toISOString() }; } - catch (error) { checks[name] = { outcome: 'failed', evidence: String(error.message), checkedAt: new Date().toISOString() }; findings.push({ observed: name + ': ' + error.message, impact: 'Database delivery cannot be certified', recommendedChange: recommendation, reversible: true, checksPassed: false, resolved: false }); } + catch (error) { + const blocked = error.failureType === 'transport-unavailable'; + checks[name] = { outcome: blocked ? 'blocked' : 'failed', failureType: blocked ? error.failureType : 'verification-failed', evidence: String(error.message), checkedAt: new Date().toISOString(), ...(blocked ? { request: error.request, errorName: error.errorName } : {}) }; + findings.push({ observed: name + ': ' + error.message, impact: 'Delivery check incomplete; the cause is not established', recommendedChange: blocked ? 'Retry this exact check after transport recovery and inspect matching Worker request logs; do not infer an application defect from an unavailable response.' : recommendation, reversible: true, checksPassed: false, resolved: false }); + } + checks[name].elapsedMs = Date.now() - started; + console.error(JSON.stringify({ check: name, outcome: checks[name].outcome, elapsedMs: checks[name].elapsedMs })); +} +async function response(path, options) { + try { return await fetch(new URL(path, base), { ...options, signal: AbortSignal.timeout(10000), redirect: 'error' }); } + catch (error) { throw transportError(error, path, options?.method || 'GET', 'response'); } +} +async function body(r, path, format) { + try { return await r[format](); } + catch (error) { if (error instanceof SyntaxError) throw error; throw transportError(error, path, 'GET', 'body'); } } -async function response(path, options) { return fetch(new URL(path, base), { ...options, signal: AbortSignal.timeout(10000), redirect: 'error' }); } -async function json(path) { const r = await response(path); if (!r.ok) throw Error('HTTP ' + r.status + ' at ' + path); return r.json(); } +async function json(path) { const r = await response(path); if (!r.ok) throw Error('HTTP ' + r.status + ' at ' + path); return body(r, path, 'json'); } function assert(condition, message) { if (!condition) throw Error(message); } (async () => { await check('database-health', async () => { const h = await json('/api/content/health'); assert(h.status === 'ready' && h.storage === 'd1' && h.revision === expected.revision && h.recordCount === expected.recordCount, 'Storage, revision or count mismatch'); return h; }, 'Check D1 binding and applied schema; preserve complete bundled fallback while repairing delivery.'); @@ -17,7 +35,7 @@ function assert(condition, message) { if (!condition) throw Error(message); } await check('bundle-integrity', async () => { const b = await json('/api/content/bundle?revision=' + expected.revision); const digest = crypto.createHash('sha256').update(JSON.stringify({ schemaVersion: 1, globals: b.globals, catalog: b.catalog })).digest('hex'); assert(b.manifest.digest === expected.digest && digest === expected.digest && b.catalog.records.length === expected.recordCount, 'Bundle digest or record count mismatch'); return { digest, recordCount: b.catalog.records.length }; }, 'Restore the exact immutable repo seed and investigate interrupted imports or changed payloads.'); await check('bounded-record-query', async () => { const r = await json('/api/content/records?revision=' + expected.revision + '&dataset=soar&limit=2'); assert(r.revision === expected.revision && r.records.length === 2 && r.records.every(x => x.dataset === 'soar'), 'Query filtering mismatch'); assert((await response('/api/content/records?limit=201')).status === 400, 'Unbounded query accepted'); return 'SOAR filter, revision and maximum page size verified'; }, 'Repair query validation and bound parameter handling, then rerun API tests.'); await check('read-only-api', async () => { assert((await response('/api/content/manifest', { method: 'POST' })).status === 405, 'Public write method accepted'); return 'POST rejected with 405'; }, 'Restore the read-only method guard before exposing the content service.'); - await check('bundled-fallback-assets', async () => { for (const file of ['data.js','product-data.js','guidance-data.js','environment-data.js','editions-data.js','forwarders-data.js','soar-data.js']) { const r = await response('/' + file); assert(r.ok && /Generated from content\/datasets\//.test(await r.text()), 'Missing generated adapter ' + file); } return 'All seven complete fallback adapters served'; }, 'Rebuild the seven adapters from canonical datasets and package them with the client.'); + await check('bundled-fallback-assets', async () => { for (const file of ['data.js','product-data.js','guidance-data.js','environment-data.js','editions-data.js','forwarders-data.js','soar-data.js']) { const r = await response('/' + file); assert(r.ok && /Generated from content\/datasets\//.test(await body(r, '/' + file, 'text')), 'Missing generated adapter ' + file); } return 'All seven complete fallback adapters served'; }, 'Rebuild the seven adapters from canonical datasets and package them with the client.'); const failed = Object.values(checks).some(x => x.outcome !== 'passed'); console.log(JSON.stringify({ evaluatedAt: new Date().toISOString(), target: base.origin, expectedRevision: expected.revision, status: failed ? 'issues-detected' : 'delivery-checks-passed', checks, findings, limitations: ['Browser journeys, actual HTML reopening and native PDF rendering require separate evidence.'] }, null, 2)); if (failed) process.exitCode = 2; diff --git a/tests/diagnose-content.test.cjs b/tests/diagnose-content.test.cjs new file mode 100644 index 0000000..d374f7c --- /dev/null +++ b/tests/diagnose-content.test.cjs @@ -0,0 +1,83 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'), os = require('node:os'), path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +function diagnose(t, failure) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vc-delivery-diagnostic-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const fixture = path.join(root, 'transport.mjs'); + fs.writeFileSync(fixture, ` + import fs from 'node:fs'; + import assert from 'node:assert/strict'; + const failure = ${JSON.stringify(failure)}; + const manifest = JSON.parse(fs.readFileSync('dist/content-manifest.json')); + const bundle = JSON.parse(fs.readFileSync('dist/content-bundle.json')); + globalThis.fetch = async (input, options) => { + const url = new URL(input), method = options.method || 'GET'; + assert.equal(url.origin, 'https://versioncompass.com'); + assert.equal(options.redirect, 'error'); + assert(options.signal instanceof AbortSignal); + if (failure && failure.request === method + ' ' + url.pathname) { + if (failure.type === 'timeout') throw new DOMException('Transport timed out', 'TimeoutError'); + if (failure.type === 'body') return {ok:true,text:async()=>{throw new DOMException('Body timed out', 'TimeoutError')}}; + if (failure.type === 'invalid-json') return new Response('{', {status:200}); + if (failure.type === 'status') return Response.json({}, {status:failure.status}); + } + if (method === 'POST') return Response.json({}, {status:405}); + if (url.pathname === '/api/content/health') return Response.json({status:'ready',storage:'d1',revision:manifest.revision,recordCount:manifest.recordCount}); + if (url.pathname === '/api/content/manifest') return Response.json(manifest); + if (url.pathname === '/api/content/bundle') return Response.json(bundle); + if (url.pathname === '/api/content/records') return url.searchParams.get('limit') === '201' ? Response.json({}, {status:400}) : Response.json({revision:manifest.revision,records:bundle.catalog.records.filter(x=>x.dataset==='soar').slice(0,2)}); + return new Response('/* Generated from content/datasets/platform.json */', {status:200}); + }; + `); + const run = spawnSync(process.execPath, ['--import', fixture, 'scripts/diagnose-content.cjs', 'https://versioncompass.com/'], { encoding: 'utf8', timeout: 10000 }); + return { ...run, report: JSON.parse(run.stdout), progress: run.stderr.trim().split('\n').map(line => JSON.parse(line)) }; +} + +test('delivery diagnostic reports success only after every check, with separate progress output', t => { + const result = diagnose(t, null); + assert.equal(result.status, 0); + assert.equal(result.report.status, 'delivery-checks-passed'); + assert.equal(Object.keys(result.report.checks).length, 6); + assert(result.progress.some(x => x.check === 'read-only-api' && x.outcome === 'running')); + assert(result.progress.every(x => x.outcome === 'running' || x.outcome === 'passed')); +}); + +test('response and body timeouts block certification without recommending an application repair', t => { + for (const failure of [{request:'GET /api/content/health',type:'timeout'}, {request:'GET /data.js',type:'body'}]) { + const result = diagnose(t, failure); + assert.equal(result.status, 2); + assert.equal(result.report.status, 'issues-detected'); + const name = failure.type === 'body' ? 'bundled-fallback-assets' : 'database-health'; + const check = result.report.checks[name]; + assert.equal(check.outcome, 'blocked'); + assert.equal(check.failureType, 'transport-unavailable'); + assert.equal(check.errorName, 'TimeoutError'); + assert.equal(check.request.phase, failure.type === 'body' ? 'body' : 'response'); + assert.match(result.report.findings[0].recommendedChange, /Retry this exact check/); + assert.equal(result.report.findings[0].checksPassed, false); + assert.equal(result.report.checks['bounded-record-query'].outcome, 'passed'); + } +}); + +test('malformed returned JSON is a failed verification rather than transport unavailability', t => { + const result = diagnose(t, {request:'GET /api/content/manifest',type:'invalid-json'}); + assert.equal(result.status, 2); + assert.equal(result.report.checks['live-manifest'].outcome, 'failed'); + assert.equal(result.report.checks['live-manifest'].failureType, 'verification-failed'); +}); + +test('public write acceptance fails; unavailable method-guard responses remain blocked', t => { + const accepted = diagnose(t, {request:'POST /api/content/manifest',type:'status',status:200}); + assert.equal(accepted.status, 2); + assert.equal(accepted.report.checks['read-only-api'].outcome, 'failed'); + assert.match(accepted.report.findings[0].recommendedChange, /Restore the read-only method guard/); + const unavailable = diagnose(t, {request:'POST /api/content/manifest',type:'timeout'}); + assert.equal(unavailable.status, 2); + assert.equal(unavailable.report.checks['read-only-api'].outcome, 'blocked'); + assert.equal(unavailable.report.checks['read-only-api'].request.method, 'POST'); + assert.equal(unavailable.report.checks['read-only-api'].request.path, '/api/content/manifest'); + assert.doesNotMatch(unavailable.report.findings[0].recommendedChange, /Restore/); +});