This cookbook builds on the plugin authoring guide with runnable scenarios. All commands use the shared fixtures in examples/fixtures/policies, so packs and plugins can exercise the same realistic config changes.
The fixture pair contains four changes:
| Change | Path | Why it is useful |
|---|---|---|
| Secret rotation | auth.api_key |
Built-in secret detection |
| Dangerous toggle | features.debug |
Boolean policy matching |
| Capacity jump | database.pool_size |
Numeric threshold matching |
| Nested drift | deployment.rollout.maxUnavailable |
Plugin or pack logic beyond a flat key |
Start in the fixture directory for every command below:
cd examples/fixtures/policiesbaseline.json is a snapshot envelope and current.json is the config being evaluated.
Run the fixture harness from the repository root:
flecto policies test examples/fixtures/policiesIt exits 0 only when the findings exactly match flecto-policy-test.json. The fixture config selects the before/after files, packs, plugins, optional profile, and expected { id, severity, path } values. Flecto reports missing and unexpected findings when they differ, making fixture failures useful in a pack or plugin's test suite.
Use a pack for a stable declarative rule and a plugin for the context-sensitive production approval. The plugin awaits before returning findings, which is supported by evaluate.
node ../../../index.js ci current.json --snapshot-file baseline.json --profile prod --policies default,deployment-review --plugins ./plugins/async-rollout-guard.js --fail-on policyThis command intentionally exits 1 because --fail-on policy treats findings as CI failures. Expected findings:
secret-key-changed(error) from the built-indefaultpackdangerous-toggle-enabled(error) from the built-indefaultpackpool-size-jump(warn) from the built-indefaultpackrollout-unavailability-increased(warn) fromdeployment-reviewasync-rollout-approval(error) from the plugin
Pack and plugin findings are merged before output. If two sources emit the same id and path, Flecto keeps only the highest severity.
async-rollout-guard.js uses all context fields to ensure it runs only for the intended evaluation:
if (
ctx.source !== 'ci'
|| ctx.profile !== 'prod'
|| basename(ctx.file) !== 'current.json'
|| !ctx.packIds.includes('deployment-review')
) {
return [];
}ctx.file is the absolute config path, ctx.profile is the selected profile (or null), ctx.source is ci or watch, and ctx.packIds lists the active packs. Keep context checks narrow: use packs for rules that should apply everywhere, and plugins for rules whose behavior changes by source, profile, file, or active pack.
To see the same plugin deliberately opt out, omit the profile:
node ../../../index.js ci current.json --snapshot-file baseline.json --policies default,deployment-review --plugins ./plugins/async-rollout-guard.js --fail-on policyThis also exits 1, but it produces the four pack findings only; async-rollout-approval is absent because ctx.profile is null.
Plugin load errors are command errors, never an empty finding set. This protects CI from silently skipping policy logic:
node ../../../index.js ci current.json --snapshot-file baseline.json --plugins ./plugins/does-not-exist.js --fail-on policyThe command exits 1 and reports Policy plugin not found: ./plugins/does-not-exist.js. The same fail-closed behavior applies when a module has no evaluate export, throws during import or evaluation, or returns something other than an array.
Copy the fixture directory into a temporary test directory when a test needs local policies/ lookup, or load baseline.json and current.json directly to unit-test diffTrees and evaluatePolicies. Keep new fixtures focused on a semantic change and document their expected finding so packs and plugins can share them without duplicating setup.