* fix(#2602): discover project-local Codex agents - Select an existing local Codex agents directory before global fallback - Prove init reports the canonical local installation through compiled CJS * test(#2602): lock Codex agent precedence - Cover override, local authority, global fallback, and runtime compatibility - Exercise installed state through the compiled resolver * fix(#2602): resolve local Codex agent skills - Pass the canonical project root to the non-Claude persona fallback - Cover nested-Codex fallback and Claude compatibility through the CLI * test(#2602): cover local Codex validation status - Assert emitted validate and health commands use the project-local install - Preserve empty local-directory authority beside complete global agents * fix(#2602): align validation with local Codex discovery - Pass the resolved runtime and project root to health W010 - Resolve the validate-agents runtime before checking installation status * test(#2602): cover local Codex docs status - Assert docs-init reports an authoritative empty local install as unhealthy * fix(#2602): align docs with local Codex discovery - Pass the resolved runtime and canonical project root to the shared agent checker * fix(#2602): honor agent-skills runtime override - Resolve agent-skills fallback runtime through the canonical project resolver - Cover conflicting config and GSD_RUNTIME values through the emitted CLI * fix(#2602): ignore non-directory local agents paths - Treat only a local Codex agents directory as authoritative - Cover regular-file fallback through the emitted install checker * chore(#2602): add changelog fragment - record the user-visible local Codex agent discovery fix for PR #2623 * fix(#2602): align local agent discovery with runtime policy - Resolve Codex's local config directory through the canonical runtime policy - Use test-managed cleanup for local-agent discovery coverage * fix(#2602): discover local agents across runtimes - Prefer manifest-backed project-local installs for non-Claude runtimes - Respect runtime-specific local install roots and preserve global fallback behavior - Cover native, partial, cross-runtime, and project-root local discovery * fix(#2602): preserve agent discovery fallback - Fall back globally when local-install probes fail - Document and test symlink rejection - Align the changeset with repository format * fix(#2602): reuse local directory policy - Resolve runtimes without local config through the canonical sentinel - Document the manifest gate and refresh the context index --------- Co-authored-by: Daniel E. <daniel.e@teachingstrategies.com> Co-authored-by: Rezolv <dave@sienkowski.com>
This commit is contained in:
committed by
GitHub
parent
7b204ad2ac
commit
f0ff23635e
@@ -31,6 +31,7 @@ const RUNTIME_HOMES_PATH = path.join(
|
||||
|
||||
const agentInstallCheck = require(AGENT_INSTALL_CHECK_PATH);
|
||||
const { getGlobalConfigDir } = require(RUNTIME_HOMES_PATH);
|
||||
const { getDirName } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'runtime-name-policy.cjs'));
|
||||
|
||||
// Get EXPECTED_AGENTS from model-profiles (same source of truth)
|
||||
const MODEL_PROFILES = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'model-profiles.cjs')).MODEL_PROFILES;
|
||||
@@ -40,12 +41,15 @@ const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES);
|
||||
|
||||
let savedAgentsDir;
|
||||
let savedRuntime;
|
||||
let savedCodexHome;
|
||||
|
||||
beforeEach(() => {
|
||||
savedAgentsDir = process.env['GSD_AGENTS_DIR'];
|
||||
savedRuntime = process.env['GSD_RUNTIME'];
|
||||
savedCodexHome = process.env['CODEX_HOME'];
|
||||
delete process.env['GSD_AGENTS_DIR'];
|
||||
delete process.env['GSD_RUNTIME'];
|
||||
delete process.env['CODEX_HOME'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -59,8 +63,34 @@ afterEach(() => {
|
||||
} else {
|
||||
process.env['GSD_RUNTIME'] = savedRuntime;
|
||||
}
|
||||
if (savedCodexHome === undefined) {
|
||||
delete process.env['CODEX_HOME'];
|
||||
} else {
|
||||
process.env['CODEX_HOME'] = savedCodexHome;
|
||||
}
|
||||
});
|
||||
|
||||
function createCompleteAgents(agentsDir) {
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
for (const agent of EXPECTED_AGENTS) {
|
||||
fs.writeFileSync(path.join(agentsDir, `${agent}.toml`), `name = "${agent}"\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function markLocalGsdInstall(configDir) {
|
||||
fs.writeFileSync(
|
||||
path.join(configDir, 'gsd-file-manifest.json'),
|
||||
JSON.stringify({ files: {} }),
|
||||
);
|
||||
}
|
||||
|
||||
function createCompleteLocalGsdInstall(configDir) {
|
||||
const agentsDir = path.join(configDir, 'agents');
|
||||
createCompleteAgents(agentsDir);
|
||||
markLocalGsdInstall(configDir);
|
||||
return agentsDir;
|
||||
}
|
||||
|
||||
// ─── 1. getAgentsDir behaviour ────────────────────────────────────────────────
|
||||
|
||||
describe('getAgentsDir', () => {
|
||||
@@ -94,6 +124,198 @@ describe('getAgentsDir', () => {
|
||||
const fromClaude = agentInstallCheck.getAgentsDir('claude');
|
||||
assert.strictEqual(fromModule, fromClaude);
|
||||
});
|
||||
|
||||
test('a manifest-backed local runtime installation wins over global agents', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
const localAgentsDir = createCompleteLocalGsdInstall(path.join(projectRoot, '.codex'));
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
|
||||
assert.strictEqual(agentInstallCheck.getAgentsDir('codex', projectRoot), localAgentsDir);
|
||||
assert.strictEqual(agentInstallCheck.checkAgentsInstalled('codex', projectRoot).agents_installed, true);
|
||||
});
|
||||
|
||||
test('GSD_AGENTS_DIR remains terminal when a local Codex installation exists', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const overrideDir = path.join(projectRoot, 'override-agents');
|
||||
createCompleteLocalGsdInstall(path.join(projectRoot, '.codex'));
|
||||
t.after(() => cleanup(projectRoot));
|
||||
fs.mkdirSync(overrideDir, { recursive: true });
|
||||
process.env['GSD_AGENTS_DIR'] = overrideDir;
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, overrideDir);
|
||||
assert.strictEqual(result.agents_installed, false);
|
||||
assert.deepStrictEqual(result.missing_agents, EXPECTED_AGENTS);
|
||||
});
|
||||
|
||||
test('a manifest-backed empty local directory is authoritative over complete global agents', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
const localAgentsDir = path.join(projectRoot, '.codex', 'agents');
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
fs.mkdirSync(localAgentsDir, { recursive: true });
|
||||
markLocalGsdInstall(path.dirname(localAgentsDir));
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, localAgentsDir);
|
||||
assert.strictEqual(result.agents_installed, false);
|
||||
assert.deepStrictEqual(result.missing_agents, EXPECTED_AGENTS);
|
||||
});
|
||||
|
||||
test('Codex falls back to global agents when no local directory exists', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
|
||||
assert.strictEqual(result.agents_installed, true);
|
||||
});
|
||||
|
||||
test('Codex falls back to global agents when the local candidate is a regular file', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
const localCandidate = path.join(projectRoot, '.codex', 'agents');
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
fs.mkdirSync(path.dirname(localCandidate), { recursive: true });
|
||||
fs.writeFileSync(localCandidate, 'not an agents directory\n');
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
|
||||
assert.strictEqual(result.agents_installed, true);
|
||||
});
|
||||
|
||||
test('Codex falls back to global agents when the local candidate cannot be inspected', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
const localAgentsDir = path.join(projectRoot, '.codex', 'agents');
|
||||
const realLstatSync = fs.lstatSync;
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
fs.mkdirSync(localAgentsDir, { recursive: true });
|
||||
markLocalGsdInstall(path.dirname(localAgentsDir));
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
t.mock.method(fs, 'lstatSync', function injectedLocalProbeFailure(target, ...args) {
|
||||
if (target === localAgentsDir) {
|
||||
throw Object.assign(new Error('injected EACCES'), { code: 'EACCES' });
|
||||
}
|
||||
return realLstatSync.call(fs, target, ...args);
|
||||
});
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
|
||||
assert.strictEqual(result.agents_installed, true);
|
||||
});
|
||||
|
||||
test('Codex does not follow a symlinked local agents directory', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
const localConfigDir = path.join(projectRoot, '.codex');
|
||||
const localAgentsDir = path.join(localConfigDir, 'agents');
|
||||
const symlinkTarget = path.join(projectRoot, 'shared-agents');
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
createCompleteAgents(symlinkTarget);
|
||||
fs.mkdirSync(localConfigDir, { recursive: true });
|
||||
markLocalGsdInstall(localConfigDir);
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
try {
|
||||
fs.symlinkSync(symlinkTarget, localAgentsDir, process.platform === 'win32' ? 'junction' : 'dir');
|
||||
} catch (error) {
|
||||
if (error && ['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) {
|
||||
t.skip('symlink creation is not available on this platform');
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
|
||||
assert.strictEqual(result.agents_installed, true);
|
||||
});
|
||||
|
||||
test('a project-native agents directory without a GSD manifest does not override global agents', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const globalHome = createTempDir('gsd-global-codex-');
|
||||
const localAgentsDir = path.join(projectRoot, '.codex', 'agents');
|
||||
t.after(() => cleanup(projectRoot));
|
||||
t.after(() => cleanup(globalHome));
|
||||
createCompleteAgents(localAgentsDir);
|
||||
createCompleteAgents(path.join(globalHome, 'agents'));
|
||||
process.env['CODEX_HOME'] = globalHome;
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
|
||||
assert.strictEqual(result.agents_installed, true);
|
||||
});
|
||||
|
||||
test('a manifest-backed Cursor installation resolves from the project root', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-cursor-');
|
||||
const localAgentsDir = createCompleteLocalGsdInstall(path.join(projectRoot, getDirName('cursor')));
|
||||
t.after(() => cleanup(projectRoot));
|
||||
|
||||
assert.strictEqual(agentInstallCheck.getAgentsDir('cursor', projectRoot), localAgentsDir);
|
||||
assert.strictEqual(agentInstallCheck.checkAgentsInstalled('cursor', projectRoot).agents_installed, true);
|
||||
});
|
||||
|
||||
test('a manifest-backed Cline installation resolves from the project root', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-cline-');
|
||||
const localAgentsDir = createCompleteLocalGsdInstall(projectRoot);
|
||||
t.after(() => cleanup(projectRoot));
|
||||
|
||||
assert.strictEqual(agentInstallCheck.getAgentsDir('cline', projectRoot), localAgentsDir);
|
||||
assert.strictEqual(agentInstallCheck.checkAgentsInstalled('cline', projectRoot).agents_installed, true);
|
||||
});
|
||||
|
||||
test('a partial manifest-backed local installation remains selected and incomplete', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
const localConfigDir = path.join(projectRoot, '.codex');
|
||||
const localAgentsDir = createCompleteLocalGsdInstall(localConfigDir);
|
||||
const partialAgent = EXPECTED_AGENTS[0];
|
||||
t.after(() => cleanup(projectRoot));
|
||||
fs.writeFileSync(path.join(localAgentsDir, `${partialAgent}.md`), `# ${partialAgent}\n`);
|
||||
fs.unlinkSync(path.join(localAgentsDir, `${partialAgent}.toml`));
|
||||
fs.writeFileSync(
|
||||
path.join(localConfigDir, 'gsd-file-manifest.json'),
|
||||
JSON.stringify({ files: { [`agents/${partialAgent}.md`]: {}, [`agents/${partialAgent}.toml`]: {} } }),
|
||||
);
|
||||
|
||||
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
|
||||
assert.strictEqual(result.agents_dir, localAgentsDir);
|
||||
assert.strictEqual(result.agents_installed, false);
|
||||
assert.deepStrictEqual(result.missing_agents, []);
|
||||
assert.deepStrictEqual(result.incomplete_agents, [partialAgent]);
|
||||
});
|
||||
|
||||
test('Claude and other runtimes ignore a supplied Codex-local candidate', (t) => {
|
||||
const projectRoot = createTempDir('gsd-local-codex-');
|
||||
t.after(() => cleanup(projectRoot));
|
||||
createCompleteLocalGsdInstall(path.join(projectRoot, '.codex'));
|
||||
|
||||
assert.strictEqual(
|
||||
agentInstallCheck.getAgentsDir('claude', projectRoot),
|
||||
agentInstallCheck.getAgentsDir('claude'),
|
||||
);
|
||||
assert.strictEqual(
|
||||
agentInstallCheck.getAgentsDir('cursor', projectRoot),
|
||||
path.join(getGlobalConfigDir('cursor'), 'agents'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 2. checkAgentsInstalled behaviour ───────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user