From 9a389205531c10d89add7bead7ccbf7fdb522b98 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:01:37 +0800 Subject: [PATCH 1/2] fix: Allow additive test handlers in reviewed main release --- .../deploy-strategy-switch-console.yml | 33 ++++++++++++++++--- scripts/console_version_comparator.mjs | 15 ++++++--- tests/console_deploy_safety_validation.mjs | 16 ++++++++- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-strategy-switch-console.yml b/.github/workflows/deploy-strategy-switch-console.yml index e8d12650..26f34006 100644 --- a/.github/workflows/deploy-strategy-switch-console.yml +++ b/.github/workflows/deploy-strategy-switch-console.yml @@ -1917,10 +1917,27 @@ jobs: } if (uploaded === prior || (separateWitness && uploaded === previousLatest)) fail(); stage = 'uploaded_configuration'; const next = resources(await request(`/versions/${uploaded}`), uploaded); - const comparable = value => ({ bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)), - script_runtime: value.script_runtime, handlers: value.script.handlers, - named_handlers: value.script.named_handlers === undefined ? null : [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name)), - placement_mode: value.script.placement_mode ?? null }); + // A reviewed main release may add test-only handlers behind the __test + // export. Treat that export as code, not configuration: every other named + // handler stays strictly equal, and test handlers may only be added. + const allowTestHandlerAdditions = reviewedMain; + const namedHandlers = value => value.script.named_handlers === undefined ? null + : [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name)); + const testExport = value => { + const named = namedHandlers(value); + if (!Array.isArray(named)) return { present: false, handlers: [], ambiguous: false }; + const entries = named.filter(item => item.name === '__test'); + const handlers = entries.length === 1 && Array.isArray(entries[0].handlers) ? [...entries[0].handlers].sort() : []; + return { present: entries.length > 0, handlers, + ambiguous: entries.length > 1 || (entries.length === 1 && entries[0].handlers !== undefined && !Array.isArray(entries[0].handlers)) }; + }; + const comparable = value => { + const named = namedHandlers(value); + return { bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)), + script_runtime: value.script_runtime, handlers: value.script.handlers, + named_handlers: allowTestHandlerAdditions && Array.isArray(named) ? named.filter(item => item.name !== '__test') : named, + placement_mode: value.script.placement_mode ?? null }; + }; const configHash = value => createHash('sha256').update(canonical(comparable(value))).digest('hex'); const desired = structuredClone(old); if (flowMode) { @@ -1932,6 +1949,14 @@ jobs: const oldTag = old.script_runtime.migration_tag, newTag = next.script_runtime.migration_tag; if (typeof oldTag !== 'string' || !oldTag || (newTag !== undefined && newTag !== oldTag)) fail('migration_tag_mismatch'); const normalized = structuredClone(next); normalized.script_runtime.migration_tag = oldTag; + if (reviewedMain) { + const before = testExport(desired), after = testExport(normalized); + if (before.ambiguous || after.ambiguous || before.present !== after.present) fail('test_export_shape_mismatch'); + const retained = new Set(after.handlers), prior = new Set(before.handlers); + if (before.handlers.some(name => !retained.has(name))) fail('test_export_handlers_removed'); + const added = after.handlers.filter(name => !prior.has(name)); + if (added.length) console.log(JSON.stringify({ status: 'reviewed_main_test_export_handlers_added', added })); + } if (configHash(normalized) !== configBefore || (!flowMode && next.script.etag === old.script.etag)) fail('configuration_hash_mismatch'); } else if (configUploaded !== configBefore || next.script.etag === old.script.etag) fail('configuration_hash_mismatch'); if (pr764 || betaRelease) { diff --git a/scripts/console_version_comparator.mjs b/scripts/console_version_comparator.mjs index c1dc6462..395b1953 100644 --- a/scripts/console_version_comparator.mjs +++ b/scripts/console_version_comparator.mjs @@ -58,10 +58,17 @@ const canonical = value => JSON.stringify(value, function (_key, item) { || !value.bindings.some(b => b.name === 'STRATEGY_SWITCH_RUNTIME_INSTANCES' && b.type === 'durable_object_namespace')) fail(); return value; } - const comparable = value => ({ bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)), - script_runtime: value.script_runtime, handlers: value.script.handlers, - named_handlers: value.script.named_handlers === undefined ? null : [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name)), - placement_mode: value.script.placement_mode ?? null }); + // The comparator stays strict: test-only handler changes remain visible as a diff. + const allowTestHandlerAdditions = false; + const namedHandlers = value => value.script.named_handlers === undefined ? null + : [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name)); + const comparable = value => { + const named = namedHandlers(value); + return { bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)), + script_runtime: value.script_runtime, handlers: value.script.handlers, + named_handlers: allowTestHandlerAdditions && Array.isArray(named) ? named.filter(item => item.name !== '__test') : named, + placement_mode: value.script.placement_mode ?? null }; + }; const configHash = value => createHash('sha256').update(canonical(comparable(value))).digest('hex'); const types = ['plain_text', 'json', 'secret_text', 'kv_namespace', 'durable_object_namespace']; diff --git a/tests/console_deploy_safety_validation.mjs b/tests/console_deploy_safety_validation.mjs index 88294834..7f3661b1 100644 --- a/tests/console_deploy_safety_validation.mjs +++ b/tests/console_deploy_safety_validation.mjs @@ -755,7 +755,7 @@ async function releaseCase({pr764=false,ibkrDaily=false,activeOverride=null,inhe releaseCases++;return {calls,stdout,stderr}; } // Exact integrated candidate: explicit inheritance, verified module, one non-forced activation. -async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c1b737d6e721eb9fe71a12c613fbb49d3fa51563", lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false, baselineItems=undefined, preUploadVersion=null, preDeployWitness=null, uploadId=null, ambiguousActive=false, expectFailure=null } = {}, expected=true) { +async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c1b737d6e721eb9fe71a12c613fbb49d3fa51563", lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false, baselineItems=undefined, preUploadVersion=null, preDeployWitness=null, uploadId=null, ambiguousActive=false, expectFailure=null, testHandlers=null } = {}, expected=true) { const calls=[], stdout=[], stderr=[], prior=settings?"7ffe9227-686a-48ad-baa9-72665969d918":daily?"14f0c9c2-09f9-40e9-bfbe-17165f499495":statusCandidate?"175ba768-484a-4fe6-9cea-e5a101bc1618":cpu?"90a0c8e9-b5ad-447a-998f-5d585ec28063":"600ec84d-a804-4ede-8d1d-0a34819ce46a"; active ??= prior; latest ??= active; const separateWitness = reviewedMain && reviewedMainSource === inboxSource && !flow; @@ -772,10 +772,16 @@ async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c for (const i of [7, 10]) responses[i].result.resources.bindings.push({name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"true"}); if (drift) responses[7].result.resources.bindings[0].namespace_id="e".repeat(32); } + if (testHandlers) { + if (testHandlers.active) responses[1].result.resources.script.named_handlers.push({name:"__test",handlers:[...testHandlers.active]}); + if (testHandlers.uploaded) responses[7].result.resources.script.named_handlers.push({name:"__test",handlers:[...testHandlers.uploaded]}); + if (testHandlers.runtimeUploaded) responses[7].result.resources.script.named_handlers.find(item=>item.name==="RuntimeInstances").handlers=[...testHandlers.runtimeUploaded]; + } const uploadedResources=responses[7].result.resources; if (uploadedTag === undefined) delete uploadedResources.script_runtime.migration_tag; else uploadedResources.script_runtime.migration_tag=uploadedTag; const postResources=resource("new-code"); + if (testHandlers?.post) postResources.script.named_handlers.push({name:"__test",handlers:[...testHandlers.post]}); if (flow) postResources.bindings.push({name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"true"}); if (postTag === null) delete postResources.script_runtime.migration_tag; else postResources.script_runtime.migration_tag=postTag; @@ -804,6 +810,7 @@ async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c } else {if (postTag !== null) assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,0); else { assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,1); assert.equal(JSON.parse(stderr.at(-1)).outcome,"UNKNOWN_REQUIRES_READBACK"); } assert.equal(JSON.parse(stderr.at(-1)).retry,false); if (expectFailure) { const error=JSON.parse(stderr.at(-1)); for (const [key,value] of Object.entries(expectFailure)) assert.equal(error[key],value,key); }} assert.ok(!JSON.stringify({stdout,stderr}).includes(privateRelease)); + return {stdout,stderr}; } // Execute the actual publisher for the exact merged source, with fake provider responses. await integratedReleaseCase({reviewedMain:true}); @@ -812,6 +819,13 @@ await integratedReleaseCase({reviewedMain:true,lineage:priorVersion},false); await integratedReleaseCase({reviewedMain:true,moduleContent:Buffer.from("wrong").toString("base64")},false); await integratedReleaseCase({reviewedMain:true,uploadedTag:"wrong-tag"},false); await integratedReleaseCase({reviewedMain:true,postTag:null},false); +const configStop={retry:false,deploy_attempted:false,stage:"uploaded_configuration",upload_attempted:true,outcome:"UPLOAD_REQUIRES_READBACK_NOT_ACTIVATED"}; +const addedTest=await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha","beta"]}}); +assert.ok(addedTest.stdout.some(line=>JSON.parse(line).status==="reviewed_main_test_export_handlers_added"),"additive __test handlers are recorded"); +await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha","beta"],uploaded:["alpha"],post:["alpha"]},expectFailure:{...configStop,checkpoint:"test_export_handlers_removed"}},false); +await integratedReleaseCase({reviewedMain:true,testHandlers:{uploaded:["alpha"],post:["alpha"]},expectFailure:{...configStop,checkpoint:"test_export_shape_mismatch"}},false); +await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha"],uploaded:["alpha"],post:["alpha"],runtimeUploaded:["other"]},expectFailure:{...configStop,checkpoint:"configuration_hash_mismatch"}},false); +await integratedReleaseCase({testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha","beta"]},expectFailure:{...configStop,checkpoint:"configuration_hash_mismatch"}},false); const orphanWitness="00000000-0000-4000-8000-000000000021"; const driftedWitness="00000000-0000-4000-8000-000000000022"; const driftedActive="00000000-0000-4000-8000-000000000023"; From ad7c97ba0f61b0f8996d5c23eceebbf2340b8a05 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:40:34 +0800 Subject: [PATCH 2/2] fix: Verify test export exactly in post-release readback --- .github/workflows/deploy-strategy-switch-console.yml | 11 ++++++++++- tests/console_deploy_safety_validation.mjs | 7 +++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-strategy-switch-console.yml b/.github/workflows/deploy-strategy-switch-console.yml index 26f34006..a146e601 100644 --- a/.github/workflows/deploy-strategy-switch-console.yml +++ b/.github/workflows/deploy-strategy-switch-console.yml @@ -2008,8 +2008,17 @@ jobs: stage = 'readback'; await active(deploymentId, uploaded); if (pr764 || integrated || reviewedMain || resume !== 'off') { stage = 'post_release_configuration'; - const configAfter = configHash(resources(await request(`/versions/${uploaded}`), uploaded)); + const readback = resources(await request(`/versions/${uploaded}`), uploaded); + const configAfter = configHash(readback); if (configAfter !== configBefore) fail('post_release_configuration_hash_mismatch'); + // The reviewed-main hash excludes __test, so readback must still + // prove the exact validated test export survived activation. + if (reviewedMain) { + const verifiedTest = testExport(next), readbackTest = testExport(readback); + if (readbackTest.ambiguous || verifiedTest.present !== readbackTest.present + || verifiedTest.handlers.length !== readbackTest.handlers.length + || verifiedTest.handlers.some((name, index) => name !== readbackTest.handlers[index])) fail('post_release_test_export_mismatch'); + } // Version metadata hides secret values. Hash all comparable bindings/runtime/ // handlers/placement, never code etag or provider bookkeeping. No KV writes. if (controlRuntimeCandidate) console.log(JSON.stringify({ status: 'integrated_configuration_verified', bindings_equal: true, runtime_equal: true, secret_values_read: false })); diff --git a/tests/console_deploy_safety_validation.mjs b/tests/console_deploy_safety_validation.mjs index 7f3661b1..2b772abf 100644 --- a/tests/console_deploy_safety_validation.mjs +++ b/tests/console_deploy_safety_validation.mjs @@ -755,7 +755,7 @@ async function releaseCase({pr764=false,ibkrDaily=false,activeOverride=null,inhe releaseCases++;return {calls,stdout,stderr}; } // Exact integrated candidate: explicit inheritance, verified module, one non-forced activation. -async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c1b737d6e721eb9fe71a12c613fbb49d3fa51563", lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false, baselineItems=undefined, preUploadVersion=null, preDeployWitness=null, uploadId=null, ambiguousActive=false, expectFailure=null, testHandlers=null } = {}, expected=true) { +async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c1b737d6e721eb9fe71a12c613fbb49d3fa51563", lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false, baselineItems=undefined, preUploadVersion=null, preDeployWitness=null, uploadId=null, ambiguousActive=false, expectFailure=null, testHandlers=null, deployed=false } = {}, expected=true) { const calls=[], stdout=[], stderr=[], prior=settings?"7ffe9227-686a-48ad-baa9-72665969d918":daily?"14f0c9c2-09f9-40e9-bfbe-17165f499495":statusCandidate?"175ba768-484a-4fe6-9cea-e5a101bc1618":cpu?"90a0c8e9-b5ad-447a-998f-5d585ec28063":"600ec84d-a804-4ede-8d1d-0a34819ce46a"; active ??= prior; latest ??= active; const separateWitness = reviewedMain && reviewedMainSource === inboxSource && !flow; @@ -807,7 +807,7 @@ async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c assert.equal(JSON.parse(stdout.at(-1)).prior_version,prior); } if (flow) assert.equal(JSON.parse(stdout.at(-1)).external_flow_enabled,true); else assert.equal(JSON.parse(stdout.at(-1)).version_id,newVersion); - } else {if (postTag !== null) assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,0); else { assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,1); assert.equal(JSON.parse(stderr.at(-1)).outcome,"UNKNOWN_REQUIRES_READBACK"); } assert.equal(JSON.parse(stderr.at(-1)).retry,false); + } else {if (postTag !== null && !deployed) assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,0); else { assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,1); assert.equal(JSON.parse(stderr.at(-1)).outcome,"UNKNOWN_REQUIRES_READBACK"); } assert.equal(JSON.parse(stderr.at(-1)).retry,false); if (expectFailure) { const error=JSON.parse(stderr.at(-1)); for (const [key,value] of Object.entries(expectFailure)) assert.equal(error[key],value,key); }} assert.ok(!JSON.stringify({stdout,stderr}).includes(privateRelease)); return {stdout,stderr}; @@ -826,6 +826,9 @@ await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha","be await integratedReleaseCase({reviewedMain:true,testHandlers:{uploaded:["alpha"],post:["alpha"]},expectFailure:{...configStop,checkpoint:"test_export_shape_mismatch"}},false); await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha"],uploaded:["alpha"],post:["alpha"],runtimeUploaded:["other"]},expectFailure:{...configStop,checkpoint:"configuration_hash_mismatch"}},false); await integratedReleaseCase({testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha","beta"]},expectFailure:{...configStop,checkpoint:"configuration_hash_mismatch"}},false); +const postTestStop={retry:false,deploy_attempted:true,stage:"post_release_configuration",upload_attempted:true,outcome:"UNKNOWN_REQUIRES_READBACK",checkpoint:"post_release_test_export_mismatch"}; +await integratedReleaseCase({reviewedMain:true,deployed:true,testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha"]},expectFailure:postTestStop},false); +await integratedReleaseCase({reviewedMain:true,deployed:true,testHandlers:{active:["alpha"],uploaded:["alpha","beta"]},expectFailure:postTestStop},false); const orphanWitness="00000000-0000-4000-8000-000000000021"; const driftedWitness="00000000-0000-4000-8000-000000000022"; const driftedActive="00000000-0000-4000-8000-000000000023";