Files
jiapuapp/tests/g12-generation-poem-openapi-adversarial-contract.ps1
T
2026-07-23 17:21:33 +08:00

1014 lines
50 KiB
PowerShell

$ErrorActionPreference = 'Stop'
$gatePath = Join-Path $PSScriptRoot 'g12-generation-poem-openapi-contract.ps1'
function New-SeedDocument {
return New-CompletePassingDocument
}
function Copy-Document([object]$Value) {
return $Value | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json
}
function Set-Property([object]$Object, [string]$Name, [object]$Value) {
$Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value -Force
}
function Rename-Property([object]$Object, [string]$OldName, [string]$NewName) {
$property = @($Object.PSObject.Properties | Where-Object { $_.Name -ceq $OldName })
if ($property.Count -ne 1) { throw "cannot rename missing exact property: $OldName" }
$value = $property[0].Value
$Object.PSObject.Properties.Remove($OldName)
Set-Property $Object $NewName $value
}
function Get-Issues([object]$Document) {
return @(& $gatePath -InputDocument $Document -SkipParity -ReturnIssues)
}
function Assert-MutationRejected {
param(
[string]$Name,
[scriptblock]$Setup,
[scriptblock]$Mutate,
[string]$Pattern
)
$baselineDocument = New-SeedDocument
& $Setup $baselineDocument
$before = @(Get-Issues $baselineDocument)
if ($before.Count -ne 0) {
throw "$Name setup must have zero issues: $($before -join ' | ')"
}
$mutant = Copy-Document $baselineDocument
& $Mutate $mutant
$after = @(Get-Issues $mutant)
$newIssues = @($after | Where-Object { $_ -notin $before })
if ($after.Count -le $before.Count -or -not ($newIssues | Where-Object { $_ -match $Pattern })) {
throw "$Name fake-green. New issues: $($newIssues -join ' | ')"
}
}
function Assert-LocalBaselineAccepted {
param(
[string]$Name,
[scriptblock]$Setup,
[string]$ForbiddenPattern
)
$document = New-SeedDocument
& $Setup $document
$issues = @(Get-Issues $document)
if ($issues.Count -ne 0 -or $issues | Where-Object { $_ -match $ForbiddenPattern }) {
throw "$Name false-positive: $($issues -join ' | ')"
}
}
function New-SecurityRequirement {
return @([pscustomobject]@{ SaToken = @() })
}
function New-MinimalOperation([string]$OperationId, [string]$Description) {
return [pscustomobject]@{
description = $Description
operationId = $OperationId
parameters = @()
responses = [pscustomobject]@{}
security = @((New-SecurityRequirement))
}
}
function Add-ShadowMutation([object]$Document, [string]$Path, [object]$Schema) {
$operation = New-MinimalOperation 'shadowMutation' 'unrelated shadow mutation'
Set-Property $operation 'requestBody' ([pscustomobject]@{
required = $true
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{ schema = $Schema }
}
})
Set-Property $Document.paths $Path ([pscustomobject]@{ put = $operation })
}
function Add-TargetGetWithValidExtensions([object]$Document) {
$operation = New-MinimalOperation 'appGetGenerationPoemSet' 'canView ACTIVE-only generationNo ascending non-disclosing 404'
Set-Property $operation 'x-projection' 'ACTIVE_ONLY'
Set-Property $operation 'x-sort-order' @('generationNo:asc')
Set-Property $operation 'x-cors-required-request-headers' @('Authorization', 'clientid')
Set-Property $operation 'x-cors-policy-owner' 'APP_GATEWAY_PREFLIGHT'
$pathName = '/genealogy/app/genealogies/{genealogyId}/generation-poems'
$pathProperty = $Document.paths.PSObject.Properties[$pathName]
if ($pathProperty) { Set-Property $pathProperty.Value 'get' $operation } else { Set-Property $Document.paths $pathName ([pscustomobject]@{ get = $operation }) }
}
function Add-TargetPutWithValidExtensions([object]$Document) {
$operation = New-MinimalOperation 'appUpdateGenerationPoemSet' 'canEditContent READY transaction If-Match poemSetVersion full candidate before write disableMissing=false preserve unrepresented ACTIVE disableMissing=true soft-disable unrepresented ACTIVE never physical delete swap no observable intermediate semantic no-op keep version unknown fresh GET target old current current target not prove request no automatic PUT'
Set-Property $operation 'requestBody' ([pscustomobject]@{ required = $true; content = [pscustomobject]@{} })
$values = [ordered]@{
'x-cas-owner' = 'GenerationPoemSetVersion'
'x-revalidates' = @('tenantScope', 'canEditContent', 'poemSetVersion', 'genealogyState')
'x-candidate-steps' = @('RESOLVE_BASELINE_ACTIVE_IDS', 'VALIDATE_DECLARED_STRICT_ORDER', 'BUILD_DECLARED_TARGET', 'APPLY_DISABLE_MISSING_POLICY', 'ORDER_MERGED_CANDIDATE_BY_GENERATION_NO', 'VALIDATE_ID_AND_GENERATION_UNIQUENESS', 'VALIDATE_GENERATION_SLOT_COLLISIONS', 'VALIDATE_CONTIGUITY', 'VALIDATE_FINAL_ACTIVE_CAPACITY', 'ALLOCATE_UNIQUE_NEW_IDS', 'VALIDATE_ALLOCATED_IDS_NONEMPTY_UNIQUE_NOT_IN_BASELINE', 'WRITE_ATOMICALLY')
'x-generation-slot-collision-policy' = 'REJECT_422_ZERO_DOMAIN_WRITE'
'x-swap-declaration-policy' = 'ALL_AFFECTED_ROWS_REQUIRED'
'x-swap-write-policy' = 'ATOMIC_NO_OBSERVABLE_INTERMEDIATE_STATE'
'x-transaction-failure-policy' = 'ZERO_DOMAIN_WRITE'
'x-transaction-effects' = @('APPLY_VALIDATED_CANDIDATE_ATOMICALLY', 'APPLY_DISABLE_MISSING_POLICY', 'UPDATE_VERSION_ON_SEMANTIC_CHANGE')
'x-semantic-noop' = 'RETURN_200_KEEP_VERSION'
'x-version-reuse' = 'FORBIDDEN'
'x-outcome-unknown-reconciliation' = 'FRESH_GET_THREE_WAY_NO_AUTO_PUT'
'x-unknown-precedence' = 'CURRENT_EQUALS_TARGET_THEN_CURRENT_EQUALS_OLD_THEN_DIVERGED'
'x-unknown-overlap-policy' = 'CURRENT_EQUALS_TARGET_FIRST_NO_ATTRIBUTION'
'x-unknown-existing-match-fields' = @('poemId', 'generationNo', 'generationText')
'x-unknown-new-match-fields' = @('generationNo', 'generationText')
'x-unknown-new-id-policy' = 'NON_EMPTY_UNIQUE_NOT_IN_BASELINE'
'x-unknown-set-equality' = 'SAME_CARDINALITY_NO_EXTRA_ACTIVE_ROWS'
'x-unknown-version-comparison' = 'IGNORE_FOR_SEMANTIC_EQUALITY'
'x-unknown-version-policy' = 'CHANGED_TARGET_REQUIRES_FRESH_NONREUSED_VERSION'
'x-unknown-target-construction-fields' = @('genealogyId', 'baselinePoemSetVersion', 'baselineOrderedItems', 'declaredItems', 'disableMissing')
'x-unknown-semantic-fields' = @('genealogyId', 'effectiveTargetOrderedItems')
'x-cors-required-request-headers' = @('Authorization', 'Content-Type', 'If-Match', 'clientid')
'x-cors-policy-owner' = 'APP_GATEWAY_PREFLIGHT'
}
foreach ($entry in $values.GetEnumerator()) { Set-Property $operation $entry.Key $entry.Value }
$pathName = '/genealogy/app/genealogies/{genealogyId}/generation-poems'
$pathProperty = $Document.paths.PSObject.Properties[$pathName]
if ($pathProperty) { Set-Property $pathProperty.Value 'put' $operation } else { Set-Property $Document.paths $pathName ([pscustomobject]@{ put = $operation }) }
}
function New-Ref([string]$Ref) {
return [pscustomobject]@{ '$ref' = $Ref }
}
function New-FixedErrorSchema([int]$Status, [string]$BusinessCode) {
return [pscustomobject]@{
type = 'object'
nullable = $false
additionalProperties = $false
required = @('code', 'businessCode', 'message')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @($Status); nullable = $false }
businessCode = [pscustomobject]@{ type = 'string'; enum = @($BusinessCode); nullable = $false }
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
}
}
}
function New-ApiResponse([string]$SchemaRef, [bool]$RetryAfter) {
$headers = [pscustomobject]@{
'Cache-Control' = (New-Ref '#/components/headers/PrivateNoStore')
}
if ($RetryAfter) { Set-Property $headers 'Retry-After' (New-Ref '#/components/headers/RetryAfter') }
return [pscustomobject]@{
description = 'typed response'
headers = $headers
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{ schema = (New-Ref $SchemaRef) }
}
}
}
function New-ClientIdParameter {
return [pscustomobject]@{
name = 'clientid'
in = 'header'
required = $true
description = 'client instance'
schema = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; nullable = $false }
}
}
function New-GenealogyIdParameter {
return [pscustomobject]@{
name = 'genealogyId'
in = 'path'
required = $true
description = 'genealogy identity'
schema = (New-Ref '#/components/schemas/GenealogyId')
}
}
function New-IfMatchParameter {
return [pscustomobject]@{
name = 'If-Match'
in = 'header'
required = $true
description = 'poem set version'
schema = (New-Ref '#/components/schemas/GenerationPoemSetVersion')
}
}
function New-CompletePassingDocument {
$identifierPattern = '^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$'
$textPattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$'
$fieldPathPattern = '^(items|items\[(?:[0-9]|[1-9][0-9]|[1-4][0-9]{2})\]\.(poemId|generationNo|generationText)|disableMissing)$'
$schemas = [pscustomobject]@{
GenealogyId = [pscustomobject]@{
type = 'string'; minLength = 1; maxLength = 128; pattern = $identifierPattern; nullable = $false
}
GenerationPoemId = [pscustomobject]@{
type = 'string'; minLength = 1; maxLength = 128; pattern = $identifierPattern; nullable = $false
'x-opaque' = $true
'x-client-semantics' = 'COMPARE_ONLY'
}
GenerationPoemSetVersion = [pscustomobject]@{
type = 'string'; minLength = 1; maxLength = 128; pattern = $identifierPattern; nullable = $false
'x-opaque' = $true
'x-version-scope-fields' = @('poemId', 'generationNo', 'generationText', 'activeState')
'x-version-change-policy' = 'ACTIVE_SEMANTIC_CHANGE_ONLY'
'x-version-reuse' = 'FORBIDDEN'
}
GenerationPoemGenerationNo = [pscustomobject]@{
type = 'integer'; format = 'int32'; minimum = 1; maximum = 2147483647; nullable = $false
}
GenerationPoemText = [pscustomobject]@{
type = 'string'; minLength = 1; maxLength = 50; pattern = $textPattern; nullable = $false
'x-normalization' = 'NFC'
'x-length-unit' = 'UNICODE_CODE_POINT'
'x-well-formed-unicode' = $true
'x-boundary-whitespace' = 'REJECT'
'x-forbidden-code-point-classes' = @('C0', 'C1', 'CR', 'LF', 'TAB', 'ARABIC_LETTER_MARK', 'LTR_RTL_MARK', 'BIDI_OVERRIDE_OR_ISOLATE', 'WORD_JOINER', 'ZERO_WIDTH_OR_BOM', 'LINE_PARAGRAPH_SEPARATOR', 'UNPAIRED_SURROGATE')
}
AppGenerationPoemSetItem = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('generationNo', 'generationText')
properties = [pscustomobject]@{
poemId = (New-Ref '#/components/schemas/GenerationPoemId')
generationNo = (New-Ref '#/components/schemas/GenerationPoemGenerationNo')
generationText = (New-Ref '#/components/schemas/GenerationPoemText')
}
'x-existing-id-policy' = 'CURRENT_BASELINE_ACTIVE_ONLY'
'x-new-id-policy' = 'OMIT_POEM_ID'
'x-id-move-policy' = 'ALLOW_EXPLICIT_TARGET_GENERATION'
'x-disabled-id-policy' = 'ROW_NOT_AVAILABLE'
}
GenerationPoemSetItem = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('poemId', 'generationNo', 'generationText')
properties = [pscustomobject]@{
poemId = (New-Ref '#/components/schemas/GenerationPoemId')
generationNo = (New-Ref '#/components/schemas/GenerationPoemGenerationNo')
generationText = (New-Ref '#/components/schemas/GenerationPoemText')
}
}
AppGenerationPoemSetUpdateBody = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('items', 'disableMissing')
properties = [pscustomobject]@{
items = [pscustomobject]@{
type = 'array'; minItems = 0; maxItems = 500; nullable = $false
items = (New-Ref '#/components/schemas/AppGenerationPoemSetItem')
}
disableMissing = [pscustomobject]@{ type = 'boolean'; nullable = $false }
}
'x-request-order' = 'STRICT_GENERATION_NO_ASC_NO_SERVER_SORT'
'x-false-policy' = 'PRESERVE_ALL_UNREPRESENTED_BASELINE_ACTIVE'
'x-true-policy' = 'SOFT_DISABLE_ALL_UNREPRESENTED_BASELINE_ACTIVE'
'x-empty-false-policy' = 'SEMANTIC_NOOP'
'x-empty-true-policy' = 'SOFT_DISABLE_ALL_ACTIVE'
'x-final-active-invariant' = 'EMPTY_OR_CONTIGUOUS_INTERVAL'
'x-candidate-validation' = 'BEFORE_ANY_DOMAIN_WRITE'
'x-duplicate-generation-text' = 'ALLOWED'
}
GenerationPoemSetSnapshot = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('genealogyId', 'poemSetVersion', 'items')
properties = [pscustomobject]@{
genealogyId = (New-Ref '#/components/schemas/GenealogyId')
poemSetVersion = (New-Ref '#/components/schemas/GenerationPoemSetVersion')
items = [pscustomobject]@{
type = 'array'; minItems = 0; maxItems = 500; nullable = $false
items = (New-Ref '#/components/schemas/GenerationPoemSetItem')
}
}
'x-projection' = 'ACTIVE_ONLY'
'x-order' = 'STRICT_GENERATION_NO_ASC'
'x-active-invariant' = 'EMPTY_OR_CONTIGUOUS_INTERVAL'
'x-identity-uniqueness' = 'POEM_ID_AND_GENERATION_NO'
}
RGenerationPoemSetSnapshot = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('code', 'data')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(200); nullable = $false }
data = (New-Ref '#/components/schemas/GenerationPoemSetSnapshot')
}
}
RGenerationPoemBadRequest = (New-FixedErrorSchema 400 'GENERATION_POEM_REQUEST_INVALID')
RGenerationPoemUnauthorized = (New-FixedErrorSchema 401 'AUTH_REQUIRED')
RGenerationPoemForbidden = (New-FixedErrorSchema 403 'GENERATION_POEM_EDIT_FORBIDDEN')
RGenerationPoemNotFound = (New-FixedErrorSchema 404 'GENEALOGY_NOT_AVAILABLE')
RGenerationPoemRateLimited = (New-FixedErrorSchema 429 'RATE_LIMITED')
RGenerationPoemReadUnavailable = (New-FixedErrorSchema 500 'GENERATION_POEM_READ_UNAVAILABLE')
RGenerationPoemOutcomeUnknown = (New-FixedErrorSchema 500 'GENERATION_POEM_OUTCOME_UNKNOWN')
GenerationPoemSetVersionConflict = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('code', 'businessCode', 'message', 'current')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(409); nullable = $false }
businessCode = [pscustomobject]@{ type = 'string'; enum = @('POEM_SET_VERSION_CONFLICT'); nullable = $false }
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
current = (New-Ref '#/components/schemas/GenerationPoemSetSnapshot')
}
}
GenerationPoemGenealogyNotReadyConflict = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('code', 'businessCode', 'message')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(409); nullable = $false }
businessCode = [pscustomobject]@{ type = 'string'; enum = @('GENEALOGY_NOT_READY'); nullable = $false }
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
}
}
RGenerationPoemSetConflict = [pscustomobject]@{
oneOf = @(
(New-Ref '#/components/schemas/GenerationPoemSetVersionConflict'),
(New-Ref '#/components/schemas/GenerationPoemGenealogyNotReadyConflict')
)
discriminator = [pscustomobject]@{
propertyName = 'businessCode'
mapping = [pscustomobject]@{
POEM_SET_VERSION_CONFLICT = '#/components/schemas/GenerationPoemSetVersionConflict'
GENEALOGY_NOT_READY = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict'
}
}
'x-conflict-precedence' = @('VERSION', 'READY')
}
GenerationPoemFieldError = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('path', 'message')
properties = [pscustomobject]@{
path = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; pattern = $fieldPathPattern; nullable = $false }
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
}
}
RGenerationPoemUnprocessable = [pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('code', 'businessCode', 'message', 'fieldErrors')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(422); nullable = $false }
businessCode = [pscustomobject]@{
type = 'string'; nullable = $false
enum = @('DUPLICATE_GENERATION_NO', 'DUPLICATE_POEM_ID', 'GENERATION_POEM_CAPACITY_EXCEEDED', 'GENERATION_POEM_REQUEST_ORDER_INVALID', 'GENERATION_POEM_TEXT_INVALID', 'GENERATION_SLOT_CONFLICT', 'NON_CONTIGUOUS_GENERATIONS', 'POEM_ROW_NOT_AVAILABLE')
}
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
fieldErrors = [pscustomobject]@{
type = 'array'; minItems = 0; maxItems = 500; nullable = $false
items = (New-Ref '#/components/schemas/GenerationPoemFieldError')
}
}
}
}
$document = [pscustomobject]@{
openapi = '3.0.1'
'x-app-gateway-policies' = [pscustomobject]@{
APP_GATEWAY_PREFLIGHT = [pscustomobject]@{
allowedRequestHeaders = @('Authorization', 'Content-Type', 'If-Match', 'clientid')
allowedMethods = @('GET', 'PUT', 'OPTIONS')
originPolicy = 'EXPLICIT_DEPLOYMENT_ALLOWLIST'
maxAgeSeconds = 600
}
}
paths = [pscustomobject]@{}
components = [pscustomobject]@{
schemas = $schemas
parameters = [pscustomobject]@{}
requestBodies = [pscustomobject]@{}
responses = [pscustomobject]@{}
callbacks = [pscustomobject]@{}
headers = [pscustomobject]@{
PrivateNoStore = [pscustomobject]@{
schema = [pscustomobject]@{ type = 'string'; enum = @('private, no-store'); nullable = $false }
}
RetryAfter = [pscustomobject]@{
schema = [pscustomobject]@{ type = 'integer'; minimum = 1; maximum = 120; nullable = $false }
}
}
securitySchemes = [pscustomobject]@{
SaToken = [pscustomobject]@{ type = 'apiKey'; in = 'header'; name = 'Authorization' }
}
}
}
Add-TargetGetWithValidExtensions $document
Add-TargetPutWithValidExtensions $document
$pathItem = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'
$get = $pathItem.get
$put = $pathItem.put
Set-Property $get 'deprecated' $false
Set-Property $put 'deprecated' $false
$get.parameters = @((New-ClientIdParameter), (New-GenealogyIdParameter))
$put.parameters = @((New-IfMatchParameter), (New-ClientIdParameter), (New-GenealogyIdParameter))
$put.requestBody = [pscustomobject]@{
required = $true
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody') }
}
}
$getResponses = [ordered]@{
'200' = '#/components/schemas/RGenerationPoemSetSnapshot'
'400' = '#/components/schemas/RGenerationPoemBadRequest'
'401' = '#/components/schemas/RGenerationPoemUnauthorized'
'404' = '#/components/schemas/RGenerationPoemNotFound'
'429' = '#/components/schemas/RGenerationPoemRateLimited'
'500' = '#/components/schemas/RGenerationPoemReadUnavailable'
}
$putResponses = [ordered]@{
'200' = '#/components/schemas/RGenerationPoemSetSnapshot'
'400' = '#/components/schemas/RGenerationPoemBadRequest'
'401' = '#/components/schemas/RGenerationPoemUnauthorized'
'403' = '#/components/schemas/RGenerationPoemForbidden'
'404' = '#/components/schemas/RGenerationPoemNotFound'
'409' = '#/components/schemas/RGenerationPoemSetConflict'
'422' = '#/components/schemas/RGenerationPoemUnprocessable'
'429' = '#/components/schemas/RGenerationPoemRateLimited'
'500' = '#/components/schemas/RGenerationPoemOutcomeUnknown'
}
foreach ($entry in $getResponses.GetEnumerator()) { Set-Property $get.responses $entry.Key (New-ApiResponse $entry.Value ($entry.Key -eq '429')) }
foreach ($entry in $putResponses.GetEnumerator()) { Set-Property $put.responses $entry.Key (New-ApiResponse $entry.Value ($entry.Key -eq '429')) }
return $document
}
$passingSeedIssues = @(Get-Issues (New-SeedDocument))
if ($passingSeedIssues.Count -ne 0) {
throw "complete G12 passing seed must have zero issues: $($passingSeedIssues -join ' | ')"
}
$noSetup = { param($document) }
Assert-MutationRejected 'Path Item parameters keyword casing' {
param($document)
$pathItem = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'
$genealogyId = @($pathItem.get.parameters | Where-Object { $_.name -ceq 'genealogyId' })[0]
Set-Property $pathItem 'parameters' @($genealogyId)
$pathItem.get.parameters = @($pathItem.get.parameters | Where-Object { $_.name -cne 'genealogyId' })
$pathItem.put.parameters = @($pathItem.put.parameters | Where-Object { $_.name -cne 'genealogyId' })
} {
param($document)
$pathItem = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'
Rename-Property $pathItem 'parameters' 'Parameters'
} 'Path Item.*non-canonical object keyword casing: Parameters'
Assert-MutationRejected 'second write through allOf' $noSetup {
param($document)
Add-ShadowMutation $document '/genealogy/app/poems-shadow' ([pscustomobject]@{
allOf = @([pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' })
})
} 'generation-poem APP mutation.*poems-shadow'
Assert-MutationRejected 'second write through two-hop alias' $noSetup {
param($document)
Set-Property $document.components.schemas 'ShadowAliasA' (New-Ref '#/components/schemas/ShadowAliasB')
Set-Property $document.components.schemas 'ShadowAliasB' ([pscustomobject]@{
allOf = @((New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody'))
})
Add-ShadowMutation $document '/genealogy/app/two-hop-shadow' (New-Ref '#/components/schemas/ShadowAliasA')
} 'generation-poem APP mutation.*two-hop-shadow'
Assert-MutationRejected 'two-hop external schema ref' $noSetup {
param($document)
Set-Property $document.components.schemas 'ExternalAliasA' (New-Ref '#/components/schemas/ExternalAliasB')
Set-Property $document.components.schemas 'ExternalAliasB' (New-Ref 'https://evil.test/openapi.json#/components/schemas/Foo')
Add-ShadowMutation $document '/genealogy/app/two-hop-external' (New-Ref '#/components/schemas/ExternalAliasA')
} 'two-hop-external.*uninspectable external'
Assert-MutationRejected 'cycle plus target remains inspectable' $noSetup {
param($document)
Set-Property $document.components.schemas 'CycleAlias' ([pscustomobject]@{
allOf = @(
(New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody'),
(New-Ref '#/components/schemas/CycleAlias')
)
})
Add-ShadowMutation $document '/genealogy/app/cycle-shadow' (New-Ref '#/components/schemas/CycleAlias')
} 'generation-poem APP mutation.*cycle-shadow'
Assert-MutationRejected 'external generic schema ref' $noSetup {
param($document)
Add-ShadowMutation $document '/genealogy/app/external-shadow' ([pscustomobject]@{
'$ref' = 'https://evil.test/openapi.json#/components/schemas/Foo'
})
} 'external-shadow.*uninspectable external'
Assert-MutationRejected 'wrong-section generic schema ref' $noSetup {
param($document)
Add-ShadowMutation $document '/genealogy/app/wrong-section-shadow' ([pscustomobject]@{
'$ref' = '#/components/responses/Foo'
})
} 'wrong-section-shadow.*wrong-section ref'
Assert-MutationRejected 'no-body lexical delete' $noSetup {
param($document)
$operation = New-MinimalOperation 'deleteGenerationPoemShadow' 'delete a generation poem shadow row'
Set-Property $document.paths '/genealogy/app/poems/{poemId}' ([pscustomobject]@{ delete = $operation })
} 'generation-poem APP mutation.*DELETE /genealogy/app/poems/\{poemId\}'
Assert-MutationRejected 'read owner hidden in 206' $noSetup {
param($document)
$operation = New-MinimalOperation 'shadowRead' 'unrelated read'
Set-Property $operation.responses '206' ([pscustomobject]@{
description = 'partial'
headers = [pscustomobject]@{}
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{
schema = [pscustomobject]@{
type = 'object'
properties = [pscustomobject]@{ genealogyId = [pscustomobject]@{}; poemSetVersion = [pscustomobject]@{}; items = [pscustomobject]@{} }
}
}
}
})
Set-Property $document.paths '/genealogy/app/shadow-read' ([pscustomobject]@{ get = $operation })
} 'canonical read.*shadow-read'
Assert-MutationRejected 'path item ref' $noSetup {
param($document)
Set-Property $document.paths '/genealogy/app/path-item-shadow' ([pscustomobject]@{ '$ref' = 'https://evil.test/path-item.yaml' })
} 'Path Item.*path-item-shadow.*must not use'
Assert-MutationRejected 'HEAD canonical read owner' $noSetup {
param($document)
$operation = New-MinimalOperation 'shadowHead' 'head response'
Set-Property $operation.responses '200' (New-ApiResponse '#/components/schemas/RGenerationPoemSetSnapshot' $false)
Set-Property $document.paths '/genealogy/app/head-shadow' ([pscustomobject]@{ head = $operation })
} 'canonical read.*HEAD /genealogy/app/head-shadow'
Assert-MutationRejected 'OPTIONS G12 surface' $noSetup {
param($document)
$operation = New-MinimalOperation 'shadowOptions' 'options response'
Set-Property $operation 'requestBody' ([pscustomobject]@{
required = $true
content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody') } }
})
Set-Property $document.paths '/genealogy/app/options-shadow' ([pscustomobject]@{ options = $operation })
} 'OPTIONS /genealogy/app/options-shadow is a forbidden G12 contract surface'
Assert-MutationRejected 'TRACE G12 surface' $noSetup {
param($document)
$operation = New-MinimalOperation 'traceGenerationPoemShadow' 'trace response'
Set-Property $document.paths '/genealogy/app/trace-shadow' ([pscustomobject]@{ trace = $operation })
} 'TRACE /genealogy/app/trace-shadow is a forbidden G12 contract surface'
Assert-MutationRejected 'requestBody ref sibling' {
param($document)
Set-Property $document.components.requestBodies 'PoemBodyAlias' ([pscustomobject]@{
required = $true
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' } }
}
})
} {
param($document)
$operation = New-MinimalOperation 'shadowRefSibling' 'unrelated write'
Set-Property $operation 'requestBody' ([pscustomobject]@{ '$ref' = '#/components/requestBodies/PoemBodyAlias'; description = 'illegal sibling' })
Set-Property $document.paths '/genealogy/app/ref-sibling-shadow' ([pscustomobject]@{ put = $operation })
} 'ref-sibling-shadow.*Reference Object must contain only'
Assert-MutationRejected 'parameter ref sibling' {
param($document)
Set-Property $document.components.parameters 'GenealogyIdPath' (New-GenealogyIdParameter)
$get = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get
$get.parameters[1] = New-Ref '#/components/parameters/GenealogyIdPath'
} {
param($document)
$ref = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.parameters[1]
Set-Property $ref 'description' 'illegal sibling'
} 'parameter OpenAPI 3.0.1 Reference Object must contain only'
Assert-MutationRejected 'response ref sibling' {
param($document)
$get = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get
Set-Property $document.components.responses 'G12GetSuccessResponse' $get.responses.'200'
$get.responses.'200' = New-Ref '#/components/responses/G12GetSuccessResponse'
} {
param($document)
$ref = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.responses.'200'
Set-Property $ref 'description' 'illegal sibling'
} '200 OpenAPI 3.0.1 Reference Object must contain only'
Assert-MutationRejected 'header ref sibling' $noSetup {
param($document)
$ref = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.responses.'200'.headers.'Cache-Control'
Set-Property $ref 'description' 'illegal sibling'
} 'Cache-Control must be the sole exact local PrivateNoStore Reference Object'
Assert-MutationRejected 'MediaType example conflict' $noSetup {
param($document)
$media = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.requestBody.content.'application/json'
Set-Property $media 'example' ([pscustomobject]@{})
Set-Property $media 'examples' ([pscustomobject]@{})
} 'MediaType Object must not define both example and examples'
Assert-MutationRejected 'SaToken drift' $noSetup {
param($document)
$document.components.securitySchemes.SaToken.type = 'oauth2'
} 'SaToken must be the header apiKey'
Assert-MutationRejected 'SaToken nonempty scopes' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.security[0].SaToken = @('read')
} 'must require exactly one SaToken security alternative'
Assert-MutationRejected 'security object instead of array' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.security = [pscustomobject]@{ SaToken = @() }
} 'must require exactly one SaToken security alternative'
Assert-MutationRejected 'SaToken requirement key casing' $noSetup {
param($document)
$requirement = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.security[0]
Rename-Property $requirement 'SaToken' 'satoken'
} 'must require exactly one SaToken security alternative'
Assert-MutationRejected 'top-level gateway policy key casing' $noSetup {
param($document)
Rename-Property $document 'x-app-gateway-policies' 'X-App-Gateway-Policies'
} 'missing top-level gateway CORS policy owner'
Assert-MutationRejected 'gateway policy owner key casing' $noSetup {
param($document)
Rename-Property $document.'x-app-gateway-policies' 'APP_GATEWAY_PREFLIGHT' 'app_gateway_preflight'
} 'missing top-level gateway CORS policy owner'
Assert-MutationRejected 'gateway policy field key casing' $noSetup {
param($document)
Rename-Property $document.'x-app-gateway-policies'.APP_GATEWAY_PREFLIGHT 'allowedMethods' 'AllowedMethods'
} 'APP_GATEWAY_PREFLIGHT gateway policy.*(unowned object keyword|missing required object keyword)'
Assert-MutationRejected 'operation CORS extension key casing' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put
Rename-Property $operation 'x-cors-policy-owner' 'X-Cors-Policy-Owner'
} 'unowned object keyword: X-Cors-Policy-Owner|PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'operation CORS owner value casing' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-cors-policy-owner' = 'app_gateway_preflight'
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'component owner key casing' $noSetup {
param($document)
Rename-Property $document.components.schemas 'GenerationPoemText' 'generationPoemText'
} 'missing schema owner: GenerationPoemText|references missing schemas owner: GenerationPoemText'
Assert-MutationRejected 'canonical ref target casing' $noSetup {
param($document)
$document.components.schemas.GenerationPoemSetItem.properties.generationText.'$ref' = '#/components/schemas/generationPoemText'
} 'GenerationPoemSetItem.generationText must be the sole exact local schema ref|references missing schemas owner: generationPoemText'
Assert-MutationRejected 'reference keyword casing' $noSetup {
param($document)
$reference = $document.components.schemas.GenerationPoemSetItem.properties.generationText
Rename-Property $reference '$ref' '$Ref'
} 'GenerationPoemSetItem.generationText must be the sole exact local schema ref'
Assert-MutationRejected 'candidate extension key casing' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put
Rename-Property $operation 'x-candidate-steps' 'X-Candidate-Steps'
} 'unowned object keyword: X-Candidate-Steps|PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'candidate step value casing' $noSetup {
param($document)
$steps = @($document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps')
$steps[0] = $steps[0].ToLowerInvariant()
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps' = $steps
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'candidate adjacent step swap' $noSetup {
param($document)
$steps = @($document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps')
$first = $steps[0]
$steps[0] = $steps[1]
$steps[1] = $first
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps' = $steps
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'operationId keyword casing' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get
Rename-Property $operation 'operationId' 'OperationId'
} 'unowned object keyword: OperationId|missing required object keyword: operationId'
Assert-MutationRejected 'schema type keyword casing' $noSetup {
param($document)
Rename-Property $document.components.schemas.GenealogyId 'type' 'Type'
} 'GenealogyId contains an unowned schema keyword: Type'
Assert-MutationRejected 'schema type value casing' $noSetup {
param($document)
$document.components.schemas.GenealogyId.type = 'String'
} 'GenealogyId must be a non-null 1\.\.128 lexical opaque identifier'
Assert-MutationRejected 'top-level webhooks in 3.0.1' $noSetup {
param($document)
Set-Property $document 'webhooks' ([pscustomobject]@{})
} 'must not define top-level webhooks'
Assert-MutationRejected 'component callback owner' $noSetup {
param($document)
Set-Property $document.components.callbacks 'PoemCallback' ([pscustomobject]@{
'{$request.body#/callbackUrl}' = [pscustomobject]@{
post = [pscustomobject]@{
requestBody = [pscustomobject]@{
required = $true
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{
schema = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' }
}
}
}
}
}
})
} 'unowned component callback exposes the G12 contract'
Assert-MutationRejected 'Header Object content conflict' $noSetup {
param($document)
Set-Property $document.components.headers.PrivateNoStore 'content' ([pscustomobject]@{})
} 'PrivateNoStore header owner contains an unowned object keyword: content'
Assert-MutationRejected 'gateway CORS policy drift' $noSetup {
param($document)
$document.'x-app-gateway-policies'.APP_GATEWAY_PREFLIGHT.allowedRequestHeaders = @('Authorization', 'Content-Type', 'clientid')
} 'APP_GATEWAY_PREFLIGHT must own the exact G12 browser preflight policy'
Assert-MutationRejected 'GET CORS header loss' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.'x-cors-required-request-headers' = @('Authorization')
} 'GET .*gateway CORS headers'
Assert-MutationRejected 'PUT candidate order drift' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put
$operation.'x-candidate-steps' = @('WRITE_ATOMICALLY')
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'PUT final capacity step missing' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put
$operation.'x-candidate-steps' = @($operation.'x-candidate-steps' | Where-Object { $_ -ne 'VALIDATE_FINAL_ACTIVE_CAPACITY' })
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'PUT allocated ID validation missing' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put
$operation.'x-candidate-steps' = @($operation.'x-candidate-steps' | Where-Object { $_ -ne 'VALIDATE_ALLOCATED_IDS_NONEMPTY_UNIQUE_NOT_IN_BASELINE' })
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'PUT unknown target omits disableMissing' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put
$operation.'x-unknown-target-construction-fields' = @('genealogyId', 'declaredItems')
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'PUT unknown NEW ID policy drift' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-unknown-new-id-policy' = 'ALLOW_BASELINE_REUSE'
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'PUT unknown set equality drift' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-unknown-set-equality' = 'ALLOW_EXTRA_ROWS'
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'PUT unknown overlap precedence drift' $noSetup {
param($document)
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-unknown-overlap-policy' = 'ATTRIBUTE_TO_REQUEST'
} 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted'
Assert-MutationRejected 'unowned operation extension' $noSetup {
param($document)
$operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get
Set-Property $operation 'x-conflict-policy' 'LAST_WINS'
} 'unowned semantic extension: x-conflict-policy'
Assert-MutationRejected 'bad 409 scalar' {
param($document)
Set-Property $document.components.schemas 'GenerationPoemSetVersionConflict' ([pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('code', 'businessCode', 'message', 'current')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; enum = @(409); nullable = $false }
businessCode = [pscustomobject]@{ type = 'string'; enum = @('POEM_SET_VERSION_CONFLICT'); nullable = $false }
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
current = [pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemSetSnapshot' }
}
})
} {
param($document)
$document.components.schemas.GenerationPoemSetVersionConflict.properties.code.type = 'string'
} 'GenerationPoemSetVersionConflict.code must be a non-null integer scalar'
Assert-MutationRejected 'nullable 409 current ref sibling' $noSetup {
param($document)
$current = $document.components.schemas.GenerationPoemSetVersionConflict.properties.current
Set-Property $current 'nullable' $true
} 'GenerationPoemSetVersionConflict.current must be the sole exact local schema ref'
Assert-MutationRejected 'extra discriminator mapping' {
param($document)
Set-Property $document.components.schemas 'RGenerationPoemSetConflict' ([pscustomobject]@{
oneOf = @(
[pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemSetVersionConflict' },
[pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict' }
)
discriminator = [pscustomobject]@{
propertyName = 'businessCode'
mapping = [pscustomobject]@{
POEM_SET_VERSION_CONFLICT = '#/components/schemas/GenerationPoemSetVersionConflict'
GENEALOGY_NOT_READY = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict'
}
}
'x-conflict-precedence' = @('VERSION', 'READY')
})
} {
param($document)
Set-Property $document.components.schemas.RGenerationPoemSetConflict.discriminator.mapping 'EVIL' 'https://evil.test/Foo'
} 'discriminator mapping keys must be exactly'
Assert-MutationRejected 'unreachable 422 soft-disable confirmation code' {
param($document)
Set-Property $document.components.schemas 'RGenerationPoemUnprocessable' ([pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('code', 'businessCode', 'message', 'fieldErrors')
properties = [pscustomobject]@{
code = [pscustomobject]@{ type = 'integer'; enum = @(422); nullable = $false }
businessCode = [pscustomobject]@{
type = 'string'; nullable = $false
enum = @('DUPLICATE_GENERATION_NO', 'DUPLICATE_POEM_ID', 'GENERATION_POEM_CAPACITY_EXCEEDED', 'GENERATION_POEM_REQUEST_ORDER_INVALID', 'GENERATION_POEM_TEXT_INVALID', 'GENERATION_SLOT_CONFLICT', 'NON_CONTIGUOUS_GENERATIONS', 'POEM_ROW_NOT_AVAILABLE')
}
message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false }
fieldErrors = [pscustomobject]@{
type = 'array'; minItems = 0; maxItems = 500; nullable = $false
items = [pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemFieldError' }
}
}
})
} {
param($document)
$codes = @($document.components.schemas.RGenerationPoemUnprocessable.properties.businessCode.enum)
$document.components.schemas.RGenerationPoemUnprocessable.properties.businessCode.enum = @($codes + 'SOFT_DISABLE_CONFIRMATION_REQUIRED')
} 'RGenerationPoemUnprocessable.businessCode must be a non-null string enum exactly'
Assert-MutationRejected 'nullable 422 businessCode' $noSetup {
param($document)
$document.components.schemas.RGenerationPoemUnprocessable.properties.businessCode.nullable = $true
} 'RGenerationPoemUnprocessable.businessCode must be a non-null string enum exactly'
Assert-MutationRejected '422 fieldErrors union wrapper' $noSetup {
param($document)
Set-Property $document.components.schemas.RGenerationPoemUnprocessable.properties.fieldErrors 'oneOf' @([pscustomobject]@{})
} 'RGenerationPoemUnprocessable.fieldErrors must not define conflicting schema keyword: oneOf'
Assert-MutationRejected 'unowned body extension' {
param($document)
Set-Property $document.components.schemas 'AppGenerationPoemSetUpdateBody' ([pscustomobject]@{
type = 'object'; nullable = $false; additionalProperties = $false
required = @('items', 'disableMissing')
properties = [pscustomobject]@{
items = [pscustomobject]@{ type = 'array'; minItems = 0; maxItems = 500; nullable = $false; items = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetItem' } }
disableMissing = [pscustomobject]@{ type = 'boolean'; nullable = $false }
}
'x-request-order' = 'STRICT_GENERATION_NO_ASC_NO_SERVER_SORT'
'x-false-policy' = 'PRESERVE_ALL_UNREPRESENTED_BASELINE_ACTIVE'
'x-true-policy' = 'SOFT_DISABLE_ALL_UNREPRESENTED_BASELINE_ACTIVE'
'x-empty-false-policy' = 'SEMANTIC_NOOP'
'x-empty-true-policy' = 'SOFT_DISABLE_ALL_ACTIVE'
'x-final-active-invariant' = 'EMPTY_OR_CONTIGUOUS_INTERVAL'
'x-candidate-validation' = 'BEFORE_ANY_DOMAIN_WRITE'
'x-duplicate-generation-text' = 'ALLOWED'
})
} {
param($document)
Set-Property $document.components.schemas.AppGenerationPoemSetUpdateBody 'x-server-sort' $true
} 'AppGenerationPoemSetUpdateBody contains an unowned semantic extension: x-server-sort'
Assert-MutationRejected 'Unicode pattern drift' {
param($document)
Set-Property $document.components.schemas 'GenerationPoemText' ([pscustomobject]@{
type = 'string'; minLength = 1; maxLength = 50; nullable = $false
pattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$'
'x-normalization' = 'NFC'
'x-length-unit' = 'UNICODE_CODE_POINT'
'x-well-formed-unicode' = $true
'x-boundary-whitespace' = 'REJECT'
'x-forbidden-code-point-classes' = @('C0', 'C1', 'CR', 'LF', 'TAB', 'ARABIC_LETTER_MARK', 'LTR_RTL_MARK', 'BIDI_OVERRIDE_OR_ISOLATE', 'WORD_JOINER', 'ZERO_WIDTH_OR_BOM', 'LINE_PARAGRAPH_SEPARATOR', 'UNPAIRED_SURROGATE')
})
} {
param($document)
$document.components.schemas.GenerationPoemText.pattern = '^.*$'
} 'GenerationPoemText must be NFC'
Assert-MutationRejected 'int64 generation number' {
param($document)
Set-Property $document.components.schemas 'GenerationPoemGenerationNo' ([pscustomobject]@{
type = 'integer'; format = 'int32'; minimum = 1; maximum = 2147483647; nullable = $false
})
} {
param($document)
$document.components.schemas.GenerationPoemGenerationNo.format = 'int64'
} 'GenerationPoemGenerationNo must be a non-null safe int32-range'
Assert-LocalBaselineAccepted 'legal int32 generation number' {
param($document)
Set-Property $document.components.schemas 'GenerationPoemGenerationNo' ([pscustomobject]@{
type = 'integer'; format = 'int32'; minimum = 1; maximum = 2147483647; nullable = $false
})
} 'GenerationPoemGenerationNo must be a non-null safe int32-range|GenerationPoemGenerationNo contains'
Assert-LocalBaselineAccepted 'legal component requestBody ref' {
param($document)
Set-Property $document.components.requestBodies 'GenerationPoemUpdateRequest' ([pscustomobject]@{
required = $true
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' } }
}
})
$document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.requestBody = [pscustomobject]@{
'$ref' = '#/components/requestBodies/GenerationPoemUpdateRequest'
}
} 'PUT generation-poem request body (must be required|must expose only|OpenAPI 3.0.1 Reference Object)'
Assert-LocalBaselineAccepted 'lineage generation projection is not a G12 owner' {
param($document)
Set-Property $document.components.schemas 'LineagePersonView' ([pscustomobject]@{
type = 'object'
description = 'generation poem display text is a consumer annotation only'
properties = [pscustomobject]@{
personId = [pscustomobject]@{ type = 'string' }
generationNo = [pscustomobject]@{ type = 'integer' }
generationName = [pscustomobject]@{ type = 'string'; description = 'generation poem label' }
}
})
Set-Property $document.components.schemas 'LineagePersonBody' ([pscustomobject]@{
type = 'object'
properties = [pscustomobject]@{
generationNo = [pscustomobject]@{ type = 'integer' }
generationName = [pscustomobject]@{ type = 'string'; description = 'generation poem label' }
}
})
$get = New-MinimalOperation 'getLineagePersonShadow' 'lineage consumer'
Set-Property $get.responses '200' (New-ApiResponse '#/components/schemas/LineagePersonView' $false)
$put = New-MinimalOperation 'updateLineagePersonShadow' 'lineage consumer'
Set-Property $put 'requestBody' ([pscustomobject]@{
required = $true
content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/LineagePersonBody') } }
})
Set-Property $document.paths '/genealogy/app/genealogies/{genealogyId}/lineage/person-shadow' ([pscustomobject]@{ get = $get; put = $put })
} 'generation-poem APP (mutation|canonical read).*lineage/person-shadow'
Assert-LocalBaselineAccepted 'schema annotations are not G12 owners' {
param($document)
Set-Property $document.components.schemas 'UnrelatedAnnotatedBody' ([pscustomobject]@{
type = 'object'
description = 'unrelated write schema'
example = [pscustomobject]@{ generationNo = 1; generationText = '示例' }
default = [pscustomobject]@{ generationNo = 2; generationText = '默认' }
properties = [pscustomobject]@{
unrelatedValue = [pscustomobject]@{ type = 'string' }
}
})
$operation = New-MinimalOperation 'updateUnrelatedAnnotatedBody' 'unrelated write'
Set-Property $operation 'parameters' @([pscustomobject]@{
name = 'sample'; in = 'query'; required = $false
schema = [pscustomobject]@{ type = 'string' }
example = [pscustomobject]@{ generationNo = 3; generationText = '参数示例' }
})
Set-Property $operation 'requestBody' ([pscustomobject]@{
required = $true
content = [pscustomobject]@{
'application/json' = [pscustomobject]@{
schema = (New-Ref '#/components/schemas/UnrelatedAnnotatedBody')
examples = [pscustomobject]@{
sample = [pscustomobject]@{ value = [pscustomobject]@{ generationNo = 4; generationText = '媒体示例' } }
}
}
}
})
Set-Property $document.paths '/genealogy/app/unrelated-annotated-write' ([pscustomobject]@{ put = $operation })
} 'generation-poem APP mutation owners.*unrelated-annotated-write'
$unicodePattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$'
$hanDe = [string][char]0x5FB7
$hanCheng = [string][char]0x627F
$validPair = "$hanDe$hanCheng"
foreach ($sample in @(" $validPair", "$validPair ", "$hanDe`t$hanCheng", "$hanDe`r$hanCheng", "$hanDe`n$hanCheng", "$hanDe$([char]0x0085)$hanCheng", "$hanDe$([char]0x061C)$hanCheng", "$hanDe$([char]0x200B)$hanCheng", "$hanDe$([char]0x200E)$hanCheng", "$hanDe$([char]0x2028)$hanCheng", "$hanDe$([char]0x202E)$hanCheng", "$hanDe$([char]0x2060)$hanCheng", "$hanDe$([char]0x2066)$hanCheng", "$hanDe$([char]0xFEFF)$hanCheng")) {
if ([regex]::IsMatch($sample, $unicodePattern)) {
throw "Unicode regex accepted a forbidden sample: $([BitConverter]::ToString([Text.Encoding]::Unicode.GetBytes($sample)))"
}
}
$supplementary = [char]::ConvertFromUtf32(0x20000)
foreach ($sample in @($validPair, "$hanDe$hanDe", "$supplementary$hanCheng")) {
if (-not [regex]::IsMatch($sample, $unicodePattern)) {
throw "Unicode regex rejected a valid sample: $sample"
}
}
$unicodeRuntimePath = Join-Path $PSScriptRoot 'g12-generation-poem-unicode-contract-runtime-smoke.js'
$unicodeRuntimeOutput = @(& node $unicodeRuntimePath 2>&1)
if ($LASTEXITCODE -ne 0 -or 'G12-GENERATION-POEM-UNICODE-RUNTIME-SMOKE PASS' -notin $unicodeRuntimeOutput) {
throw "G12 ECMAScript Unicode contract smoke failed: $($unicodeRuntimeOutput -join ' | ')"
}
Write-Output 'G12-GENERATION-POEM-OPENAPI-ADVERSARIAL-CONTRACT PASS'