()
| 26 | return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\/\/.*\n/, '')); |
| 27 | } |
| 28 | |
| 29 | function testRepositoryHookPackagingBoundary() { |
| 30 | const claudeAutoDiscoveryPath = path.join(CITADEL_ROOT, 'hooks', 'hooks.json'); |
| 31 | assert(!fs.existsSync(claudeAutoDiscoveryPath), |
| 32 | 'hooks/hooks.json must stay absent because Claude Code auto-discovers it and project settings already install hooks'); |
| 33 | |
| 34 | const claudeTemplatePath = path.join(CITADEL_ROOT, 'hooks', 'hooks-template.json'); |
| 35 | const claudeTemplate = fs.readFileSync(claudeTemplatePath, 'utf8'); |
| 36 | assert(claudeTemplate.includes('${CLAUDE_PLUGIN_ROOT}'), 'Claude hook template must use CLAUDE_PLUGIN_ROOT'); |
| 37 | assert(!claudeTemplate.includes('${PLUGIN_ROOT}'), 'Claude hook template must not use Codex PLUGIN_ROOT'); |
| 38 | assert(!claudeTemplate.includes('codex-adapter.js'), 'Claude hooks must invoke their scripts directly'); |
| 39 | |
| 40 | const codexManifest = readJson(path.join(CITADEL_ROOT, '.codex-plugin', 'plugin.json')); |
| 41 | assert.equal(codexManifest.hooks, CODEX_PLUGIN_HOOKS_PATH, |
| 42 | 'Codex manifest must keep its hook bundle outside Claude auto-discovery'); |
| 43 | assert(fs.existsSync(path.resolve(CITADEL_ROOT, codexManifest.hooks)), |
| 44 | 'Codex manifest hook bundle must exist at its runtime-owned path'); |
| 45 | } |
| 46 | |
| 47 | function testGeneratedCodexArtifacts() { |
| 48 | const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'citadel-codex-native-')); |
| 49 | try { |
| 50 | execFileSync(process.execPath, [path.join(CITADEL_ROOT, 'scripts', 'codex-compat.js'), tmp], { |
| 51 | cwd: CITADEL_ROOT, |
| 52 | encoding: 'utf8', |
| 53 | stdio: 'pipe', |
| 54 | timeout: 20000, |
| 55 | }); |
| 56 | |
| 57 | const config = fs.readFileSync(path.join(tmp, '.codex', 'config.toml'), 'utf8'); |
| 58 | assert(config.includes('hooks = true'), 'Codex config must use canonical hooks feature'); |
| 59 | assert(!config.includes('codex_hooks = true'), 'Codex config must not emit deprecated codex_hooks feature'); |
| 60 | assert(config.includes('[mcp_servers.citadel-state]'), 'Codex config must include citadel-state MCP server'); |
| 61 | |
| 62 | const manifestPath = path.join(tmp, '.codex-plugin', 'plugin.json'); |
| 63 | const manifest = readJson(manifestPath); |
| 64 | assert.doesNotThrow(() => JSON.parse(fs.readFileSync(manifestPath, 'utf8')), 'Codex plugin manifest must be strict JSON'); |
| 65 | assert.equal(manifest.skills, './.agents/skills/'); |
| 66 | assert.equal(manifest.hooks, CODEX_PLUGIN_HOOKS_PATH); |
| 67 | assert.equal(manifest.mcpServers, './.mcp.json'); |
| 68 | assert(!/claude/i.test(manifest.description), 'Codex manifest description should not be Claude-specific'); |
| 69 | assert(/Codex-native/.test(manifest.interface.shortDescription), 'manifest should be Codex-native'); |
| 70 | |
| 71 | const mcp = readJson(path.join(tmp, '.mcp.json')); |
| 72 | assert(mcp.mcpServers['citadel-state'], 'generated plugin MCP config must include citadel-state'); |
| 73 | |
| 74 | assert(!fs.existsSync(path.join(tmp, 'hooks', 'hooks.json')), |
| 75 | 'Codex generation must not recreate Claude Code auto-discovery path'); |
no test coverage detected