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 { 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)), ) }) })