diff --git a/phpunit.xml b/phpunit.xml index 0d6f653..337cec8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -74,8 +74,12 @@ tests/Architecture/ApiSchemaContractTest.php - tests/Architecture/AgentSystemContractTest.php - tests/Architecture/CodexSkillsContractTest.php + tests/Architecture/AgentSystemCatalogContractTest.php + tests/Architecture/AgentSystemFileContractTest.php + tests/Architecture/AgentSystemReferenceContractTest.php + tests/Architecture/CodexAutomationContractTest.php + tests/Architecture/CodexDocsIntegrityContractTest.php + tests/Architecture/CodexSkillReferenceContractTest.php diff --git a/tests/Architecture/AgentSystemCatalogContractTest.php b/tests/Architecture/AgentSystemCatalogContractTest.php new file mode 100644 index 0000000..1c4932d --- /dev/null +++ b/tests/Architecture/AgentSystemCatalogContractTest.php @@ -0,0 +1,64 @@ +decodeAgentJson('.agents/checks/guard-catalog.json'); + $guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : []; + $this->assertNotSame([], $guards, 'Guard catalog must not be empty.'); + + $ids = []; + foreach ($guards as $guard) { + $id = (string) ($guard['id'] ?? ''); + $reviewer = (string) ($guard['reviewer'] ?? ''); + $this->assertMatchesRegularExpression('/^GR-(CORE|TEST|UI|SEC)-[A-Z0-9]{3,}$/', $id, 'Invalid guard id: ' . $id); + $this->assertContains($reviewer, ['code', 'security'], 'Invalid reviewer for ' . $id); + $ids[] = $id; + } + + $this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate guard IDs found.'); + } + + public function testGuardCatalogReviewerAssignment(): void + { + $catalog = $this->decodeAgentJson('.agents/checks/guard-catalog.json'); + $guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : []; + + foreach ($guards as $guard) { + $id = (string) ($guard['id'] ?? ''); + $reviewer = (string) ($guard['reviewer'] ?? ''); + + if (str_starts_with($id, 'GR-SEC-')) { + $this->assertSame('security', $reviewer, $id . ' must be assigned to security reviewer'); + } else { + $this->assertSame('code', $reviewer, $id . ' must be assigned to code reviewer'); + } + } + } + + public function testQualityGateIdsAreUniqueAndWellFormed(): void + { + $gates = $this->decodeAgentJson('.agents/checks/quality-gates.json'); + $items = is_array($gates['gates'] ?? null) ? $gates['gates'] : []; + $this->assertNotSame([], $items, 'Quality gates must not be empty.'); + + $ids = []; + foreach ($items as $gate) { + $id = (string) ($gate['id'] ?? ''); + $command = (string) ($gate['command'] ?? ''); + $this->assertMatchesRegularExpression('/^QG-[0-9]{3}$/', $id, 'Invalid quality gate id: ' . $id); + $this->assertNotSame('', trim($command), 'Missing command for ' . $id); + $ids[] = $id; + } + + $this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate quality gate IDs found.'); + } +} diff --git a/tests/Architecture/AgentSystemContractSupport.php b/tests/Architecture/AgentSystemContractSupport.php new file mode 100644 index 0000000..dd4dbba --- /dev/null +++ b/tests/Architecture/AgentSystemContractSupport.php @@ -0,0 +1,16 @@ + + */ + private function decodeAgentJson(string $path): array + { + $decoded = json_decode($this->readProjectFile($path), true); + $this->assertIsArray($decoded, 'Invalid JSON in ' . $path); + return $decoded; + } +} diff --git a/tests/Architecture/AgentSystemContractTest.php b/tests/Architecture/AgentSystemContractTest.php deleted file mode 100644 index a180200..0000000 --- a/tests/Architecture/AgentSystemContractTest.php +++ /dev/null @@ -1,198 +0,0 @@ -readProjectFile($file), true); - $this->assertIsArray($decoded, 'Invalid JSON: ' . $file); - } - } - - public function testGuardCatalogIdsAreUniqueAndWellFormed(): void - { - $catalog = $this->decodeJson('.agents/checks/guard-catalog.json'); - $guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : []; - $this->assertNotSame([], $guards, 'Guard catalog must not be empty.'); - - $ids = []; - foreach ($guards as $guard) { - $id = (string) ($guard['id'] ?? ''); - $reviewer = (string) ($guard['reviewer'] ?? ''); - $this->assertMatchesRegularExpression('/^GR-(CORE|TEST|UI|SEC)-[A-Z0-9]{3,}$/', $id, 'Invalid guard id: ' . $id); - $this->assertContains($reviewer, ['code', 'security'], 'Invalid reviewer for ' . $id); - $ids[] = $id; - } - - $this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate guard IDs found.'); - } - - public function testGuardCatalogReviewerAssignment(): void - { - $catalog = $this->decodeJson('.agents/checks/guard-catalog.json'); - $guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : []; - - foreach ($guards as $guard) { - $id = (string) ($guard['id'] ?? ''); - $reviewer = (string) ($guard['reviewer'] ?? ''); - - if (str_starts_with($id, 'GR-SEC-')) { - $this->assertSame('security', $reviewer, $id . ' must be assigned to security reviewer'); - } else { - $this->assertSame('code', $reviewer, $id . ' must be assigned to code reviewer'); - } - } - } - - public function testQualityGateIdsAreUniqueAndWellFormed(): void - { - $gates = $this->decodeJson('.agents/checks/quality-gates.json'); - $items = is_array($gates['gates'] ?? null) ? $gates['gates'] : []; - $this->assertNotSame([], $items, 'Quality gates must not be empty.'); - - $ids = []; - foreach ($items as $gate) { - $id = (string) ($gate['id'] ?? ''); - $command = (string) ($gate['command'] ?? ''); - $this->assertMatchesRegularExpression('/^QG-[0-9]{3}$/', $id, 'Invalid quality gate id: ' . $id); - $this->assertNotSame('', trim($command), 'Missing command for ' . $id); - $ids[] = $id; - } - - $this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate quality gate IDs found.'); - } - - public function testTemplatesReferenceKnownGuardAndGateIds(): void - { - $catalog = $this->decodeJson('.agents/checks/guard-catalog.json'); - $gates = $this->decodeJson('.agents/checks/quality-gates.json'); - $guardIds = array_map( - static fn (array $guard): string => (string) ($guard['id'] ?? ''), - is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [] - ); - $gateIds = array_map( - static fn (array $gate): string => (string) ($gate['id'] ?? ''), - is_array($gates['gates'] ?? null) ? $gates['gates'] : [] - ); - - $planTemplate = $this->decodeJson('.agents/templates/plan.template.json'); - $planGuardIds = is_array($planTemplate['guardrails']['required_guard_ids'] ?? null) - ? $planTemplate['guardrails']['required_guard_ids'] - : []; - $planGateIds = is_array($planTemplate['guardrails']['required_quality_gate_ids'] ?? null) - ? $planTemplate['guardrails']['required_quality_gate_ids'] - : []; - foreach ($planGuardIds as $id) { - $this->assertContains($id, $guardIds, 'Unknown guard id in plan template: ' . (string) $id); - } - foreach ($planGateIds as $id) { - $this->assertContains($id, $gateIds, 'Unknown gate id in plan template: ' . (string) $id); - } - - $reviewCodeTemplate = $this->decodeJson('.agents/templates/review-code.template.json'); - $reviewGuardIds = is_array($reviewCodeTemplate['checked_guard_ids'] ?? null) - ? $reviewCodeTemplate['checked_guard_ids'] - : []; - $reviewGateIds = is_array($reviewCodeTemplate['checked_quality_gate_ids'] ?? null) - ? $reviewCodeTemplate['checked_quality_gate_ids'] - : []; - foreach ($reviewGuardIds as $id) { - $this->assertContains($id, $guardIds, 'Unknown guard id in review-code template: ' . (string) $id); - } - foreach ($reviewGateIds as $id) { - $this->assertContains($id, $gateIds, 'Unknown gate id in review-code template: ' . (string) $id); - } - - $reviewSecurityTemplate = $this->decodeJson('.agents/templates/review-security.template.json'); - $secGuardIds = is_array($reviewSecurityTemplate['checked_guard_ids'] ?? null) - ? $reviewSecurityTemplate['checked_guard_ids'] - : []; - foreach ($secGuardIds as $id) { - $this->assertContains($id, $guardIds, 'Unknown guard id in review-security template: ' . (string) $id); - } - } - - public function testRolePromptsReferenceGuardCatalog(): void - { - $plannerPrompt = $this->readProjectFile('.agents/prompts/planner.md'); - $executorPrompt = $this->readProjectFile('.agents/prompts/executor.md'); - $codeReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-code.md'); - $securityReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-security.md'); - - $this->assertStringContainsString('.agents/checks/guard-catalog.json', $plannerPrompt); - $this->assertStringContainsString('.agents/checks/quality-gates.json', $plannerPrompt); - $this->assertStringContainsString('.agents/checks/guard-catalog.json', $executorPrompt); - $this->assertStringContainsString('.agents/checks/quality-gates.json', $executorPrompt); - $this->assertStringContainsString('.agents/checks/guard-catalog.json', $codeReviewerPrompt); - $this->assertStringContainsString('.agents/checks/guard-catalog.json', $securityReviewerPrompt); - } - - public function testAllSevenRolePromptsExist(): void - { - $roles = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer']; - - foreach ($roles as $role) { - $this->readProjectFile('.agents/prompts/' . $role . '.md'); - } - } - - public function testAllSevenContractsExist(): void - { - $contracts = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer']; - - foreach ($contracts as $contract) { - $decoded = json_decode($this->readProjectFile('.agents/contracts/' . $contract . '.schema.json'), true); - $this->assertIsArray($decoded, 'Invalid JSON schema: ' . $contract); - } - } - - public function testAllSevenTemplatesExist(): void - { - $templates = [ - 'analysis', 'plan', 'execution-report', - 'review-code', 'review-security', 'review-acceptance', 'finalize', - ]; - - foreach ($templates as $template) { - $decoded = json_decode($this->readProjectFile('.agents/templates/' . $template . '.template.json'), true); - $this->assertIsArray($decoded, 'Invalid JSON template: ' . $template); - } - } - - /** - * @return array - */ - private function decodeJson(string $path): array - { - $decoded = json_decode($this->readProjectFile($path), true); - $this->assertIsArray($decoded, 'Invalid JSON in ' . $path); - return $decoded; - } -} diff --git a/tests/Architecture/AgentSystemFileContractTest.php b/tests/Architecture/AgentSystemFileContractTest.php new file mode 100644 index 0000000..6bb5409 --- /dev/null +++ b/tests/Architecture/AgentSystemFileContractTest.php @@ -0,0 +1,70 @@ +readProjectFile($file), true); + $this->assertIsArray($decoded, 'Invalid JSON: ' . $file); + } + } + + public function testAllSevenRolePromptsExist(): void + { + $roles = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer']; + + foreach ($roles as $role) { + $this->readProjectFile('.agents/prompts/' . $role . '.md'); + } + } + + public function testAllSevenContractsExist(): void + { + $contracts = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer']; + + foreach ($contracts as $contract) { + $decoded = json_decode($this->readProjectFile('.agents/contracts/' . $contract . '.schema.json'), true); + $this->assertIsArray($decoded, 'Invalid JSON schema: ' . $contract); + } + } + + public function testAllSevenTemplatesExist(): void + { + $templates = [ + 'analysis', 'plan', 'execution-report', + 'review-code', 'review-security', 'review-acceptance', 'finalize', + ]; + + foreach ($templates as $template) { + $decoded = json_decode($this->readProjectFile('.agents/templates/' . $template . '.template.json'), true); + $this->assertIsArray($decoded, 'Invalid JSON template: ' . $template); + } + } +} diff --git a/tests/Architecture/AgentSystemReferenceContractTest.php b/tests/Architecture/AgentSystemReferenceContractTest.php new file mode 100644 index 0000000..49dd44c --- /dev/null +++ b/tests/Architecture/AgentSystemReferenceContractTest.php @@ -0,0 +1,76 @@ +decodeAgentJson('.agents/checks/guard-catalog.json'); + $gates = $this->decodeAgentJson('.agents/checks/quality-gates.json'); + $guardIds = array_map( + static fn (array $guard): string => (string) ($guard['id'] ?? ''), + is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [] + ); + $gateIds = array_map( + static fn (array $gate): string => (string) ($gate['id'] ?? ''), + is_array($gates['gates'] ?? null) ? $gates['gates'] : [] + ); + + $planTemplate = $this->decodeAgentJson('.agents/templates/plan.template.json'); + $planGuardIds = is_array($planTemplate['guardrails']['required_guard_ids'] ?? null) + ? $planTemplate['guardrails']['required_guard_ids'] + : []; + $planGateIds = is_array($planTemplate['guardrails']['required_quality_gate_ids'] ?? null) + ? $planTemplate['guardrails']['required_quality_gate_ids'] + : []; + foreach ($planGuardIds as $id) { + $this->assertContains($id, $guardIds, 'Unknown guard id in plan template: ' . (string) $id); + } + foreach ($planGateIds as $id) { + $this->assertContains($id, $gateIds, 'Unknown gate id in plan template: ' . (string) $id); + } + + $reviewCodeTemplate = $this->decodeAgentJson('.agents/templates/review-code.template.json'); + $reviewGuardIds = is_array($reviewCodeTemplate['checked_guard_ids'] ?? null) + ? $reviewCodeTemplate['checked_guard_ids'] + : []; + $reviewGateIds = is_array($reviewCodeTemplate['checked_quality_gate_ids'] ?? null) + ? $reviewCodeTemplate['checked_quality_gate_ids'] + : []; + foreach ($reviewGuardIds as $id) { + $this->assertContains($id, $guardIds, 'Unknown guard id in review-code template: ' . (string) $id); + } + foreach ($reviewGateIds as $id) { + $this->assertContains($id, $gateIds, 'Unknown gate id in review-code template: ' . (string) $id); + } + + $reviewSecurityTemplate = $this->decodeAgentJson('.agents/templates/review-security.template.json'); + $secGuardIds = is_array($reviewSecurityTemplate['checked_guard_ids'] ?? null) + ? $reviewSecurityTemplate['checked_guard_ids'] + : []; + foreach ($secGuardIds as $id) { + $this->assertContains($id, $guardIds, 'Unknown guard id in review-security template: ' . (string) $id); + } + } + + public function testRolePromptsReferenceGuardCatalog(): void + { + $plannerPrompt = $this->readProjectFile('.agents/prompts/planner.md'); + $executorPrompt = $this->readProjectFile('.agents/prompts/executor.md'); + $codeReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-code.md'); + $securityReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-security.md'); + + $this->assertStringContainsString('.agents/checks/guard-catalog.json', $plannerPrompt); + $this->assertStringContainsString('.agents/checks/quality-gates.json', $plannerPrompt); + $this->assertStringContainsString('.agents/checks/guard-catalog.json', $executorPrompt); + $this->assertStringContainsString('.agents/checks/quality-gates.json', $executorPrompt); + $this->assertStringContainsString('.agents/checks/guard-catalog.json', $codeReviewerPrompt); + $this->assertStringContainsString('.agents/checks/guard-catalog.json', $securityReviewerPrompt); + } +} diff --git a/tests/Architecture/CodexAutomationContractTest.php b/tests/Architecture/CodexAutomationContractTest.php new file mode 100644 index 0000000..258c538 --- /dev/null +++ b/tests/Architecture/CodexAutomationContractTest.php @@ -0,0 +1,46 @@ +readProjectFile('bin/codex-skills-sync.sh'); + $this->assertStringContainsString('--check', $script); + $this->assertStringContainsString('--apply', $script); + $this->assertStringContainsString('mode="check"', $script); + $this->assertStringContainsString('"core-guardrails"', $script); + $this->assertStringContainsString('"starterkit-planner"', $script); + $this->assertStringContainsString('"starterkit-grid-standards"', $script); + $this->assertStringContainsString('"starterkit-php-style-ci"', $script); + } + + public function testDocsLinkCheckScriptExcludesHistoricalRunArtifactsByDefault(): void + { + $script = $this->readProjectFile('bin/docs-link-check.sh'); + $this->assertStringContainsString("!.agents/runs/**", $script); + } + + public function testQualityGatesIncludeDocsLinksAndSkillsSyncChecks(): void + { + $qualityGates = json_decode($this->readProjectFile('.agents/checks/quality-gates.json'), true); + $this->assertIsArray($qualityGates, 'Invalid quality-gates.json'); + $gates = is_array($qualityGates['gates'] ?? null) ? $qualityGates['gates'] : []; + + $byId = []; + foreach ($gates as $gate) { + $id = (string) ($gate['id'] ?? ''); + $byId[$id] = $gate; + } + + $this->assertArrayHasKey('QG-008', $byId); + $this->assertArrayHasKey('QG-009', $byId); + $this->assertSame('bin/docs-link-check.sh', (string) ($byId['QG-008']['command'] ?? '')); + $this->assertSame('bin/codex-skills-sync.sh --check', (string) ($byId['QG-009']['command'] ?? '')); + } +} diff --git a/tests/Architecture/CodexDocsContractSupport.php b/tests/Architecture/CodexDocsContractSupport.php new file mode 100644 index 0000000..fa49d7a --- /dev/null +++ b/tests/Architecture/CodexDocsContractSupport.php @@ -0,0 +1,46 @@ + + */ + private function filesCoveredByDocsLinkIntegrityCheck(): array + { + $root = dirname(__DIR__, 2); + $files = ['README.md']; + $scanDirs = ['docs', '.agents']; + + foreach ($scanDirs as $scanDir) { + $directory = new \RecursiveDirectoryIterator( + $root . '/' . $scanDir, + \FilesystemIterator::SKIP_DOTS + ); + $iterator = new \RecursiveIteratorIterator($directory); + + foreach ($iterator as $fileInfo) { + if (!$fileInfo->isFile()) { + continue; + } + + $fullPath = str_replace('\\', '/', $fileInfo->getPathname()); + if (!str_ends_with($fullPath, '.md')) { + continue; + } + + $relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $fullPath), '/'); + if (str_starts_with($relative, '.agents/runs/')) { + continue; + } + + $files[] = $relative; + } + } + + $files = array_values(array_unique($files)); + sort($files); + return $files; + } +} diff --git a/tests/Architecture/CodexDocsIntegrityContractTest.php b/tests/Architecture/CodexDocsIntegrityContractTest.php new file mode 100644 index 0000000..27df9e1 --- /dev/null +++ b/tests/Architecture/CodexDocsIntegrityContractTest.php @@ -0,0 +1,75 @@ +readProjectFile('docs/reference-codex-prompts.md'); + $this->assertStringContainsString('core-guardrails', $prompts); + $this->assertStringContainsString('starterkit-planner', $prompts); + } + + public function testDocsIndexReferencesCodexPrompts(): void + { + $index = $this->readProjectFile('docs/index.md'); + $this->assertStringContainsString('/docs/reference-codex-prompts.md', $index); + } + + public function testDocumentationLinksResolveAcrossDocsSkillsAndAgentGuides(): void + { + foreach ($this->filesCoveredByDocsLinkIntegrityCheck() as $file) { + $content = $this->readProjectFile($file); + preg_match_all('/(?assertFileExists($absolute, sprintf('Broken docs link "%s" in %s', $link, $file)); + } + } + } + + public function testDocsIndexCoversAllMarkdownDocsWithoutOrphansOrDuplicates(): void + { + $index = $this->readProjectFile('docs/index.md'); + preg_match_all('/\\/docs\\/([a-z0-9-]+\\.md)\\b/', $index, $matches); + $references = $matches[1]; + + $docFiles = glob(dirname(__DIR__, 2) . '/docs/*.md'); + $this->assertNotFalse($docFiles, 'Failed to enumerate docs/*.md files.'); + + $expectedDocs = []; + foreach ($docFiles as $fullPath) { + $basename = basename($fullPath); + if ($basename !== 'index.md') { + $expectedDocs[] = $basename; + } + } + + $refCounts = array_count_values($references); + $duplicates = []; + foreach ($refCounts as $ref => $count) { + if ($count > 1) { + $duplicates[] = $ref; + } + } + + $uniqueReferences = array_values(array_unique($references)); + sort($expectedDocs); + sort($uniqueReferences); + sort($duplicates); + + $orphans = array_values(array_diff($expectedDocs, $uniqueReferences)); + $missingTargets = array_values(array_diff($uniqueReferences, $expectedDocs)); + + $this->assertSame([], $duplicates, 'Duplicate docs/index.md references: ' . implode(', ', $duplicates)); + $this->assertSame([], $orphans, 'Docs markdown files missing in docs/index.md: ' . implode(', ', $orphans)); + $this->assertSame([], $missingTargets, 'docs/index.md references missing files: ' . implode(', ', $missingTargets)); + } +} diff --git a/tests/Architecture/CodexSkillReferenceContractTest.php b/tests/Architecture/CodexSkillReferenceContractTest.php new file mode 100644 index 0000000..1651b23 --- /dev/null +++ b/tests/Architecture/CodexSkillReferenceContractTest.php @@ -0,0 +1,46 @@ +readProjectFile('.agents/skills/core-guardrails/SKILL.md'); + $this->assertStringContainsString('name: core-guardrails', $skill); + + $this->readProjectFile('.agents/skills/core-guardrails/references/boundaries-core.md'); + $this->readProjectFile('.agents/skills/core-guardrails/references/boundaries-ui.md'); + $this->readProjectFile('.agents/skills/core-guardrails/references/quality-gates.md'); + $this->readProjectFile('.agents/skills/core-guardrails/references/source-map.md'); + } + + public function testStarterkitPlannerSkillAndReferencesExist(): void + { + $skill = $this->readProjectFile('.agents/skills/starterkit-planner/SKILL.md'); + $this->assertStringContainsString('name: starterkit-planner', $skill); + + $this->readProjectFile('.agents/skills/starterkit-planner/references/plan-template.md'); + $this->readProjectFile('.agents/skills/starterkit-planner/references/tradeoff-matrix.md'); + } + + public function testStarterkitGridStandardsSkillAndReferencesExist(): void + { + $skill = $this->readProjectFile('.agents/skills/starterkit-grid-standards/SKILL.md'); + $this->assertStringContainsString('name: starterkit-grid-standards', $skill); + + $this->readProjectFile('.agents/skills/starterkit-grid-standards/references/list-checklist.md'); + $this->readProjectFile('.agents/skills/starterkit-grid-standards/references/row-interaction-standard.md'); + $this->readProjectFile('.agents/skills/starterkit-grid-standards/references/page-size-standard.md'); + } + + public function testStarterkitPhpStyleCiSkillExists(): void + { + $skill = $this->readProjectFile('.agents/skills/starterkit-php-style-ci/SKILL.md'); + $this->assertStringContainsString('name: starterkit-php-style-ci', $skill); + } +} diff --git a/tests/Architecture/CodexSkillsContractTest.php b/tests/Architecture/CodexSkillsContractTest.php deleted file mode 100644 index b577c66..0000000 --- a/tests/Architecture/CodexSkillsContractTest.php +++ /dev/null @@ -1,209 +0,0 @@ -readProjectFile('tools/codex-skills/core-guardrails/SKILL.md'); - $this->assertStringContainsString('name: core-guardrails', $skill); - - $this->readProjectFile('tools/codex-skills/core-guardrails/references/boundaries-core.md'); - $this->readProjectFile('tools/codex-skills/core-guardrails/references/boundaries-ui.md'); - $this->readProjectFile('tools/codex-skills/core-guardrails/references/quality-gates.md'); - $this->readProjectFile('tools/codex-skills/core-guardrails/references/source-map.md'); - } - - public function testStarterkitPlannerSkillAndReferencesExist(): void - { - $skill = $this->readProjectFile('tools/codex-skills/starterkit-planner/SKILL.md'); - $this->assertStringContainsString('name: starterkit-planner', $skill); - - $this->readProjectFile('tools/codex-skills/starterkit-planner/references/plan-template.md'); - $this->readProjectFile('tools/codex-skills/starterkit-planner/references/tradeoff-matrix.md'); - } - - public function testStarterkitGridStandardsSkillAndReferencesExist(): void - { - $skill = $this->readProjectFile('tools/codex-skills/starterkit-grid-standards/SKILL.md'); - $this->assertStringContainsString('name: starterkit-grid-standards', $skill); - - $this->readProjectFile('tools/codex-skills/starterkit-grid-standards/references/list-checklist.md'); - $this->readProjectFile('tools/codex-skills/starterkit-grid-standards/references/row-interaction-standard.md'); - $this->readProjectFile('tools/codex-skills/starterkit-grid-standards/references/page-size-standard.md'); - } - - public function testStarterkitPhpStyleCiSkillExists(): void - { - $skill = $this->readProjectFile('tools/codex-skills/starterkit-php-style-ci/SKILL.md'); - $this->assertStringContainsString('name: starterkit-php-style-ci', $skill); - } - - public function testCodexPromptsDocumentationReferencesBothSkills(): void - { - $prompts = $this->readProjectFile('docs/reference-codex-prompts.md'); - $this->assertStringContainsString('core-guardrails', $prompts); - $this->assertStringContainsString('starterkit-planner', $prompts); - } - - public function testDocsIndexReferencesCodexPrompts(): void - { - $index = $this->readProjectFile('docs/index.md'); - $this->assertStringContainsString('/docs/reference-codex-prompts.md', $index); - } - - public function testCodexSkillsSyncScriptSupportsCheckAndApply(): void - { - $script = $this->readProjectFile('bin/codex-skills-sync.sh'); - $this->assertStringContainsString('--check', $script); - $this->assertStringContainsString('--apply', $script); - $this->assertStringContainsString('mode="check"', $script); - $this->assertStringContainsString('"core-guardrails"', $script); - $this->assertStringContainsString('"starterkit-planner"', $script); - $this->assertStringContainsString('"starterkit-grid-standards"', $script); - $this->assertStringContainsString('"starterkit-php-style-ci"', $script); - } - - public function testDocsLinkCheckScriptExcludesHistoricalRunArtifactsByDefault(): void - { - $script = $this->readProjectFile('bin/docs-link-check.sh'); - $this->assertStringContainsString("!agent-system/runs/**", $script); - } - - public function testDocumentationLinksResolveAcrossDocsSkillsAndAgentGuides(): void - { - foreach ($this->filesCoveredByDocsLinkIntegrityCheck() as $file) { - $content = $this->readProjectFile($file); - preg_match_all('/(?projectRootPath() . '/' . $normalized; - $this->assertFileExists($absolute, sprintf('Broken docs link "%s" in %s', $link, $file)); - } - } - } - - public function testDocsIndexCoversAllMarkdownDocsWithoutOrphansOrDuplicates(): void - { - $index = $this->readProjectFile('docs/index.md'); - preg_match_all('/\\/docs\\/([a-z0-9-]+\\.md)\\b/', $index, $matches); - $references = $matches[1]; - - $docFiles = glob($this->projectRootPath() . '/docs/*.md'); - $this->assertNotFalse($docFiles, 'Failed to enumerate docs/*.md files.'); - - $expectedDocs = []; - foreach ($docFiles as $fullPath) { - $basename = basename($fullPath); - if ($basename !== 'index.md') { - $expectedDocs[] = $basename; - } - } - - $refCounts = array_count_values($references); - $duplicates = []; - foreach ($refCounts as $ref => $count) { - if ($count > 1) { - $duplicates[] = $ref; - } - } - - $uniqueReferences = array_values(array_unique($references)); - sort($expectedDocs); - sort($uniqueReferences); - sort($duplicates); - - $orphans = array_values(array_diff($expectedDocs, $uniqueReferences)); - $missingTargets = array_values(array_diff($uniqueReferences, $expectedDocs)); - - $this->assertSame([], $duplicates, 'Duplicate docs/index.md references: ' . implode(', ', $duplicates)); - $this->assertSame([], $orphans, 'Docs markdown files missing in docs/index.md: ' . implode(', ', $orphans)); - $this->assertSame([], $missingTargets, 'docs/index.md references missing files: ' . implode(', ', $missingTargets)); - } - - public function testQualityGatesIncludeDocsLinksAndSkillsSyncChecks(): void - { - $qualityGates = json_decode($this->readProjectFile('agent-system/checks/quality-gates.json'), true); - $this->assertIsArray($qualityGates, 'Invalid quality-gates.json'); - $gates = is_array($qualityGates['gates'] ?? null) ? $qualityGates['gates'] : []; - - $byId = []; - foreach ($gates as $gate) { - $id = (string) ($gate['id'] ?? ''); - $byId[$id] = $gate; - } - - $this->assertArrayHasKey('QG-008', $byId); - $this->assertArrayHasKey('QG-009', $byId); - $this->assertSame('bin/docs-link-check.sh', (string) ($byId['QG-008']['command'] ?? '')); - $this->assertSame('bin/codex-skills-sync.sh --check', (string) ($byId['QG-009']['command'] ?? '')); - } - - public function testDocsSkillsAuditPolicyExists(): void - { - $policy = $this->readProjectFile('agent-system/checks/docs-skills-audit-policy.md'); - $this->assertStringContainsString('P0', $policy); - $this->assertStringContainsString('P1', $policy); - $this->assertStringContainsString('DOCS-SKILLS-AUDIT-00x', $policy); - } - - public function testReviewerGuardsFlowEnforcesDocsSkillsAuditPolicy(): void - { - $prompt = $this->readProjectFile('agent-system/prompts/reviewer-guards.md'); - $schema = $this->readProjectFile('agent-system/contracts/reviewer-guards.schema.json'); - - $this->assertStringContainsString('agent-system/checks/docs-skills-audit-policy.md', $prompt); - $this->assertStringContainsString('DOCS-SKILLS-AUDIT', $prompt); - - $this->assertStringContainsString('audit_policy_ref', $schema); - $this->assertStringContainsString('"P0"', $schema); - $this->assertStringContainsString('"P1"', $schema); - $this->assertStringContainsString('run_artifact_id', $schema); - $this->assertStringContainsString('DOCS-SKILLS-AUDIT-[0-9]{3}', $schema); - } - - /** - * @return list - */ - private function filesCoveredByDocsLinkIntegrityCheck(): array - { - $root = $this->projectRootPath(); - $files = ['README.md']; - $scanDirs = ['docs', 'tools/codex-skills', 'agent-system']; - - foreach ($scanDirs as $scanDir) { - $directory = new \RecursiveDirectoryIterator( - $root . '/' . $scanDir, - \FilesystemIterator::SKIP_DOTS - ); - $iterator = new \RecursiveIteratorIterator($directory); - - foreach ($iterator as $fileInfo) { - if (!$fileInfo->isFile()) { - continue; - } - - $fullPath = str_replace('\\', '/', $fileInfo->getPathname()); - if (!str_ends_with($fullPath, '.md')) { - continue; - } - - $relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $fullPath), '/'); - if (str_starts_with($relative, 'agent-system/runs/')) { - continue; - } - - $files[] = $relative; - } - } - - $files = array_values(array_unique($files)); - sort($files); - return $files; - } -} diff --git a/tests/README.md b/tests/README.md index 53dc58d..a2c0d1a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -32,8 +32,8 @@ ohne dass jede Aenderung immer die komplette Suite im Kopf behalten muss. ## Naechste Refactor-Kandidaten -- `tests/Architecture/CodexSkillsContractTest.php` - `tests/Architecture/ModuleRegistryContractTest.php` -- `tests/Architecture/AgentSystemContractTest.php` - `tests/Architecture/DetailValidationSummaryContractTest.php` - `tests/Architecture/DetailActionPolicyContractTest.php` +- `tests/Architecture/NoAddressBookHardcodingTest.php` +- `tests/Architecture/NoBookmarksHardcodingTest.php`