411 lines
13 KiB
TypeScript
411 lines
13 KiB
TypeScript
/**
|
|
* S3: Init command scaffold templates
|
|
* Each preset returns a config object and file contents for the init command.
|
|
*/
|
|
|
|
import type { ApophisConfig, PresetDefinition, ProfileDefinition, EnvironmentPolicy } from '../../../core/types.js';
|
|
|
|
export interface ScaffoldResult {
|
|
config: ApophisConfig;
|
|
readmeContent: string;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// safe-ci: Minimal CI-safe preset (default)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export function safeCiScaffold(): ScaffoldResult {
|
|
const preset: PresetDefinition = {
|
|
name: 'safe-ci',
|
|
timeout: 5000,
|
|
parallel: false,
|
|
chaos: false,
|
|
observe: false,
|
|
};
|
|
|
|
const profile: ProfileDefinition = {
|
|
name: 'quick',
|
|
mode: 'verify',
|
|
preset: 'safe-ci',
|
|
routes: ['POST /users'],
|
|
};
|
|
|
|
const envLocal: EnvironmentPolicy = {
|
|
name: 'local',
|
|
allowVerify: true,
|
|
allowObserve: true,
|
|
allowQualify: false,
|
|
allowChaos: false,
|
|
allowBlocking: true,
|
|
requireSink: false,
|
|
};
|
|
|
|
const config: ApophisConfig = {
|
|
mode: 'verify',
|
|
profiles: { quick: profile },
|
|
presets: { 'safe-ci': preset },
|
|
environments: { local: envLocal },
|
|
// Uncomment to enforce cross-cutting behavioral rules across all matching routes:
|
|
// pluginContracts: {
|
|
// 'auth-presence': {
|
|
// appliesTo: '/api/**',
|
|
// hooks: { onRequest: { requires: ['request_headers(this).authorization != null'] } },
|
|
// },
|
|
// },
|
|
};
|
|
|
|
const readmeContent = `
|
|
# APOPHIS Setup — safe-ci preset
|
|
|
|
This project was scaffolded with \`apophis init --preset safe-ci\`.
|
|
|
|
## Quick Start
|
|
|
|
1. Ensure you have a Fastify app with @fastify/swagger registered.
|
|
2. Add behavioral contracts to your route schemas using \`x-ensures\`.
|
|
3. Run: apophis verify --profile quick
|
|
|
|
## What This Preset Does
|
|
|
|
- Runs only behavioral contracts (not schema-only routes).
|
|
- No chaos, no observe, no stateful testing.
|
|
- Safe for CI pipelines.
|
|
- Timeout: 5s per route.
|
|
|
|
## Example Behavioral Contract
|
|
|
|
Add this inside your route schema to check that a created resource is retrievable:
|
|
|
|
\`\`\`javascript
|
|
"x-ensures": [
|
|
"response_code(GET /users/{response_body(this).id}) == 200"
|
|
]
|
|
\`\`\`
|
|
|
|
If \`apophis verify\` says "No behavioral contracts found", it means your routes have schemas but no \`x-ensures\` or \`x-requires\` clauses. Add at least one clause per route you want to verify.
|
|
|
|
## Next Steps
|
|
|
|
- Add more routes to the \`routes\` array in your profile.
|
|
- Try \`apophis init --preset platform-observe\` for production readiness.
|
|
- Try \`apophis init --preset protocol-lab\` for multi-step flows.
|
|
`;
|
|
|
|
return { config, readmeContent };
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// platform-observe: Production-ready with observe mode
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export function platformObserveScaffold(): ScaffoldResult {
|
|
const preset: PresetDefinition = {
|
|
name: 'platform-observe',
|
|
timeout: 10000,
|
|
parallel: true,
|
|
chaos: false,
|
|
observe: true,
|
|
sampling: 0.1,
|
|
blocking: false,
|
|
sinks: { logs: true, metrics: true },
|
|
};
|
|
|
|
const profile: ProfileDefinition = {
|
|
name: 'staging-observe',
|
|
mode: 'observe',
|
|
preset: 'platform-observe',
|
|
routes: [],
|
|
};
|
|
|
|
const envStaging: EnvironmentPolicy = {
|
|
name: 'staging',
|
|
allowVerify: true,
|
|
allowObserve: true,
|
|
allowQualify: true,
|
|
allowChaos: false,
|
|
allowBlocking: false,
|
|
requireSink: true,
|
|
sinks: { logs: true, metrics: true },
|
|
};
|
|
|
|
const envProduction: EnvironmentPolicy = {
|
|
name: 'production',
|
|
allowVerify: true,
|
|
allowObserve: true,
|
|
allowQualify: false,
|
|
allowChaos: false,
|
|
allowBlocking: false,
|
|
requireSink: true,
|
|
};
|
|
|
|
const config: ApophisConfig = {
|
|
mode: 'observe',
|
|
profile: 'staging-observe',
|
|
profiles: { 'staging-observe': profile },
|
|
presets: { 'platform-observe': preset },
|
|
environments: {
|
|
staging: envStaging,
|
|
production: envProduction,
|
|
},
|
|
// pluginContracts: {
|
|
// 'request-id': {
|
|
// appliesTo: '/api/**',
|
|
// hooks: { onSend: { ensures: ['response_headers(this).x-request-id != null'] } },
|
|
// },
|
|
// },
|
|
};
|
|
|
|
const readmeContent = `
|
|
# APOPHIS Setup — platform-observe preset
|
|
|
|
This project was scaffolded with \`apophis init --preset platform-observe\`.
|
|
|
|
## Quick Start
|
|
|
|
1. Ensure you have a Fastify app with @fastify/swagger registered.
|
|
2. Configure your reporting sink (see environments.staging.requireSink).
|
|
3. Run: apophis observe --profile staging-observe
|
|
|
|
## What This Preset Does
|
|
|
|
- Enables observe mode for production readiness checks.
|
|
- Validates non-blocking semantics and sink configuration.
|
|
- Parallel execution for faster feedback.
|
|
- Requires sink config in staging/production.
|
|
|
|
## Safety
|
|
|
|
- Observe mode is non-blocking by default.
|
|
- Production requires explicit policy to enable blocking.
|
|
- Chaos is disabled in this preset.
|
|
|
|
## Next Steps
|
|
|
|
- Add a sink configuration to your environment policy.
|
|
- Run \`apophis doctor\` to validate the full setup.
|
|
`;
|
|
|
|
return { config, readmeContent };
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// llm-safe: Minimal preset for LLM-generated codebases
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export function llmSafeScaffold(): ScaffoldResult {
|
|
const preset: PresetDefinition = {
|
|
name: 'llm-safe',
|
|
timeout: 3000,
|
|
parallel: false,
|
|
chaos: false,
|
|
observe: false,
|
|
};
|
|
|
|
const profile: ProfileDefinition = {
|
|
name: 'llm-check',
|
|
mode: 'verify',
|
|
preset: 'llm-safe',
|
|
routes: [],
|
|
};
|
|
|
|
const envLocal: EnvironmentPolicy = {
|
|
name: 'local',
|
|
allowVerify: true,
|
|
allowObserve: false,
|
|
allowQualify: false,
|
|
allowChaos: false,
|
|
allowBlocking: false,
|
|
requireSink: false,
|
|
};
|
|
|
|
const config: ApophisConfig = {
|
|
mode: 'verify',
|
|
profile: 'llm-check',
|
|
profiles: { 'llm-check': profile },
|
|
presets: { 'llm-safe': preset },
|
|
environments: { local: envLocal },
|
|
// pluginContracts: {
|
|
// 'auth-presence': {
|
|
// appliesTo: '/api/**',
|
|
// hooks: { onRequest: { requires: ['request_headers(this).authorization != null'] } },
|
|
// },
|
|
// },
|
|
};
|
|
|
|
const readmeContent = `
|
|
# APOPHIS Setup — llm-safe preset
|
|
|
|
This project was scaffolded with \`apophis init --preset llm-safe\`.
|
|
|
|
## Quick Start
|
|
|
|
1. Ensure you have a Fastify app with @fastify/swagger registered.
|
|
2. Add behavioral contracts to your route schemas using \`x-ensures\`.
|
|
3. Run: apophis verify --profile llm-check
|
|
|
|
## What This Preset Does
|
|
|
|
- Ultra-minimal preset for LLM-generated codebases.
|
|
- 3s timeout per route (fast feedback).
|
|
- No observe, no qualify, no chaos — verify only.
|
|
- Conservative defaults to avoid surprising failures.
|
|
|
|
## Example Behavioral Contract
|
|
|
|
Add this inside your route schema to check that a created resource is retrievable:
|
|
|
|
\`\`\`javascript
|
|
"x-ensures": [
|
|
"response_code(GET /users/{response_body(this).id}) == 200"
|
|
]
|
|
\`\`\`
|
|
|
|
If \`apophis verify\` says "No behavioral contracts found", it means your routes have schemas but no \`x-ensures\` or \`x-requires\` clauses. Add at least one clause per route you want to verify.
|
|
|
|
## Next Steps
|
|
|
|
- Add routes to the \`routes\` array once you have behavioral contracts.
|
|
- Run \`apophis doctor\` to check for missing dependencies.
|
|
`;
|
|
|
|
return { config, readmeContent };
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// protocol-lab: Multi-step flow and stateful testing
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export function protocolLabScaffold(): ScaffoldResult {
|
|
const preset: PresetDefinition = {
|
|
name: 'protocol-lab',
|
|
timeout: 15000,
|
|
parallel: false,
|
|
chaos: true,
|
|
observe: false,
|
|
// chaosStrategy: 'sample', // 'one' | 'all' | 'sample' | 'routes'
|
|
// chaosSampleSize: 3, // routes to target when strategy is 'sample'
|
|
// chaosSampleRoutes: [], // explicit route list when strategy is 'routes'
|
|
};
|
|
|
|
const profile: ProfileDefinition = {
|
|
name: 'oauth-nightly',
|
|
mode: 'qualify',
|
|
preset: 'protocol-lab',
|
|
routes: [],
|
|
seed: 42,
|
|
};
|
|
|
|
const envLocal: EnvironmentPolicy = {
|
|
name: 'local',
|
|
allowVerify: true,
|
|
allowObserve: true,
|
|
allowQualify: true,
|
|
allowChaos: true,
|
|
allowBlocking: true,
|
|
requireSink: false,
|
|
};
|
|
|
|
const envTest: EnvironmentPolicy = {
|
|
name: 'test',
|
|
allowVerify: true,
|
|
allowObserve: true,
|
|
allowQualify: true,
|
|
allowChaos: true,
|
|
allowBlocking: true,
|
|
requireSink: false,
|
|
};
|
|
|
|
const config: ApophisConfig = {
|
|
mode: 'qualify',
|
|
profile: 'oauth-nightly',
|
|
profiles: { 'oauth-nightly': profile },
|
|
presets: { 'protocol-lab': preset },
|
|
environments: {
|
|
local: envLocal,
|
|
test: envTest,
|
|
},
|
|
// pluginContracts: {
|
|
// 'rate-limit': {
|
|
// appliesTo: 'POST /api/**',
|
|
// hooks: { onResponse: { ensures: ['status != 429'] } },
|
|
// },
|
|
// },
|
|
// scenarios: [{
|
|
// name: 'create-and-read',
|
|
// steps: [
|
|
// { request: { method: 'POST', url: '/users', body: { name: 'test' } }, expect: ['status:201'], capture: { userId: 'response_body(this).id' } },
|
|
// { request: { method: 'GET', url: '/users/{userId}' }, expect: ['status:200', 'response_body(this).name == "test"'] },
|
|
// ],
|
|
// }],
|
|
};
|
|
|
|
const readmeContent = `
|
|
# APOPHIS Setup — protocol-lab preset
|
|
|
|
This project was scaffolded with \`apophis init --preset protocol-lab\`.
|
|
|
|
## Quick Start
|
|
|
|
1. Ensure you have a Fastify app with @fastify/swagger registered.
|
|
2. Define multi-step flows in your route schemas.
|
|
3. Run: apophis qualify --profile oauth-nightly --seed 42
|
|
|
|
## What This Preset Does
|
|
|
|
- Enables qualify mode for stateful and scenario testing.
|
|
- Chaos engineering enabled (local/test only).
|
|
- Deep depth for thorough exploration.
|
|
- 15s timeout per route.
|
|
|
|
## Safety
|
|
|
|
- Chaos is blocked in production by default.
|
|
- Use \`apophis doctor\` to validate environment safety before qualifying.
|
|
|
|
## Machine Output in CI
|
|
|
|
Qualify can produce large output. In CI, use machine-readable formats and filter events:
|
|
|
|
- \`--format json\` emits a single stable JSON artifact (good for small-to-medium runs).
|
|
- \`--format ndjson\` emits one event per line (good for streaming parsers).
|
|
- Use \`--quiet\` to suppress human progress text.
|
|
- Pipe ndjson to \`jq\` or a custom filter to extract only failures:
|
|
\`\`\`bash
|
|
apophis qualify --profile oauth-nightly --format ndjson | jq 'select(.type == "route.failed")'
|
|
\`\`\`
|
|
- For very large runs, consider writing artifacts to a directory and parsing the JSON file instead of stdout:
|
|
\`\`\`bash
|
|
apophis qualify --profile oauth-nightly --format json --artifact-dir reports/apophis
|
|
\`\`\`
|
|
|
|
## Next Steps
|
|
|
|
- Define scenario sequences in your config.
|
|
- Example scenario is commented out in apophis.config.js — uncomment and adapt.
|
|
- Add route allowlists for chaos if needed.
|
|
- Run \`apophis replay --artifact <path>\` to debug failures.
|
|
`;
|
|
|
|
return { config, readmeContent };
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Preset registry
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export const PRESETS: Record<string, () => ScaffoldResult> = {
|
|
'safe-ci': safeCiScaffold,
|
|
'platform-observe': platformObserveScaffold,
|
|
'llm-safe': llmSafeScaffold,
|
|
'protocol-lab': protocolLabScaffold,
|
|
};
|
|
|
|
export function getPresetNames(): string[] {
|
|
return Object.keys(PRESETS);
|
|
}
|
|
|
|
export function getScaffoldForPreset(preset: string): ScaffoldResult | null {
|
|
const fn = PRESETS[preset];
|
|
return fn ? fn() : null;
|
|
}
|