docs: final cleanup and accuracy pass before public push
- Fix const inference bug: wrap inferred contracts with status-code guards - Add integration test for status-guarded contract inference - Tighten and deduplicate docs across verify, qualify, getting-started, cli - Fix broken cross-references and TypeScript→JavaScript conversions - Fix factual errors: license, Date.now(), sampling defaults, cache env - Add missing features: --workspace, --generation-profile, json-summary formats - Move stale extension docs (AUTH-RATE-LIMIT-REVISED, HTTP-EXTENSIONS) to attic - Update PLUGIN_CONTRACTS_SPEC status to Implemented - Build: clean | Tests: 849 pass, 0 fail
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
*
|
||||
* Inferred contracts are additive: they supplement, never replace, explicit x-ensures.
|
||||
*
|
||||
* Supported inference:
|
||||
* - required fields → response_body(this).field != null
|
||||
* - minimum (number/integer) → response_body(this).field >= N
|
||||
* - maximum (number/integer) → response_body(this).field <= N
|
||||
* - pattern (string) → response_body(this).field matches "..."
|
||||
* - const → response_body(this).field == value
|
||||
* - enum (small) → response_body(this).field == "a" || response_body(this).field == "b"
|
||||
* Supported inference (all wrapped with status-code guard):
|
||||
* - required fields → response_code(this) == N => response_body(this).field != null
|
||||
* - minimum (number/integer) → response_code(this) == N => response_body(this).field >= N
|
||||
* - maximum (number/integer) → response_code(this) == N => response_body(this).field <= N
|
||||
* - pattern (string) → response_code(this) == N => response_body(this).field matches "..."
|
||||
* - const → response_code(this) == N => response_body(this).field == value
|
||||
* - enum (small) → response_code(this) == N => response_body(this).field == "a" || ...
|
||||
*
|
||||
* Not inferred (leave to x-ensures for business logic):
|
||||
* - minLength/maxLength
|
||||
@@ -188,7 +188,12 @@ export function inferContractsFromRouteSchema(
|
||||
const code = parseInt(statusCode, 10)
|
||||
if (code >= 200 && code < 300) {
|
||||
const inferred = inferContractsFromResponseSchema(statusSchema)
|
||||
formulas.push(...inferred)
|
||||
// Wrap each inferred contract with a status-code guard so it only
|
||||
// applies when the response actually matches the schema it was
|
||||
// inferred from. Prevents a 200-schema const from failing on a 404.
|
||||
for (const formula of inferred) {
|
||||
formulas.push(`response_code(this) == ${code} => ${formula}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -753,3 +753,40 @@ test('integration: route-level x-variants are extracted and executed', async ()
|
||||
await fastify.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('integration: inferred contracts are guarded by status code', async () => {
|
||||
const fastify = Fastify() as unknown as TestFastifyInstance
|
||||
try {
|
||||
await fastify.register(swagger, {})
|
||||
await fastify.register(apophisPlugin, {})
|
||||
fastify.get('/status-guarded', {
|
||||
schema: {
|
||||
'x-category': 'observer',
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: { status: { type: 'string', const: 'success' } },
|
||||
required: ['status']
|
||||
},
|
||||
404: {
|
||||
type: 'object',
|
||||
properties: { error: { type: 'string' } },
|
||||
required: ['error']
|
||||
}
|
||||
}
|
||||
} as Record<string, unknown>
|
||||
}, async (request, reply) => {
|
||||
// Return 404 to verify the 200-schema const doesn't fail
|
||||
reply.status(404)
|
||||
return { error: 'not found' }
|
||||
})
|
||||
await fastify.ready()
|
||||
const result = await fastify.apophis.contract({ depth: 'quick' })
|
||||
// Should pass because the inferred const contract is guarded:
|
||||
// response_code(this) == 200 => response_body(this).status == "success"
|
||||
// The 404 response doesn't trigger the antecedent, so the implication holds.
|
||||
assert.strictEqual(result.summary.failed, 0, 'inferred 200-schema const should not fail on 404')
|
||||
} finally {
|
||||
await fastify.close()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user