Deduplicate Codex skill commands (#992)

This commit is contained in:
Mohamed Boudra
2026-05-13 13:58:31 +00:00
committed by GitHub
parent 316eeee9ca
commit e96cd16a0f
2 changed files with 90 additions and 7 deletions
@@ -11,6 +11,7 @@ import type {
AgentLaunchContext,
AgentSession,
AgentSessionConfig,
AgentSlashCommand,
AgentStreamEvent,
} from "../agent-sdk-types.js";
import {
@@ -199,6 +200,63 @@ function capturedThreadStartConfig(records: CapturedFakeCodexRecord[]): unknown
return params?.config;
}
async function listCommandsFromFakeCodex(skills: unknown[]): Promise<AgentSlashCommand[]> {
const tempDir = await mkdtemp(path.join(tmpdir(), "codex-command-list-"));
const fakeCodexPath = path.join(tempDir, "fake-codex.cjs");
writeFileSync(
fakeCodexPath,
`
let buffer = "";
function resultFor(method) {
if (method === "initialize") return {};
if (method === "collaborationMode/list") return { data: [] };
if (method === "skills/list") {
return {
data: [
{
cwd: "/tmp/codex-question-test",
skills: ${JSON.stringify(skills)},
errors: [],
},
],
};
}
throw new Error("Unexpected Codex request: " + method);
}
process.stdin.on("data", (chunk) => {
buffer += chunk.toString();
for (;;) {
const newlineIndex = buffer.indexOf("\\n");
if (newlineIndex === -1) break;
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (!line) continue;
const message = JSON.parse(line);
if (typeof message.id !== "number") continue;
try {
process.stdout.write(JSON.stringify({ id: message.id, result: resultFor(message.method) }) + "\\n");
} catch (error) {
process.stdout.write(JSON.stringify({ id: message.id, error: { message: error.message } }) + "\\n");
}
}
});
`,
);
const client = new CodexAppServerAgentClient(createTestLogger(), {
command: { mode: "replace", argv: [process.execPath, fakeCodexPath] },
});
const session = await client.createSession(createConfig());
try {
return await session.listCommands();
} finally {
await session.close();
rmSync(tempDir, { recursive: true, force: true });
}
}
describe("Codex app-server provider", () => {
test("getAvailableModes includes auto-review when the Codex version supports it", async () => {
const session = createSession({}, { autoReviewEnabled: true });
@@ -824,6 +882,29 @@ describe("Codex app-server provider", () => {
);
});
test("deduplicates Codex skill slash commands returned from multiple skill roots", async () => {
const commands = await listCommandsFromFakeCodex([
{
name: "paseo",
description: "Shared orchestration skill.",
path: "/Users/test/.agents/skills/paseo/SKILL.md",
},
{
name: "paseo",
description: "Shared orchestration skill.",
path: "/Users/test/.codex/skills/paseo/SKILL.md",
},
]);
expect(commands.filter((command) => command.name === "paseo")).toEqual([
{
name: "paseo",
description: "Shared orchestration skill.",
argumentHint: "",
},
]);
});
test("maps image prompt blocks to Codex localImage input", async () => {
const input = await codexAppServerTurnInputFromPrompt(
[
@@ -2852,7 +2852,7 @@ class CodexAppServerAgentSession implements AgentSession {
}),
);
const entries = Array.isArray(response?.data) ? response.data : [];
const skills: Array<{ name: string; description: string; path: string }> = [];
const skillsByName = new Map<string, { name: string; description: string; path: string }>();
for (const entry of entries) {
const entryRecord = toObjectRecord(entry);
const list = Array.isArray(entryRecord?.skills) ? entryRecord.skills : [];
@@ -2860,14 +2860,16 @@ class CodexAppServerAgentSession implements AgentSession {
const skillRecord = toObjectRecord(skill);
if (typeof skillRecord?.name !== "string" || typeof skillRecord?.path !== "string")
continue;
skills.push({
name: skillRecord.name,
description: resolveSkillDescription(skillRecord),
path: skillRecord.path,
});
if (!skillsByName.has(skillRecord.name)) {
skillsByName.set(skillRecord.name, {
name: skillRecord.name,
description: resolveSkillDescription(skillRecord),
path: skillRecord.path,
});
}
}
}
this.cachedSkills = skills;
this.cachedSkills = Array.from(skillsByName.values());
} catch (error) {
this.logger.trace(
{