Files
DevRunbook-Public/packages/application/src/quality/playbook-package-linter.test.ts
T
DevRunbook release export cfd2804e27
Managed validation / full (push) Successful in 3m18s
Publish DevRunbook source
2026-09-03 04:09:17 +02:00

293 lines
8.3 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
lintPlaybookPackage,
type PlaybookPackageLintInput,
} from './playbook-package-linter'
interface MutableManifest {
metadata: { lifecycle: string }
package: { files: unknown[] }
spec: {
intent: unknown
modes: unknown[]
autonomy: unknown
inputs: unknown[]
workflow: unknown[]
validation: unknown
completion: unknown
reporting: unknown
}
quality: { evaluationCaseIds: string[] }
}
function mutableManifest(input: PlaybookPackageLintInput): MutableManifest {
return input.packageJson as unknown as MutableManifest
}
function validInput(
overrides: Partial<PlaybookPackageLintInput> = {},
): PlaybookPackageLintInput {
return {
packageDigest: 'a'.repeat(64),
packageJson: {
metadata: { lifecycle: 'reviewed' },
package: {
files: [{ path: 'CHANGELOG.md', role: 'changelog' }],
},
spec: {
intent: {
problem: 'A bounded problem.',
outcome: 'A verifiable outcome.',
whenToUse: ['For a bounded change.'],
whenNotToUse: ['When authority is missing.'],
},
modes: ['implement'],
autonomy: { min: 'observe', max: 'verify', default: 'implement' },
inputs: [{ key: 'target' }],
workflow: [
{
id: 'implement',
instruction: 'Implement the requested bounded behavior.',
},
],
validation: {
commandRoles: ['build'],
checks: [
{
id: 'build',
description: 'Run the production build.',
evidence: 'Record command result and exit status.',
},
],
},
completion: { criteria: ['The requested behavior is verified.'] },
reporting: {
sections: [
{
id: 'evidence',
description: 'Report inspected evidence sources.',
},
],
},
},
quality: { evaluationCaseIds: [] },
},
template: {
path: 'prompt.md',
content: 'Work only on {{ inputs.target }}.',
},
representativeRenderedPrompts: [
{
path: 'examples/minimal.rendered.md',
content: 'Work only on packages/application. Record evidence.',
},
],
...overrides,
}
}
describe('playbook package linter', () => {
it('reports a clean validated package as export-ready only with exact evidence', () => {
const input = validInput()
const manifest = mutableManifest(input)
manifest.metadata.lifecycle = 'validated'
manifest.quality.evaluationCaseIds = ['bounded.static']
const result = lintPlaybookPackage({
...input,
lifecycleEvidence: {
targetDigest: 'a'.repeat(64),
passingEvaluationCaseIds: ['bounded.static'],
fixtureVersionRecorded: true,
environmentDigest: 'b'.repeat(64),
unresolvedSafetyRegression: false,
},
})
expect(result).toMatchObject({
exportReadiness: 'ready',
findings: [],
provenance: {
kind: 'static-analysis',
source: 'playbook-package-linter/v1',
artifactDigest: 'a'.repeat(64),
},
})
})
it('finds structural, duplicate, autonomy, template and publication defects', () => {
const input = validInput({ published: true })
const manifest = mutableManifest(input)
manifest.spec.intent = {}
manifest.spec.inputs = [{ key: 'same' }, { key: 'same' }]
manifest.spec.workflow = [{ id: 'same' }, { id: 'same' }]
manifest.spec.autonomy = {
min: 'repair',
max: 'observe',
default: 'invalid',
}
manifest.spec.completion = { criteria: [] }
manifest.spec.reporting = { sections: [] }
manifest.package.files = []
const result = lintPlaybookPackage({
...input,
template: { path: 'prompt.md', content: '{{ inputs.missing }}' },
})
expect(result.exportReadiness).toBe('blocked')
expect(result.findings.map((finding) => finding.ruleId)).toEqual([
'PB001',
'PB002',
'PB003',
'PB004',
'PB005',
'PB005',
'PB006',
'PB007',
'PB008',
])
expect(
result.findings.find((finding) => finding.ruleId === 'PB007')?.path,
).toBe('prompt.md:1')
})
it('blocks validated claims when evidence is absent, stale or incomplete', () => {
const input = validInput()
const manifest = mutableManifest(input)
manifest.metadata.lifecycle = 'validated'
manifest.quality.evaluationCaseIds = ['case-a', 'case-b']
const result = lintPlaybookPackage({
...input,
lifecycleEvidence: {
targetDigest: 'stale',
passingEvaluationCaseIds: ['case-a'],
fixtureVersionRecorded: false,
unresolvedSafetyRegression: true,
},
})
expect(result.findings).toEqual([
expect.objectContaining({
ruleId: 'PB009',
path: '/metadata/lifecycle',
provenance: expect.objectContaining({ artifactDigest: 'a'.repeat(64) }),
}),
])
})
it('flags only literal prompt risks and never exposes a matched secret', () => {
const secret = `ghp_${'x'.repeat(32)}`
const result = lintPlaybookPackage({
...validInput(),
representativeRenderedPrompts: [
{
path: 'rendered/risky.md',
content: [
'Improve everything using best practices.',
'Do not modify the repository. Modify the repository and claim success.',
'Drop table users.',
'Run git push and create a release.',
`Use ${secret}`,
].join('\n'),
},
],
repository: {},
})
expect(result.findings.map((finding) => finding.ruleId)).toEqual([
'PR001',
'PR002',
'PR003',
'PR004',
'SA001',
'SA003',
'SA004',
'SA004',
])
expect(JSON.stringify(result)).not.toContain(secret)
})
it('uses explicit repository, trust-boundary and task context for safety and validation rules', () => {
const input = validInput({
taskKind: 'dependency-change',
repository: {
protectedPaths: ['infra/production'],
changeScopePaths: ['infra'],
},
importedContentPlacements: [
{
sourcePath: 'README.md',
destinationSection: 'Authoritative policy',
},
],
})
const result = lintPlaybookPackage(input)
expect(result.findings.map((finding) => finding.ruleId)).toEqual([
'SA002',
'SA005',
'VA003',
])
expect(result.findings.every((finding) => finding.path.length > 0)).toBe(
true,
)
expect(
result.findings.every((finding) => finding.rationale.length > 0),
).toBe(true)
expect(
result.findings.every((finding) => finding.remediation.length > 0),
).toBe(true)
})
it('covers contextual bugfix, implementation, frontend and inspection validation rules', () => {
const base = validInput()
const manifest = mutableManifest(base)
manifest.spec.validation = { commandRoles: [], checks: [] }
manifest.spec.workflow = [{ id: 'work', instruction: 'Perform work.' }]
manifest.spec.reporting = {
sections: [{ id: 'outcome', description: 'Outcome.' }],
}
manifest.spec.modes = []
expect(
lintPlaybookPackage({ ...base, taskKind: 'bugfix' }).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA001' })]),
)
expect(
lintPlaybookPackage({
...base,
taskKind: 'implementation',
repository: { availableCommandRoles: ['build'] },
}).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA002' })]),
)
expect(
lintPlaybookPackage({ ...base, taskKind: 'frontend-flow' }).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA004' })]),
)
expect(
lintPlaybookPackage({ ...base, taskKind: 'inspection' }).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA005' })]),
)
})
it('is byte-for-byte deterministic for inert pattern-like package text', () => {
const input = validInput({
template: {
path: 'prompt.md',
content: '(a+)+$ {{ inputs.target }} ${notExecuted} <%= inert %>',
},
})
expect(JSON.stringify(lintPlaybookPackage(input))).toBe(
JSON.stringify(lintPlaybookPackage(input)),
)
})
})