Files
ECC/tests/plugin-manifest.test.js
T
28e53a0bc1 feat(install): add guided multi-harness installer (#2649)
* feat(install): add guided Claude plugin setup

* fix: support Claude command shims on Windows

* feat: support safe Claude plugin scope migration

* fix(install): preserve interactive setup terminal

* fix(install): auto-migrate setup scope changes

* feat(install): add guided multi-harness installer

* fix(install): sync Yarn binary metadata

* fix(install): handle wizard EOF on Node 18

* ci: allow installer matrix tests to finish

* test(install): allow slower PowerShell delegation

* fix(install): harden guided provider reconciliation

* test(install): harden packaged and local compatibility

* chore: prepare guided installer release 2.2.0

* fix(install): report refreshed Codex marketplace state

* fix(install): verify managed content provenance

* test(install): allow empty Yarn smoke fixture

* test(install): invoke Windows package shims safely

* fix(install): close cross-platform release gaps

* fix(install): require trusted GitHub origins

* fix(install): preserve hook profile precedence

* refactor(install): centralize trusted GitHub origins

* ci: retrigger workflow run after merge of main

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:39:49 -04:00

688 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for plugin manifests:
* - .claude-plugin/plugin.json (Claude Code plugin)
* - .codex-plugin/plugin.json (Codex native plugin)
* - .mcp.json (MCP server config at plugin root)
* - .agents/plugins/marketplace.json (Codex marketplace discovery)
*
* Enforces rules from:
* - .claude-plugin/PLUGIN_SCHEMA_NOTES.md (Claude Code validator rules)
* - https://platform.openai.com/docs/codex/plugins (Codex official docs)
*
* Run with: node tests/run-all.js
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const repoRoot = path.resolve(__dirname, '..');
const packageJsonPath = path.join(repoRoot, 'package.json');
const packageLockPath = path.join(repoRoot, 'package-lock.json');
const rootAgentsPath = path.join(repoRoot, 'AGENTS.md');
const trAgentsPath = path.join(repoRoot, 'docs', 'tr', 'AGENTS.md');
const zhCnAgentsPath = path.join(repoRoot, 'docs', 'zh-CN', 'AGENTS.md');
const ptBrReadmePath = path.join(repoRoot, 'docs', 'pt-BR', 'README.md');
const trReadmePath = path.join(repoRoot, 'docs', 'tr', 'README.md');
const rootZhCnReadmePath = path.join(repoRoot, 'README.zh-CN.md');
const agentYamlPath = path.join(repoRoot, 'agent.yaml');
const versionFilePath = path.join(repoRoot, 'VERSION');
const zhCnReadmePath = path.join(repoRoot, 'docs', 'zh-CN', 'README.md');
const selectiveInstallArchitecturePath = path.join(repoRoot, 'docs', 'SELECTIVE-INSTALL-ARCHITECTURE.md');
const opencodePackageJsonPath = path.join(repoRoot, '.opencode', 'package.json');
const opencodePackageLockPath = path.join(repoRoot, '.opencode', 'package-lock.json');
const opencodeHooksPluginPath = path.join(repoRoot, '.opencode', 'plugins', 'ecc-hooks.ts');
const hooksReadmePath = path.join(repoRoot, 'hooks', 'README.md');
const semverPattern = '[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?';
const installPrPublishedBaseline = '2.1.0';
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
passed++;
} catch (err) {
console.log(` ✗ ${name}`);
console.log(` Error: ${err.message}`);
failed++;
}
}
function loadJsonObject(filePath, label) {
assert.ok(fs.existsSync(filePath), `Expected ${label} to exist`);
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
assert.fail(`Expected ${label} to contain valid JSON: ${error.message}`);
}
assert.ok(parsed && typeof parsed === 'object' && !Array.isArray(parsed), `Expected ${label} to contain a JSON object`);
return parsed;
}
function collectMarkdownFiles(rootPath) {
if (!fs.existsSync(rootPath)) {
return [];
}
const stat = fs.statSync(rootPath);
if (stat.isFile()) {
return rootPath.endsWith('.md') ? [rootPath] : [];
}
const files = [];
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
const nextPath = path.join(rootPath, entry.name);
if (entry.isDirectory()) {
files.push(...collectMarkdownFiles(nextPath));
} else if (entry.isFile() && nextPath.endsWith('.md')) {
files.push(nextPath);
}
}
return files;
}
const rootPackage = loadJsonObject(packageJsonPath, 'package.json');
const packageLock = loadJsonObject(packageLockPath, 'package-lock.json');
const opencodePackageLock = loadJsonObject(opencodePackageLockPath, '.opencode/package-lock.json');
const expectedVersion = rootPackage.version;
test('package.json has version field', () => {
assert.ok(expectedVersion, 'Expected package.json version field');
});
test('package.json declares a stable release after the install PR published baseline', () => {
const parseStableSemver = (version) => {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
assert.ok(match, `Expected a stable semver version, got ${version}`);
return match.slice(1).map(Number);
};
const compareSemver = (left, right) => {
for (let index = 0; index < left.length; index++) {
if (left[index] !== right[index]) return left[index] - right[index];
}
return 0;
};
assert.ok(
compareSemver(parseStableSemver(expectedVersion), parseStableSemver(installPrPublishedBaseline)) > 0,
`Expected package version after install PR baseline ${installPrPublishedBaseline}, got ${expectedVersion}`
);
});
test('package-lock.json root version matches package.json', () => {
assert.strictEqual(packageLock.version, expectedVersion);
assert.ok(packageLock.packages && packageLock.packages[''], 'Expected package-lock root package entry');
assert.strictEqual(packageLock.packages[''].version, expectedVersion);
});
test('AGENTS.md version line matches package.json', () => {
const agentsSource = fs.readFileSync(rootAgentsPath, 'utf8');
const match = agentsSource.match(new RegExp(`^\\*\\*Version:\\*\\* (${semverPattern})$`, 'm'));
assert.ok(match, 'Expected AGENTS.md to declare a top-level version line');
assert.strictEqual(match[1], expectedVersion);
});
test('docs/tr/AGENTS.md version line matches package.json', () => {
const agentsSource = fs.readFileSync(trAgentsPath, 'utf8');
const match = agentsSource.match(new RegExp(`^\\*\\*Sürüm:\\*\\* (${semverPattern})$`, 'm'));
assert.ok(match, 'Expected docs/tr/AGENTS.md to declare a top-level version line');
assert.strictEqual(match[1], expectedVersion);
});
test('docs/zh-CN/AGENTS.md version line matches package.json', () => {
const agentsSource = fs.readFileSync(zhCnAgentsPath, 'utf8');
const match = agentsSource.match(new RegExp(`^\\*\\*版本:\\*\\* (${semverPattern})$`, 'm'));
assert.ok(match, 'Expected docs/zh-CN/AGENTS.md to declare a top-level version line');
assert.strictEqual(match[1], expectedVersion);
});
test('agent.yaml version matches package.json', () => {
const agentYamlSource = fs.readFileSync(agentYamlPath, 'utf8');
const match = agentYamlSource.match(new RegExp(`^version:\\s*(${semverPattern})$`, 'm'));
assert.ok(match, 'Expected agent.yaml to declare a top-level version field');
assert.strictEqual(match[1], expectedVersion);
});
test('agent.yaml uses canonical ECC identity', () => {
const agentYamlSource = fs.readFileSync(agentYamlPath, 'utf8');
assert.ok(/^name:\s*ecc$/m.test(agentYamlSource), 'Expected agent.yaml to use the ecc name');
});
test('VERSION file matches package.json', () => {
const versionFile = fs.readFileSync(versionFilePath, 'utf8').trim();
assert.ok(versionFile, 'Expected VERSION file to be non-empty');
assert.strictEqual(versionFile, expectedVersion);
});
test('docs/SELECTIVE-INSTALL-ARCHITECTURE.md repoVersion example matches package.json', () => {
const source = fs.readFileSync(selectiveInstallArchitecturePath, 'utf8');
const match = source.match(new RegExp(`"repoVersion":\\s*"(${semverPattern})"`));
assert.ok(match, 'Expected docs/SELECTIVE-INSTALL-ARCHITECTURE.md to declare a repoVersion example');
assert.strictEqual(match[1], expectedVersion);
});
test('.opencode/plugins/ecc-hooks.ts active plugin banner matches package.json', () => {
const source = fs.readFileSync(opencodeHooksPluginPath, 'utf8');
const match = source.match(new RegExp(`## Active Plugin: ECC v(${semverPattern})`));
assert.ok(match, 'Expected .opencode/plugins/ecc-hooks.ts to declare an active plugin banner');
assert.strictEqual(match[1], expectedVersion);
});
test('docs/pt-BR/README.md latest release heading matches package.json', () => {
const source = fs.readFileSync(ptBrReadmePath, 'utf8');
assert.ok(source.includes(`### v${expectedVersion} `), 'Expected docs/pt-BR/README.md to advertise the current release heading');
});
test('docs/tr/README.md latest release heading matches package.json', () => {
const source = fs.readFileSync(trReadmePath, 'utf8');
assert.ok(source.includes(`### v${expectedVersion} `), 'Expected docs/tr/README.md to advertise the current release heading');
});
test('README.zh-CN.md latest release heading matches package.json', () => {
const source = fs.readFileSync(rootZhCnReadmePath, 'utf8');
assert.ok(source.includes(`### v${expectedVersion} `), 'Expected README.zh-CN.md to advertise the current release heading');
});
test('docs/zh-CN/README.md latest release heading matches package.json', () => {
const source = fs.readFileSync(zhCnReadmePath, 'utf8');
assert.ok(source.includes(`### v${expectedVersion} `), 'Expected docs/zh-CN/README.md to advertise the current release heading');
});
// ── Claude plugin manifest ────────────────────────────────────────────────────
console.log('\n=== .claude-plugin/plugin.json ===\n');
const claudePluginPath = path.join(repoRoot, '.claude-plugin', 'plugin.json');
const claudeMarketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json');
test('claude plugin.json exists', () => {
assert.ok(fs.existsSync(claudePluginPath), 'Expected .claude-plugin/plugin.json to exist');
});
const claudePlugin = loadJsonObject(claudePluginPath, '.claude-plugin/plugin.json');
test('claude plugin.json has version field', () => {
assert.ok(claudePlugin.version, 'Expected version field');
});
test('claude plugin.json version matches package.json', () => {
assert.strictEqual(claudePlugin.version, expectedVersion);
});
test('claude plugin.json uses short plugin slug', () => {
assert.strictEqual(claudePlugin.name, 'ecc');
});
test('claude plugin.json does NOT have agents field (unsupported by Claude Code validator)', () => {
assert.ok(!('agents' in claudePlugin), 'agents field must NOT be declared — Claude Code plugin validator rejects it');
});
test('claude plugin.json skills is an array', () => {
assert.ok(Array.isArray(claudePlugin.skills), 'Expected skills to be an array');
});
test('claude plugin.json commands is an array', () => {
assert.ok(Array.isArray(claudePlugin.commands), 'Expected commands to be an array');
});
test('claude plugin.json disables bundled MCP servers for provider tool-name compatibility', () => {
const legacyPluginName = 'everything-claude-code';
const reportedOverlongToolName = `mcp__plugin_${legacyPluginName}_github__create_pull_request_review`;
assert.ok(reportedOverlongToolName.length > 64, 'Expected the reported GitHub MCP tool name to exceed strict provider limits without the MCP opt-out');
assert.ok(Object.prototype.hasOwnProperty.call(claudePlugin, 'mcpServers'), 'Expected mcpServers to be explicitly declared so Claude Code does not auto-load root .mcp.json');
assert.deepStrictEqual(claudePlugin.mcpServers, {}, 'Claude plugin installs must not auto-bundle root MCP servers; document/manual MCP install remains supported');
});
test('claude plugin.json does NOT have explicit hooks declaration', () => {
assert.ok(!('hooks' in claudePlugin), 'hooks field must NOT be declared — Claude Code v2.1+ auto-loads hooks/hooks.json by convention');
});
test('claude plugin.json exposes only supported durable hook preferences', () => {
assert.deepStrictEqual(
Object.keys(claudePlugin.userConfig || {}).sort(),
['hook_profile', 'hooks_enabled']
);
const hooksEnabled = claudePlugin.userConfig.hooks_enabled;
assert.deepStrictEqual(
Object.keys(hooksEnabled).sort(),
['default', 'description', 'title', 'type']
);
assert.strictEqual(hooksEnabled.type, 'boolean');
assert.strictEqual(hooksEnabled.default, true);
assert.ok(typeof hooksEnabled.title === 'string' && hooksEnabled.title.trim());
assert.ok(typeof hooksEnabled.description === 'string' && hooksEnabled.description.trim());
const hookProfile = claudePlugin.userConfig.hook_profile;
assert.deepStrictEqual(
Object.keys(hookProfile).sort(),
['default', 'description', 'title', 'type'],
'Claude userConfig does not support enum'
);
assert.strictEqual(hookProfile.type, 'string');
assert.strictEqual(hookProfile.default, 'standard');
assert.ok(typeof hookProfile.title === 'string' && hookProfile.title.trim());
assert.ok(typeof hookProfile.description === 'string' && hookProfile.description.trim());
});
console.log('\n=== .claude-plugin/marketplace.json ===\n');
test('claude marketplace.json exists', () => {
assert.ok(fs.existsSync(claudeMarketplacePath), 'Expected .claude-plugin/marketplace.json to exist');
});
const claudeMarketplace = loadJsonObject(claudeMarketplacePath, '.claude-plugin/marketplace.json');
test('claude marketplace.json keeps only Claude-supported top-level keys', () => {
const unsupportedTopLevelKeys = ['$schema', 'description'];
for (const key of unsupportedTopLevelKeys) {
assert.ok(!(key in claudeMarketplace), `.claude-plugin/marketplace.json must not declare unsupported top-level key "${key}"`);
}
});
test('claude marketplace.json has plugins array with the published plugin entry', () => {
assert.ok(Array.isArray(claudeMarketplace.plugins) && claudeMarketplace.plugins.length > 0, 'Expected plugins array');
assert.strictEqual(claudeMarketplace.name, 'ecc');
assert.strictEqual(claudeMarketplace.plugins[0].name, 'ecc');
});
test('claude marketplace.json plugin version matches package.json', () => {
assert.strictEqual(claudeMarketplace.plugins[0].version, expectedVersion);
});
// ── Codex plugin manifest ─────────────────────────────────────────────────────
// Per official docs: https://platform.openai.com/docs/codex/plugins
// - .codex-plugin/plugin.json is the required manifest
// - skills, mcpServers, apps are STRING paths relative to plugin root (not arrays)
// - .mcp.json must be at plugin root (NOT inside .codex-plugin/)
console.log('\n=== .codex-plugin/plugin.json ===\n');
const codexPluginPath = path.join(repoRoot, '.codex-plugin', 'plugin.json');
test('codex plugin.json exists', () => {
assert.ok(fs.existsSync(codexPluginPath), 'Expected .codex-plugin/plugin.json to exist');
});
const codexPlugin = loadJsonObject(codexPluginPath, '.codex-plugin/plugin.json');
test('codex plugin.json has name field', () => {
assert.ok(codexPlugin.name, 'Expected name field');
});
test('codex plugin.json uses short plugin slug', () => {
assert.strictEqual(codexPlugin.name, 'ecc');
});
test('codex plugin.json has version field', () => {
assert.ok(codexPlugin.version, 'Expected version field');
});
test('codex plugin.json version matches package.json', () => {
assert.strictEqual(codexPlugin.version, expectedVersion);
});
test('codex plugin.json skills is a string (not array) per official spec', () => {
assert.strictEqual(typeof codexPlugin.skills, 'string', 'skills must be a string path per Codex official docs, not an array');
});
test('codex plugin.json mcpServers is a string path (not array) per official spec', () => {
assert.strictEqual(typeof codexPlugin.mcpServers, 'string', 'mcpServers must be a string path per Codex official docs');
});
test('codex plugin.json mcpServers exactly matches "./.mcp.json"', () => {
assert.strictEqual(codexPlugin.mcpServers, './.mcp.json', 'mcpServers must point exactly to "./.mcp.json" per official docs');
const mcpPath = path.join(repoRoot, codexPlugin.mcpServers.replace(/^\.\//, ''));
assert.ok(fs.existsSync(mcpPath), `mcpServers file missing at plugin root: ${codexPlugin.mcpServers}`);
});
test('codex plugin.json explicitly declares the supported lifecycle hook bundle', () => {
assert.strictEqual(
codexPlugin.hooks,
'./hooks/codex-hooks.json',
'Codex supports a top-level hooks path; keep the ECC hook bundle explicit instead of inventing provider-specific settings'
);
const hooksPath = path.join(repoRoot, codexPlugin.hooks.replace(/^\.\//, ''));
assert.ok(fs.existsSync(hooksPath), `Codex hooks file missing at plugin root: ${codexPlugin.hooks}`);
});
test('codex lifecycle hook bundle contains only Codex 0.146-supported schema', () => {
const hooksPath = path.join(repoRoot, 'hooks', 'codex-hooks.json');
const config = loadJsonObject(hooksPath, 'hooks/codex-hooks.json');
assert.deepStrictEqual(Object.keys(config).sort(), ['description', 'hooks'], 'Codex rejects Claude\'s top-level $schema field');
const supportedEvents = new Set([
'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact',
'SessionStart', 'SessionEnd', 'SubagentStart', 'SubagentStop',
'UserPromptSubmit', 'Stop'
]);
assert.deepStrictEqual(
Object.keys(config.hooks || {}),
['SessionStart'],
'Only the verified, non-blocking SessionStart hook ships natively; Claude hook profiles are not Codex hook profiles'
);
assert.deepStrictEqual(
config.hooks.SessionStart.map(group => group.id),
['session:start'],
'Do not ship Claude handlers that surface hook failures in Codex'
);
for (const [event, groups] of Object.entries(config.hooks || {})) {
assert.ok(supportedEvents.has(event), `Unsupported Codex hook event: ${event}`);
assert.ok(Array.isArray(groups) && groups.length > 0, `Expected non-empty matcher groups for ${event}`);
for (const group of groups) {
assert.ok(Array.isArray(group.hooks) && group.hooks.length > 0, `Expected non-empty handlers for ${event}`);
for (const handler of group.hooks) {
assert.strictEqual(handler.type, 'command', `Codex 0.146 only executes command handlers (${event})`);
assert.ok(!Object.prototype.hasOwnProperty.call(handler, 'async'), `Codex 0.146 skips async handlers (${event})`);
assert.ok(
handler.command.includes('process.env.CLAUDE_PLUGIN_ROOT=process.env.PLUGIN_ROOT'),
`Codex plugin hooks must pin Claude-compatible bootstrap resolution to Codex PLUGIN_ROOT (${event})`
);
if (event === 'SessionEnd' && Number.isFinite(handler.timeout)) {
assert.ok(handler.timeout <= 3, 'Codex clamps SessionEnd timeouts to 3 seconds');
}
}
}
}
const claudeConfig = loadJsonObject(path.join(repoRoot, 'hooks', 'hooks.json'), 'hooks/hooks.json');
const sourceSessionStart = claudeConfig.hooks.SessionStart.find(group => group.id === 'session:start');
const expectedSessionStart = {
...sourceSessionStart,
hooks: sourceSessionStart.hooks.map(handler => ({
...handler,
command: handler.command.replace(
'node -e "',
'node -e "if(!process.env.PLUGIN_ROOT)throw new Error(\'Missing Codex PLUGIN_ROOT\');process.env.CLAUDE_PLUGIN_ROOT=process.env.PLUGIN_ROOT;'
)
}))
};
assert.deepStrictEqual(config.hooks.SessionStart[0], expectedSessionStart, 'Codex SessionStart hook must track its canonical implementation with a Codex-root bootstrap');
});
test('hook documentation distinguishes the Claude off setting from runtime profiles', () => {
const source = fs.readFileSync(hooksReadmePath, 'utf8');
assert.ok(source.includes('Claude setup-only value:'), 'Expected hooks README to label off as a Claude setup-only value');
const runtimeProfiles = source.match(/Runtime hook profiles:\n((?:- `[^`]+`[^\n]*\n)+)/);
assert.ok(runtimeProfiles, 'Expected hooks README to identify runtime hook profiles separately');
assert.ok(!runtimeProfiles[1].includes('`off`'), 'off is a Claude setup value, not a runtime hook profile');
for (const profile of ['minimal', 'standard', 'strict']) {
assert.ok(runtimeProfiles[1].includes(`\`${profile}\``), `Expected documented runtime hook profile: ${profile}`);
}
});
test('Chinese capability matrix documents the native Codex SessionStart hook', () => {
const source = fs.readFileSync(zhCnReadmePath, 'utf8');
assert.ok(
source.includes('| **钩子事件** | 8 种类型 | 15 种类型 | SessionStart1 种类型) | 11 种类型 |'),
'Expected the Codex capability column to document one native SessionStart event'
);
assert.ok(
source.includes('| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 |'),
'Expected the Codex capability column to document the SessionStart bootstrap script'
);
assert.ok(
!source.includes('Codex 缺少钩子功能'),
'Codex architecture guidance must not contradict its native SessionStart hook'
);
});
test('codex plugin.json has interface.displayName', () => {
assert.ok(codexPlugin.interface && codexPlugin.interface.displayName, 'Expected interface.displayName for plugin directory presentation');
});
test('codex plugin.json uses canonical ECC repo and display name', () => {
assert.strictEqual(codexPlugin.repository, 'https://github.com/affaan-m/ECC');
assert.strictEqual(codexPlugin.interface.displayName, 'ECC');
});
test('codex plugin presentation assets exist and ship in npm package', () => {
assert.ok(Array.isArray(rootPackage.files), 'Expected package.json files array');
const packageFiles = new Set(rootPackage.files);
for (const field of ['composerIcon', 'logo']) {
const assetPath = codexPlugin.interface[field];
assert.ok(assetPath, `Expected interface.${field}`);
assert.ok(assetPath.startsWith('./assets/'), `Expected interface.${field} to point at a root assets path, got ${assetPath}`);
const packagePath = assetPath.replace(/^\.\//, '');
assert.ok(fs.existsSync(path.join(repoRoot, packagePath)), `Expected interface.${field} asset to exist: ${packagePath}`);
assert.ok(packageFiles.has(packagePath), `Expected package.json files to include interface.${field} asset: ${packagePath}`);
}
});
// ── .mcp.json at plugin root ──────────────────────────────────────────────────
// Per official docs: keep .mcp.json at plugin root, NOT inside .codex-plugin/
console.log('\n=== .mcp.json (plugin root) ===\n');
const mcpJsonPath = path.join(repoRoot, '.mcp.json');
test('.mcp.json exists at plugin root (not inside .codex-plugin/)', () => {
assert.ok(fs.existsSync(mcpJsonPath), 'Expected .mcp.json at repo root (plugin root)');
assert.ok(!fs.existsSync(path.join(repoRoot, '.codex-plugin', '.mcp.json')), '.mcp.json must NOT be inside .codex-plugin/ — only plugin.json belongs there');
});
const mcpConfig = loadJsonObject(mcpJsonPath, '.mcp.json');
test('.mcp.json has mcpServers object', () => {
assert.ok(mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object', 'Expected mcpServers object');
});
test('.mcp.json default set follows the connector policy', () => {
const servers = Object.keys(mcpConfig.mcpServers);
assert.ok(servers.includes('chrome-devtools'), 'Expected chrome-devtools as the default browser connector');
assert.ok(servers.length <= 2, `Default connector set must stay minimal per docs/MCP-CONNECTOR-POLICY.md (found ${servers.length})`);
});
test('.mcp.json does not reintroduce retired default connectors', () => {
const retired = ['github', 'context7', 'exa', 'memory', 'playwright', 'sequential-thinking'];
const servers = Object.keys(mcpConfig.mcpServers);
for (const name of retired) {
assert.ok(!servers.includes(name), `${name} was retired from the default set (June 2026 audit) — it lives in mcp-configs/mcp-servers.json as opt-in; see docs/MCP-CONNECTOR-POLICY.md`);
}
});
// ── Codex marketplace file ────────────────────────────────────────────────────
// Per official docs: repo marketplace lives at $REPO_ROOT/.agents/plugins/marketplace.json
console.log('\n=== .agents/plugins/marketplace.json ===\n');
const marketplacePath = path.join(repoRoot, '.agents', 'plugins', 'marketplace.json');
test('marketplace.json exists at .agents/plugins/', () => {
assert.ok(fs.existsSync(marketplacePath), 'Expected .agents/plugins/marketplace.json for Codex repo marketplace discovery');
});
const marketplace = loadJsonObject(marketplacePath, '.agents/plugins/marketplace.json');
const opencodePackage = loadJsonObject(opencodePackageJsonPath, '.opencode/package.json');
test('marketplace.json has name field', () => {
assert.ok(marketplace.name, 'Expected name field');
});
test('marketplace.json uses short marketplace slug', () => {
assert.strictEqual(marketplace.name, 'ecc');
});
test('marketplace.json has plugins array with at least one entry', () => {
assert.ok(Array.isArray(marketplace.plugins) && marketplace.plugins.length > 0, 'Expected plugins array');
});
test('marketplace.json plugin entries have required fields', () => {
for (const plugin of marketplace.plugins) {
assert.ok(plugin.name, `Plugin entry missing name`);
assert.ok(plugin.version, `Plugin "${plugin.name}" missing version`);
assert.ok(plugin.source && plugin.source.source, `Plugin "${plugin.name}" missing source.source`);
assert.ok(plugin.policy && plugin.policy.installation, `Plugin "${plugin.name}" missing policy.installation`);
assert.ok(plugin.category, `Plugin "${plugin.name}" missing category`);
}
});
test('marketplace.json plugin entry uses short plugin slug', () => {
assert.strictEqual(marketplace.plugins[0].name, 'ecc');
});
test('marketplace.json plugin version matches package.json', () => {
assert.strictEqual(marketplace.plugins[0].version, expectedVersion);
});
test('marketplace local plugin source is a self-contained native Codex bundle', () => {
// Codex 0.146.0 accepts the marketplace root as a plugin source and copies
// that source into its install cache. Parent-relative references from a thin
// subdirectory are broken after that copy, so every bundled path must remain
// inside the selected source root.
for (const plugin of marketplace.plugins) {
if (!plugin.source || plugin.source.source !== 'local') {
continue;
}
assert.ok(plugin.source.path.startsWith('./'), `Codex marketplace source.path must be ./-prefixed: ${plugin.source.path}`);
const sourceRoot = path.resolve(repoRoot, plugin.source.path);
assert.strictEqual(sourceRoot, repoRoot, `ECC's native Codex bundle must use the self-contained repository root, got: ${plugin.source.path}`);
const manifest = loadJsonObject(path.join(sourceRoot, '.codex-plugin', 'plugin.json'), 'marketplace Codex plugin manifest');
for (const field of ['skills', 'mcpServers', 'hooks']) {
assert.strictEqual(typeof manifest[field], 'string', `Expected Codex manifest ${field} path`);
const target = path.resolve(sourceRoot, manifest[field]);
assert.ok(target === sourceRoot || target.startsWith(sourceRoot + path.sep), `${field} escapes the installed source root: ${manifest[field]}`);
assert.ok(fs.existsSync(target), `${field} target is missing from the installed source root: ${manifest[field]}`);
}
assert.ok(fs.existsSync(path.join(sourceRoot, 'scripts', 'hooks', 'plugin-hook-bootstrap.js')), 'Codex hook runtime must ship inside the installed source root');
assert.ok(fs.existsSync(path.join(sourceRoot, 'skills', 'configure-ecc', 'SKILL.md')), 'Codex configure-ecc skill must ship inside the installed source root');
}
});
// ── plugins/ecc marketplace plugin folder ─────────────────────────────────────
// Thin Codex plugin target for the repo marketplace. Content is single-sourced
// at the repo root (no vendored skills/MCP copies) per the maintainer direction
// on #2097; these tests pin the manifest sync and the parent-relative refs.
console.log('\n=== plugins/ecc Codex marketplace plugin folder ===\n');
const marketplacePluginManifestPath = path.join(repoRoot, 'plugins', 'ecc', '.codex-plugin', 'plugin.json');
const marketplacePluginManifest = loadJsonObject(marketplacePluginManifestPath, 'plugins/ecc/.codex-plugin/plugin.json');
const rootCodexManifest = loadJsonObject(path.join(repoRoot, '.codex-plugin', 'plugin.json'), '.codex-plugin/plugin.json');
test('plugins/ecc manifest name matches the root Codex manifest', () => {
assert.strictEqual(marketplacePluginManifest.name, rootCodexManifest.name);
});
test('plugins/ecc manifest version matches package.json', () => {
assert.strictEqual(marketplacePluginManifest.version, expectedVersion);
});
test('plugins/ecc manifest version matches the root Codex manifest', () => {
assert.strictEqual(marketplacePluginManifest.version, rootCodexManifest.version);
});
test('plugins/ecc manifest reuses root skills and MCP config without vendoring', () => {
const pluginDir = path.dirname(path.dirname(marketplacePluginManifestPath));
const skillsTarget = path.resolve(pluginDir, marketplacePluginManifest.skills);
assert.strictEqual(skillsTarget, path.join(repoRoot, 'skills'), `skills ref must resolve to the root skills/ directory, got: ${marketplacePluginManifest.skills}`);
assert.ok(fs.existsSync(skillsTarget), 'Root skills/ directory missing');
const mcpTarget = path.resolve(pluginDir, marketplacePluginManifest.mcpServers);
assert.strictEqual(mcpTarget, path.join(repoRoot, '.mcp.json'), `mcpServers ref must resolve to the root .mcp.json, got: ${marketplacePluginManifest.mcpServers}`);
assert.ok(fs.existsSync(mcpTarget), 'Root .mcp.json missing');
assert.ok(!fs.existsSync(path.join(pluginDir, 'skills')), 'plugins/ecc must not vendor a second skills/ copy (see #2097 review)');
assert.ok(!fs.existsSync(path.join(pluginDir, '.mcp.json')), 'plugins/ecc must not vendor a second .mcp.json (see #2097 review)');
});
test('plugins/ecc manifest interface assets resolve to root assets', () => {
const pluginDir = path.dirname(path.dirname(marketplacePluginManifestPath));
for (const ref of [marketplacePluginManifest.interface.composerIcon, marketplacePluginManifest.interface.logo]) {
const target = path.resolve(pluginDir, ref);
assert.ok(target.startsWith(path.join(repoRoot, 'assets') + path.sep), `Asset ref must resolve under root assets/: ${ref}`);
assert.ok(fs.existsSync(target), `Asset ref target missing: ${ref}`);
}
});
test('plugins/ecc README marks the thin folder as a legacy compatibility artifact', () => {
const readmePath = path.join(repoRoot, 'plugins', 'ecc', 'README.md');
assert.ok(fs.existsSync(readmePath), 'Expected plugins/ecc/README.md');
const source = fs.readFileSync(readmePath, 'utf8');
assert.ok(source.includes('legacy compatibility artifact'));
assert.ok(source.includes('repository root'));
assert.ok(source.includes('check-plugin-cache.js'), 'plugins/ecc README must point at the cache health check');
assert.ok(!source.includes('points at this directory'));
});
test('.opencode/package.json version matches package.json', () => {
assert.strictEqual(opencodePackage.version, expectedVersion);
});
test('.opencode/package-lock.json root version matches package.json', () => {
assert.strictEqual(opencodePackageLock.version, expectedVersion);
assert.ok(opencodePackageLock.packages && opencodePackageLock.packages[''], 'Expected .opencode/package-lock root package entry');
assert.strictEqual(opencodePackageLock.packages[''].version, expectedVersion);
});
test('user-facing docs do not use overlong legacy marketplace install commands', () => {
const markdownFiles = [
path.join(repoRoot, 'README.md'),
path.join(repoRoot, 'README.zh-CN.md'),
path.join(repoRoot, 'skills', 'configure-ecc', 'SKILL.md'),
...collectMarkdownFiles(path.join(repoRoot, 'docs'))
].filter(filePath => !path.relative(repoRoot, filePath).startsWith(`docs${path.sep}drafts${path.sep}`));
const offenders = [];
for (const filePath of markdownFiles) {
const source = fs.readFileSync(filePath, 'utf8');
if (/\/plugin\s+(install|list)\s+everything-claude-code(?:@everything-claude-code)?\b/.test(source)) {
offenders.push(path.relative(repoRoot, filePath));
}
}
assert.deepStrictEqual(offenders, [], `Overlong legacy install commands must not appear in user-facing docs: ${offenders.join(', ')}`);
});
test('user-facing docs do not use the legacy non-URL marketplace add form', () => {
const markdownFiles = [path.join(repoRoot, 'README.md'), path.join(repoRoot, 'README.zh-CN.md'), ...collectMarkdownFiles(path.join(repoRoot, 'docs'))];
const offenders = [];
for (const filePath of markdownFiles) {
const source = fs.readFileSync(filePath, 'utf8');
if (source.includes('/plugin marketplace add affaan-m/everything-claude-code')) {
offenders.push(path.relative(repoRoot, filePath));
}
}
assert.deepStrictEqual(offenders, [], `Legacy non-URL marketplace add form must not appear in user-facing docs: ${offenders.join(', ')}`);
});
test('.codex-plugin README uses current marketplace add flow', () => {
const readme = fs.readFileSync(path.join(repoRoot, '.codex-plugin', 'README.md'), 'utf8');
assert.ok(readme.includes('codex plugin marketplace add'), 'Expected .codex-plugin README to document codex plugin marketplace add');
assert.ok(readme.includes('codex plugin marketplace add affaan-m/ECC'), 'Expected .codex-plugin README to document the canonical ECC repo marketplace source');
assert.ok(readme.includes('codex plugin add ecc@ecc'), 'Expected .codex-plugin README to document the current Codex install command');
assert.ok(readme.includes('codex plugin list --json'), 'Expected .codex-plugin README to document a machine-checkable verification command');
assert.ok(readme.includes('safe to run again'), 'Expected .codex-plugin README to explain idempotent marketplace and plugin registration');
assert.ok(/does not\s+use Claude's `user`, `project`, or `local` install scopes/.test(readme), 'Expected .codex-plugin README to distinguish Codex plugin state from Claude scopes');
assert.ok(readme.includes('review and trust'), 'Expected .codex-plugin README to explain Codex hook trust');
assert.ok(readme.includes('legacy managed sync'), 'Expected .codex-plugin README to distinguish native plugins from the legacy managed sync');
assert.ok(!/\bcodex plugin install\b/.test(readme), 'codex plugin install is not a current Codex CLI command');
});
test('docs/zh-CN/README.md version row matches package.json', () => {
const readme = fs.readFileSync(zhCnReadmePath, 'utf8');
const match = readme.match(new RegExp(`^\\| \\*\\*版本\\*\\* \\| 插件 \\| 插件 \\| 参考配置 \\| (${semverPattern}) \\|$`, 'm'));
assert.ok(match, 'Expected docs/zh-CN/README.md version summary row');
assert.strictEqual(match[1], expectedVersion);
});
// ── Summary ───────────────────────────────────────────────────────────────────
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);