diff --git a/.github/workflows/deploy-strategy-switch-console.yml b/.github/workflows/deploy-strategy-switch-console.yml index ee04b129..79082d8e 100644 --- a/.github/workflows/deploy-strategy-switch-console.yml +++ b/.github/workflows/deploy-strategy-switch-console.yml @@ -261,6 +261,13 @@ jobs: const list = Array.isArray(bindings) && bindings.length > 0 && bindings.length <= 128 ? bindings : null; const all = check => !!list && list.every(check); const named = script?.named_handlers; + const namedList = Array.isArray(named) && named.length <= 128 ? named : null; + const namedCategories = []; + if (namedList) for (const item of namedList) { + const fields = shape(item), signature = JSON.stringify(fields); + const found = namedCategories.find(entry => entry.signature === signature); + if (found) found.count++; else namedCategories.push({ signature, shape: fields, count: 1 }); + } const contracts = { resources_keys: only(resources, ['bindings','script','script_runtime']), runtime_keys: only(runtime, ['compatibility_date','compatibility_flags','usage_model','limits','migration_tag','exports']), @@ -273,6 +280,12 @@ jobs: compatibility_flags: runtime?.compatibility_flags === undefined || (Array.isArray(runtime.compatibility_flags) && runtime.compatibility_flags.every(x => typeof x === 'string')), usage_model: ['standard','bundled','unbound'].includes(runtime?.usage_model), named_handlers: named === undefined || (Array.isArray(named) && named.length <= 128 && named.every(item => only(item,['name','handlers']) && identifier(item.name) && (item.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.length <= 32 && item.handlers.every(identifier)))) && new Set(named.map(item => item.name)).size === named.length), + named_list_bounds: named === undefined || !!namedList, + named_item_keys: named === undefined || (!!namedList && namedList.every(item => only(item,['name','handlers']))), + named_name_format: named === undefined || (!!namedList && namedList.every(item => identifier(item?.name))), + named_names_unique: named === undefined || (!!namedList && new Set(namedList.map(item => item?.name)).size === namedList.length), + named_handler_list_bounds: named === undefined || (!!namedList && namedList.every(item => item?.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.length <= 32))), + named_handler_identifiers: named === undefined || (!!namedList && namedList.every(item => item?.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.every(identifier)))), binding_names: all(b => object(b) && bindingName(b.name)), binding_names_unique: !!list && new Set(list.map(b => b?.name)).size === list.length, binding_types: all(b => ['plain_text','json','secret_text','kv_namespace','durable_object_namespace'].includes(b?.type)), @@ -289,6 +302,7 @@ jobs: console.log(JSON.stringify({ status: 'frontend_schema_categories', version_id: '2beea000-cce5-43a5-9255-9724f7318e83', result_id_matches: body.result?.id === '2beea000-cce5-43a5-9255-9724f7318e83', result: shape(body.result), resources: shape(resources), binding_container: kind(bindings), binding_count: Array.isArray(bindings) ? bindings.length : null, binding_categories: categories.map(({ signature, ...item }) => item), script: shape(resources?.script), runtime: shape(resources?.script_runtime), contracts, + named_handler_categories: namedCategories.map(({ signature, ...item }) => item), writes: 0, retry: false })); } catch { console.error(JSON.stringify({ error: 'frontend_schema_readback_failed', http_status: httpStatus, writes: 0, retry: false })); process.exitCode = 1; diff --git a/tests/console_deploy_safety_validation.mjs b/tests/console_deploy_safety_validation.mjs index 8d2d0e67..b4f5e676 100644 --- a/tests/console_deploy_safety_validation.mjs +++ b/tests/console_deploy_safety_validation.mjs @@ -139,6 +139,18 @@ for(const [mutate,flag]of diagnosticCases){ assert.ok(Object.values(result.contracts).every(x=>typeof x==="boolean")); if(flag==="date_ymd")assert.equal(result.contracts.date_midnight_utc,true); } +for(const [item,flag,fieldType]of [ + [{name:"PRIVATE_SCHEMA_SENTINEL",handlers:null},"named_handler_list_bounds","null"], + [{name:"PRIVATE_SCHEMA_SENTINEL",handlers:[123]},"named_handler_identifiers","array"], + [{name:null,handlers:[]},"named_name_format","array"], + [{name:"PRIVATE_SCHEMA_SENTINEL",handlers:[],PRIVATE_SCHEMA_SENTINEL:"PRIVATE_SCHEMA_SENTINEL"},"named_item_keys","array"], +]){ + const changed=structuredClone(validSchema);changed.result.resources.script.named_handlers=[item]; + const result=await schemaCase(changed);assert.equal(result.contracts[flag],false,flag); + assert.equal(result.named_handler_categories[0].shape.fields.handlers,fieldType); + assert.equal(result.named_handler_categories[0].count,1); + assert.ok(!JSON.stringify(result).includes("PRIVATE_SCHEMA_SENTINEL")); +} // Execute the exact selection shell with a fake Git boundary, never checkout the // test repository or evaluate any selected Worker. Real object identities are