|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Validate the Docs Agent consumer against its pinned native producer chain. |
| 4 | + * |
| 5 | + * Run with DOCS_AGENT_DIR at Docs Agent #121's merge commit and WP_CODEBOX_DIR |
| 6 | + * at WP Codebox #1754's merge commit. This test intentionally fails without |
| 7 | + * both checkouts because it verifies the producer interfaces, not just copies |
| 8 | + * of their expected values in this repository. |
| 9 | + * |
| 10 | + * @package AgentsAPI\Tests |
| 11 | + */ |
| 12 | + |
| 13 | +declare( strict_types=1 ); |
| 14 | + |
| 15 | +const AGENTS_API_DOCS_AGENT_REVISION = '5344a4bfbda4a0553cc92636258e46a715b1c72d'; |
| 16 | +const AGENTS_API_WP_CODEBOX_REVISION = '54c2f9a7bc3cd1fe20055d496c83efcfb99afb41'; |
| 17 | + |
| 18 | +$root = dirname( __DIR__ ); |
| 19 | +$failures = array(); |
| 20 | + |
| 21 | +function agents_api_docs_agent_contract_assert( bool $condition, string $message, array &$failures ): void { |
| 22 | + if ( ! $condition ) { |
| 23 | + $failures[] = $message; |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +function agents_api_docs_agent_contract_directory( string $name, array &$failures ): string { |
| 28 | + $directory = getenv( $name ); |
| 29 | + if ( ! is_string( $directory ) || '' === $directory || ! is_dir( $directory ) ) { |
| 30 | + $failures[] = sprintf( |
| 31 | + '%1$s must point to the pinned producer checkout. Run DOCS_AGENT_DIR=/path/to/docs-agent WP_CODEBOX_DIR=/path/to/wp-codebox php tests/docs-agent-native-workflow-contract.php.', |
| 32 | + $name |
| 33 | + ); |
| 34 | + return ''; |
| 35 | + } |
| 36 | + |
| 37 | + return rtrim( $directory, '/' ); |
| 38 | +} |
| 39 | + |
| 40 | +function agents_api_docs_agent_contract_read( string $directory, string $path, array &$failures ): string { |
| 41 | + $file = $directory . '/' . $path; |
| 42 | + if ( ! is_file( $file ) ) { |
| 43 | + $failures[] = "Missing producer file: {$path}."; |
| 44 | + return ''; |
| 45 | + } |
| 46 | + |
| 47 | + return (string) file_get_contents( $file ); |
| 48 | +} |
| 49 | + |
| 50 | +function agents_api_docs_agent_contract_revision( string $directory, string $expected, string $name, array &$failures ): void { |
| 51 | + $revision = trim( (string) shell_exec( 'git -C ' . escapeshellarg( $directory ) . ' rev-parse HEAD 2>/dev/null' ) ); |
| 52 | + agents_api_docs_agent_contract_assert( $expected === $revision, "{$name} must be checked out at {$expected}; found {$revision}.", $failures ); |
| 53 | +} |
| 54 | + |
| 55 | +function agents_api_docs_agent_contract_match( string $content, string $pattern, string $message, array &$failures ): void { |
| 56 | + agents_api_docs_agent_contract_assert( 1 === preg_match( $pattern, $content ), $message, $failures ); |
| 57 | +} |
| 58 | + |
| 59 | +$docs_agent_dir = agents_api_docs_agent_contract_directory( 'DOCS_AGENT_DIR', $failures ); |
| 60 | +$wp_codebox_dir = agents_api_docs_agent_contract_directory( 'WP_CODEBOX_DIR', $failures ); |
| 61 | + |
| 62 | +if ( $failures ) { |
| 63 | + fwrite( STDERR, "Docs Agent native workflow contract failed:\n- " . implode( "\n- ", $failures ) . "\n" ); |
| 64 | + exit( 1 ); |
| 65 | +} |
| 66 | + |
| 67 | +agents_api_docs_agent_contract_revision( $docs_agent_dir, AGENTS_API_DOCS_AGENT_REVISION, 'Docs Agent', $failures ); |
| 68 | +agents_api_docs_agent_contract_revision( $wp_codebox_dir, AGENTS_API_WP_CODEBOX_REVISION, 'WP Codebox', $failures ); |
| 69 | + |
| 70 | +$consumer_workflow = (string) file_get_contents( $root . '/.github/workflows/docs-agent.yml' ); |
| 71 | +$docs_workflow = agents_api_docs_agent_contract_read( $docs_agent_dir, '.github/workflows/maintain-docs.yml', $failures ); |
| 72 | +$schema_json = agents_api_docs_agent_contract_read( $wp_codebox_dir, 'contracts/run-agent-task-reusable-workflow-interface.v1.json', $failures ); |
| 73 | +$schema = json_decode( $schema_json, true ); |
| 74 | + |
| 75 | +agents_api_docs_agent_contract_assert( is_array( $schema ), 'WP Codebox reusable-workflow schema must be valid JSON.', $failures ); |
| 76 | +if ( is_array( $schema ) ) { |
| 77 | + agents_api_docs_agent_contract_assert( 'wp-codebox/reusable-workflow-interface/v1' === ( $schema['schema'] ?? null ), 'WP Codebox reusable-workflow schema version must match.', $failures ); |
| 78 | + |
| 79 | + foreach ( |
| 80 | + array( |
| 81 | + 'external_package_source' => array( true, 'string' ), |
| 82 | + 'target_repo' => array( true, 'string' ), |
| 83 | + 'prompt' => array( true, 'string' ), |
| 84 | + 'writable_paths' => array( false, 'string' ), |
| 85 | + 'verification_commands' => array( false, 'string' ), |
| 86 | + 'drift_checks' => array( false, 'string' ), |
| 87 | + 'success_requires_pr' => array( false, 'boolean' ), |
| 88 | + 'access_token_repos' => array( false, 'string' ), |
| 89 | + 'require_access_token' => array( false, 'boolean' ), |
| 90 | + 'allowed_repos' => array( false, 'string' ), |
| 91 | + 'run_agent' => array( false, 'boolean' ), |
| 92 | + 'dry_run' => array( false, 'boolean' ), |
| 93 | + ) as $input => $contract |
| 94 | + ) { |
| 95 | + agents_api_docs_agent_contract_assert( $contract[0] === ( $schema['inputs'][ $input ]['required'] ?? null ), "WP Codebox {$input} required contract must match.", $failures ); |
| 96 | + agents_api_docs_agent_contract_assert( $contract[1] === ( $schema['inputs'][ $input ]['type'] ?? null ), "WP Codebox {$input} type contract must match.", $failures ); |
| 97 | + } |
| 98 | + |
| 99 | + foreach ( array( 'OPENAI_API_KEY' => false, 'ACCESS_TOKEN' => false, 'EXTERNAL_PACKAGE_SOURCE_POLICY' => true ) as $secret => $required ) { |
| 100 | + agents_api_docs_agent_contract_assert( $required === ( $schema['secrets'][ $secret ]['required'] ?? null ), "WP Codebox {$secret} secret contract must match.", $failures ); |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*uses:\s*Automattic/docs-agent/\.github/workflows/maintain-docs\.yml@' . AGENTS_API_DOCS_AGENT_REVISION . '\s*$~m', 'Consumer must pin Docs Agent #121.', $failures ); |
| 105 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*audience:\s*technical\s*$~m', 'Consumer audience must be technical.', $failures ); |
| 106 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*run_kind:\s*maintenance\s*$~m', 'Consumer run kind must be maintenance.', $failures ); |
| 107 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*base_ref:\s*main\s*$~m', 'Consumer target base must be main.', $failures ); |
| 108 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*docs_branch:\s*docs-agent/agents-api-docs-upkeep\s*$~m', 'Consumer publication branch must be stable and exact.', $failures ); |
| 109 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*writable_paths:\s*README\.md,docs/\*\*\s*$~m', 'Consumer writable paths must be README.md,docs/**.', $failures ); |
| 110 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~"composer test"\s*,\s*"php tests/no-product-imports-smoke\.php"~s', 'Consumer verification commands must include the test chain.', $failures ); |
| 111 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~"git diff --check"~', 'Consumer drift check must run git diff --check.', $failures ); |
| 112 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*OPENAI_API_KEY:\s*\$\{\{ secrets\.OPENAI_API_KEY \}\}\s*$~m', 'Consumer must forward OPENAI_API_KEY.', $failures ); |
| 113 | +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*EXTERNAL_PACKAGE_SOURCE_POLICY:\s*\$\{\{ secrets\.EXTERNAL_PACKAGE_SOURCE_POLICY \}\}\s*$~m', 'Consumer must forward EXTERNAL_PACKAGE_SOURCE_POLICY.', $failures ); |
| 114 | +agents_api_docs_agent_contract_assert( 0 === preg_match( '~secrets\.ACCESS_TOKEN|^\s*ACCESS_TOKEN:\s*~m', $consumer_workflow ), 'Consumer must not require or forward ACCESS_TOKEN.', $failures ); |
| 115 | + |
| 116 | +foreach ( array( 'contents: write', 'pull-requests: write', 'issues: write' ) as $permission ) { |
| 117 | + agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*' . preg_quote( $permission, '~' ) . '\s*$~m', "Consumer must grant {$permission} for built-in-token publication.", $failures ); |
| 118 | +} |
| 119 | + |
| 120 | +agents_api_docs_agent_contract_match( $docs_workflow, '~technical:maintenance\)\s+package_path="bundles/technical-docs-agent/native/technical-docs-maintenance-agent\.agent\.json"\s+agent_slug="technical-docs-maintenance-agent"\s+package_digest="sha256-bytes-v1:6057aad4eb7c5f0320ccfbce9da93a5fa1d3fc521478b5571ed81c28129325aa"\s+success_requires_pr=false~s', 'Docs Agent #121 technical maintenance lane must map to its exact native package.', $failures ); |
| 121 | +agents_api_docs_agent_contract_match( $docs_workflow, '~uses:\s*Automattic/wp-codebox/\.github/workflows/run-agent-task\.yml@' . AGENTS_API_WP_CODEBOX_REVISION . '~', 'Docs Agent must pin WP Codebox #1754.', $failures ); |
| 122 | +agents_api_docs_agent_contract_match( $docs_workflow, '~external_package_source:\s*\$\{\{ needs\.prepare\.outputs\.external_package_source \}\}.*?target_repo:\s*\$\{\{ github\.repository \}\}.*?writable_paths:\s*\$\{\{ inputs\.writable_paths \}\}.*?verification_commands:\s*\$\{\{ needs\.prepare\.outputs\.verification_commands \}\}.*?drift_checks:\s*\$\{\{ needs\.prepare\.outputs\.drift_checks \}\}.*?success_requires_pr:\s*\$\{\{ needs\.prepare\.outputs\.success_requires_pr == \'true\' \}\}.*?access_token_repos:\s*\$\{\{ github\.repository \}\}.*?require_access_token:\s*true.*?allowed_repos:\s*\'\["\$\{\{ github\.repository \}\}"\]\'~s', 'Docs Agent must preserve the external-package, target, writable, verification, publication, and access chain.', $failures ); |
| 123 | +agents_api_docs_agent_contract_match( $docs_workflow, '~OPENAI_API_KEY:\s*\$\{\{ secrets\.OPENAI_API_KEY \}\}\s+ACCESS_TOKEN:\s*\$\{\{ github\.token \}\}\s+EXTERNAL_PACKAGE_SOURCE_POLICY:\s*\$\{\{ secrets\.EXTERNAL_PACKAGE_SOURCE_POLICY \}\}~s', 'Docs Agent must forward caller credentials and its built-in publication token.', $failures ); |
| 124 | + |
| 125 | +if ( $failures ) { |
| 126 | + fwrite( STDERR, "Docs Agent native workflow contract failed:\n- " . implode( "\n- ", $failures ) . "\n" ); |
| 127 | + exit( 1 ); |
| 128 | +} |
| 129 | + |
| 130 | +fwrite( STDOUT, "Docs Agent native workflow contract passed.\n" ); |
0 commit comments