diff --git a/packages/omo-codex/plugin/test/aggregate.test.mjs b/packages/omo-codex/plugin/test/aggregate.test.mjs new file mode 100644 index 000000000..2f58c0bf3 --- /dev/null +++ b/packages/omo-codex/plugin/test/aggregate.test.mjs @@ -0,0 +1,329 @@ +import assert from "node:assert/strict"; +import { readdir, readFile, stat } from "node:fs/promises"; +import { basename, dirname, join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); + +async function readJson(relativePath) { + return JSON.parse(await readFile(join(root, relativePath), "utf8")); +} + +async function readComponentHookManifests() { + const components = await readdir(join(root, "components"), { withFileTypes: true }); + const manifests = []; + for (const entry of components) { + if (!entry.isDirectory()) continue; + const source = join("components", entry.name, "hooks", "hooks.json"); + manifests.push({ source, hooks: await readJson(source) }); + } + return manifests.sort((left, right) => left.source.localeCompare(right.source)); +} + +function collectCommandHooks(hooks, source) { + const config = hooks.hooks; + if (typeof config !== "object" || config === null || Array.isArray(config)) { + throw new TypeError(`Invalid hooks manifest: ${source}`); + } + const commandHooks = []; + for (const [eventName, groups] of Object.entries(config)) { + if (!Array.isArray(groups)) { + throw new TypeError(`Invalid hook groups in ${source}:${eventName}`); + } + groups.forEach((group, groupIndex) => { + if (typeof group !== "object" || group === null || !Array.isArray(group.hooks)) { + throw new TypeError(`Invalid hook group in ${source}:${eventName}:${groupIndex}`); + } + group.hooks.forEach((handler, handlerIndex) => { + if (typeof handler !== "object" || handler === null || handler.type !== "command") return; + commandHooks.push({ source, eventName, groupIndex, handlerIndex, handler }); + }); + }); + } + return commandHooks; +} + +function hookLocation({ source, eventName, groupIndex, handlerIndex, handler }) { + return `${source}:${eventName}:${groupIndex}:${handlerIndex}:${handler.command}`; +} + +function findSpawnAgentTypes(content) { + const agentTypes = new Set(); + const regex = /spawn_agent\(agent_type="([^"]+)"/g; + for (const match of content.matchAll(regex)) { + agentTypes.add(match[1]); + } + return [...agentTypes].sort(); +} + +test("#given aggregate plugin manifest #when inspected #then it owns the omo namespace", async () => { + // given + const manifest = await readJson(".codex-plugin/plugin.json"); + + // when + const hookPath = manifest.hooks; + const skillsPath = manifest.skills; + const mcpPath = manifest.mcpServers; + + // then + assert.equal(manifest.name, "omo"); + assert.equal(hookPath, "./hooks/hooks.json"); + assert.equal(skillsPath, "./skills/"); + assert.equal(mcpPath, "./.mcp.json"); +}); + +test("#given aggregate plugin metadata #when inspected #then ulw-loop is the public loop name", async () => { + // given + const manifestText = await readFile(join(root, ".codex-plugin", "plugin.json"), "utf8"); + const manifest = JSON.parse(manifestText); + + // when + const longDescription = String(manifest.interface?.longDescription ?? ""); + + // then + assert.match(longDescription, /ulw-loop/); +}); + +test("#given isolated components #when hooks are inspected #then commands stay inside component roots", async () => { + // given + const hooks = await readJson("hooks/hooks.json"); + const text = JSON.stringify(hooks); + + // when + const componentMarkers = [ + "components/comment-checker/dist/cli.js", + "components/lsp/dist/cli.js", + "components/rules/dist/cli.js", + "components/start-work-continuation/dist/cli.js", + "components/telemetry/dist/cli.js", + "components/ulw-loop/dist/cli.js", + "components/ultrawork/dist/cli.js", + ]; + + // then + for (const marker of componentMarkers) { + assert.match(text, new RegExp(marker.replaceAll("/", "\\/"))); + } + assert.doesNotMatch(text, /codex-(comment-checker|lsp|rules|telemetry|ulw-loop|ultrawork)@/); +}); + +test("#given aggregate hook commands #when inspected #then every command exposes a Codex status message", async () => { + // given + const hooks = await readJson("hooks/hooks.json"); + + // when + const commandHooks = collectCommandHooks(hooks, "hooks/hooks.json"); + const missingStatusMessages = commandHooks + .filter(({ handler }) => typeof handler.statusMessage !== "string" || handler.statusMessage.trim() === "") + .map(hookLocation); + + // then + assert.deepEqual(missingStatusMessages, []); +}); + +test("#given component hook commands #when inspected #then standalone packages expose Codex status messages", async () => { + // given + const componentHooks = await readComponentHookManifests(); + + // when + const missingStatusMessages = componentHooks + .flatMap(({ source, hooks }) => collectCommandHooks(hooks, source)) + .filter(({ handler }) => typeof handler.statusMessage !== "string" || handler.statusMessage.trim() === "") + .map(hookLocation); + + // then + assert.deepEqual(missingStatusMessages, []); +}); + +test("#given hook status messages #when inspected #then labels describe OMO responsibilities instead of the hook runner", async () => { + // given + const aggregateHooks = await readJson("hooks/hooks.json"); + const componentHooks = await readComponentHookManifests(); + + // when + const commandHooks = [ + ...collectCommandHooks(aggregateHooks, "hooks/hooks.json"), + ...componentHooks.flatMap(({ source, hooks }) => collectCommandHooks(hooks, source)), + ]; + const genericStatusMessages = commandHooks + .filter(({ handler }) => typeof handler.statusMessage !== "string" || /\bhook\b/i.test(handler.statusMessage)) + .map(hookLocation); + + // then + assert.deepEqual(genericStatusMessages, []); +}); + +test("#given aggregate OMO plugin is enabled #when hooks are inspected #then ulw-loop guards budgeted create_goal calls", async () => { + // given + const hooks = await readJson("hooks/hooks.json"); + const text = JSON.stringify(hooks); + + // when + const preToolUseGroups = hooks.hooks.PreToolUse; + + // then + assert.match(text, /components\/ulw-loop\/dist\/cli\.js/); + assert.match(text, /hook pre-tool-use/); + assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^create_goal$"]); +}); + +test("#given aggregate MCP config #when inspected #then code MCPs reference package runtimes without package names", async () => { + // given + const packageJson = await readJson("package.json"); + const mcp = await readJson(".mcp.json"); + const lspSources = await readdir(join(root, "components", "lsp", "src")); + + // when + const lspServer = mcp.mcpServers.lsp; + const astGrepServer = mcp.mcpServers.ast_grep; + const codeMcpNames = Object.keys(mcp.mcpServers) + .filter((name) => name === "lsp" || name === "ast_grep") + .sort(); + const componentLocalMcpSources = lspSources.filter((name) => name.startsWith("lazy-mcp") || name === "lazy-lsp-mcp.ts"); + + // then + assert.deepEqual(codeMcpNames, ["ast_grep", "lsp"]); + assert.equal(packageJson.workspaces.includes("components/lsp/packages/lsp-tools-mcp"), false); + assert.equal(packageJson.workspaces.includes("components/ast-grep/packages/ast-grep-mcp"), false); + assert.deepEqual(packageJson.dependencies, { "@oh-my-opencode/shared-skills": "file:../../shared-skills" }); + assert.match(packageJson.scripts.build, /ast-grep-mcp/); + assert.doesNotMatch(packageJson.scripts.build, /--workspaces/); + assert.equal(lspServer.command, "node"); + assert.deepEqual(lspServer.args, ["../../lsp-tools-mcp/dist/cli.js", "mcp"]); + assert.equal(lspServer.cwd, "."); + assert.equal(astGrepServer.command, "node"); + assert.deepEqual(astGrepServer.args, ["../../ast-grep-mcp/dist/cli.js", "mcp"]); + assert.equal(astGrepServer.cwd, "."); + assert.deepEqual(componentLocalMcpSources, []); +}); + +test("#given package-level MCP CLIs #when package metadata is inspected #then bin names use the omo prefix", async () => { + // given + const lspPackageJson = await readJson("../../lsp-tools-mcp/package.json"); + const astGrepPackageJson = await readJson("../../ast-grep-mcp/package.json"); + + // when + const binNames = [...Object.keys(lspPackageJson.bin ?? {}), ...Object.keys(astGrepPackageJson.bin ?? {})].sort(); + + // then + assert.deepEqual(binNames, ["omo-ast-grep", "omo-lsp"]); + for (const name of binNames) { + assert.match(name, /^omo-/); + } +}); + +test("#given aggregate plugin build script #when inspected #then telemetry sync runs before workspace builds", async () => { + // given + const packageJson = await readJson("package.json"); + const telemetrySyncScript = await readFile(join(root, "..", "scripts", "sync-telemetry-component.mjs"), "utf8"); + + // when + const buildScript = packageJson.scripts.build; + + // then + assert.equal( + buildScript, + "bun run --cwd ../../lsp-tools-mcp build && bun run --cwd ../../ast-grep-mcp build && node scripts/sync-skills.mjs && node ../scripts/sync-telemetry-component.mjs && node scripts/build-components.mjs", + ); + assert.match(telemetrySyncScript, /syncTelemetryComponent/); +}); + +test("#given omo-codex package build script #when inspected #then delegates to the aggregate plugin package", async () => { + // given + const packageJson = JSON.parse(await readFile(join(root, "..", "package.json"), "utf8")); + + // when + const buildPluginScript = packageJson.scripts["build:plugin"]; + + // then + assert.equal(buildPluginScript, "bun run --cwd plugin build"); +}); + +test("#given component directories #when scanned #then only intentional resource roots declare plugin manifests", async () => { + // given + const components = await readdir(join(root, "components"), { withFileTypes: true }); + const expectedComponentManifests = new Map([["rules", { hooks: "./hooks/hooks.json" }]]); + + // when + const componentNames = components.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort(); + + // then + assert.deepEqual(componentNames, [ + "comment-checker", + "lsp", + "rules", + "start-work-continuation", + "telemetry", + "ultrawork", + "ulw-loop", + ]); + for (const name of componentNames) { + const expectedManifest = expectedComponentManifests.get(name); + if (expectedManifest !== undefined) { + assert.deepEqual(await readJson(join("components", name, ".codex-plugin", "plugin.json")), expectedManifest); + continue; + } + + await assert.rejects( + readFile(join(root, "components", name, ".codex-plugin", "plugin.json"), "utf8"), + /code: 'ENOENT'|ENOENT/, + ); + } +}); + +test("#given bundled Codex agents #when components/ultrawork/agents directory is scanned #then planner support TOMLs are present and match expected schema keys", async () => { + const agentsDir = join(root, "components", "ultrawork", "agents"); + const entries = (await readdir(agentsDir, { withFileTypes: true })) + .filter((entry) => entry.isFile() && entry.name.endsWith(".toml")) + .map((entry) => entry.name) + .sort(); + + assert.deepEqual(entries, [ + "codex-ultrawork-reviewer.toml", + "explorer.toml", + "librarian.toml", + "metis.toml", + "momus.toml", + "plan.toml", + ]); + + for (const fileName of entries) { + const content = await readFile(join(agentsDir, fileName), "utf8"); + assert.match(content, /^name\s*=\s*".+"$/m); + assert.match(content, /^description\s*=\s*".+"$/m); + assert.match(content, /^nickname_candidates\s*=\s*\[.+\]$/m); + assert.match(content, /^model\s*=\s*".+"$/m); + assert.match(content, /^model_reasoning_effort\s*=\s*".+"$/m); + assert.match(content, /^developer_instructions\s*=\s*"""/m); + } +}); + +test("#given synced skills with Codex compatibility guidance #when a bundled agent_type is referenced #then a matching TOML is bundled", async () => { + const skillsDir = join(root, "skills"); + const skillEntries = await readdir(skillsDir, { withFileTypes: true }); + const skillFiles = skillEntries + .filter((entry) => entry.isDirectory()) + .map((entry) => join(skillsDir, entry.name, "SKILL.md")); + + const referencedAgentTypes = new Set(); + for (const skillPath of skillFiles) { + const content = await readFile(skillPath, "utf8"); + for (const agentType of findSpawnAgentTypes(content)) { + if (agentType === "worker" || agentType === "codex-ultrawork-reviewer") { + continue; + } + referencedAgentTypes.add(agentType); + } + } + + const expected = [...referencedAgentTypes].sort(); + assert.deepEqual(expected, ["explorer", "librarian", "metis", "momus", "plan"]); + + for (const agentType of expected) { + const tomlPath = join(root, "components", "ultrawork", "agents", `${agentType}.toml`); + const fileStat = await stat(tomlPath); + assert.equal(fileStat.isFile(), true); + assert.equal(basename(tomlPath), `${agentType}.toml`); + } +}); diff --git a/packages/omo-codex/plugin/test/component-bin-names.test.mjs b/packages/omo-codex/plugin/test/component-bin-names.test.mjs new file mode 100644 index 000000000..0af913c4b --- /dev/null +++ b/packages/omo-codex/plugin/test/component-bin-names.test.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); + +const EXPECTED_COMPONENT_BINS = new Map([ + ["comment-checker", "omo-comment-checker"], + ["lsp", "omo-lsp"], + ["rules", "omo-rules"], + ["start-work-continuation", "omo-start-work-continuation"], + ["telemetry", "omo-telemetry"], + ["ultrawork", "omo-ultrawork"], + ["ulw-loop", "omo"], +]); + +const EXPECTED_USAGE_PREFIXES = new Map([ + ["comment-checker", "Usage: omo-comment-checker "], + ["lsp", "Usage: omo-lsp "], + ["rules", "Usage: omo-rules "], + ["start-work-continuation", "Usage: omo-start-work-continuation "], + ["telemetry", "Usage: omo-telemetry "], + ["ultrawork", "Usage: omo-ultrawork "], +]); + +async function readJson(relativePath) { + return JSON.parse(await readFile(join(root, relativePath), "utf8")); +} + +test("#given aggregate component package metadata #when bin names are inspected #then local component CLIs use the OMO prefix", async () => { + // given + const components = [...EXPECTED_COMPONENT_BINS.entries()]; + + // when + const mismatches = []; + for (const [component, expectedName] of components) { + const packageJson = await readJson(join("components", component, "package.json")); + const bin = packageJson.bin ?? {}; + const binNames = Object.keys(bin).sort(); + if (bin[expectedName] !== "./dist/cli.js" || binNames.some((name) => name.startsWith("codex-"))) { + mismatches.push({ component, expectedName, bin }); + } + } + + // then + assert.deepEqual(mismatches, []); +}); + +test("#given component CLI sources #when usage text is inspected #then user-facing command names use OMO names", async () => { + // given + const components = [...EXPECTED_USAGE_PREFIXES.entries()]; + + // when + const mismatches = []; + for (const [component, expectedUsage] of components) { + const source = await readFile(join(root, "components", component, "src", "cli.ts"), "utf8"); + if (!source.includes(expectedUsage) || /Usage: codex-/.test(source)) { + mismatches.push({ component, expectedUsage }); + } + } + + // then + assert.deepEqual(mismatches, []); +}); diff --git a/packages/omo-codex/plugin/test/hook-status-message.test.mjs b/packages/omo-codex/plugin/test/hook-status-message.test.mjs new file mode 100644 index 000000000..961c56b0f --- /dev/null +++ b/packages/omo-codex/plugin/test/hook-status-message.test.mjs @@ -0,0 +1,149 @@ +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { + formatLazyCodexHookStatusMessage, + normalizeLazyCodexHookStatusLabel, + parseLazyCodexHookStatusMessage, +} from "../scripts/hook-status-message.mjs"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); + +const AGGREGATE_EXPECTED_LABELS = new Map([ + ["hooks/hooks.json:SessionStart:0:0", "Loading Project Rules"], + ["hooks/hooks.json:SessionStart:1:0", "Recording Session Telemetry"], + ["hooks/hooks.json:UserPromptSubmit:0:0", "Loading Project Rules"], + ["hooks/hooks.json:UserPromptSubmit:1:0", "Checking Ultrawork Trigger"], + ["hooks/hooks.json:UserPromptSubmit:2:0", "Checking Ulw-Loop Steering"], + ["hooks/hooks.json:PreToolUse:0:0", "Enforcing Unlimited Goal Budget"], + ["hooks/hooks.json:PostToolUse:0:0", "Checking Comments"], + ["hooks/hooks.json:PostToolUse:0:1", "Checking LSP Diagnostics"], + ["hooks/hooks.json:PostToolUse:1:0", "Matching Project Rules"], + ["hooks/hooks.json:PostCompact:0:0", "Resetting Project Rule Cache"], + ["hooks/hooks.json:Stop:0:0", "Checking Start-Work Continuation"], + ["hooks/hooks.json:SubagentStop:0:0", "Checking Start-Work Continuation"], +]); + +const COMPONENT_EXPECTED_LABELS = new Map([ + ["components/comment-checker/hooks/hooks.json:PostToolUse:0:0", "Checking Comments"], + ["components/lsp/hooks/hooks.json:PostToolUse:0:0", "Checking LSP Diagnostics"], + ["components/rules/hooks/hooks.json:SessionStart:0:0", "Loading Project Rules"], + ["components/rules/hooks/hooks.json:UserPromptSubmit:0:0", "Loading Project Rules"], + ["components/rules/hooks/hooks.json:PostToolUse:0:0", "Matching Project Rules"], + ["components/rules/hooks/hooks.json:PostCompact:0:0", "Resetting Project Rule Cache"], + ["components/telemetry/hooks/hooks.json:SessionStart:0:0", "Recording Session Telemetry"], + ["components/ultrawork/hooks/hooks.json:UserPromptSubmit:0:0", "Checking Ultrawork Trigger"], + ["components/ulw-loop/hooks/hooks.json:UserPromptSubmit:0:0", "Checking Ulw-Loop Steering"], + ["components/ulw-loop/hooks/hooks.json:PreToolUse:0:0", "Enforcing Unlimited Ulw-Loop Budget"], + ["components/start-work-continuation/hooks/hooks.json:Stop:0:0", "Checking Start-Work Continuation"], + ["components/start-work-continuation/hooks/hooks.json:SubagentStop:0:0", "Checking Start-Work Continuation"], +]); + +async function readJson(relativePath) { + return JSON.parse(await readFile(join(root, relativePath), "utf8")); +} + +async function readComponentHookManifests() { + const components = await readdir(join(root, "components"), { withFileTypes: true }); + const manifests = []; + for (const entry of components) { + if (!entry.isDirectory()) continue; + const source = join("components", entry.name, "hooks", "hooks.json"); + const packageJson = await readJson(join("components", entry.name, "package.json")); + manifests.push({ source, version: packageJson.version, hooks: await readJson(source) }); + } + return manifests.sort((left, right) => left.source.localeCompare(right.source)); +} + +function collectCommandHooks(hooks, source, version) { + const commandHooks = []; + for (const [eventName, groups] of Object.entries(hooks.hooks)) { + groups.forEach((group, groupIndex) => { + group.hooks.forEach((handler, handlerIndex) => { + if (handler.type !== "command") return; + commandHooks.push({ + id: `${source}:${eventName}:${groupIndex}:${handlerIndex}`, + version, + statusMessage: handler.statusMessage, + }); + }); + }); + } + return commandHooks; +} + +test("#given hook status label #when formatting #then prefixes LazyCodex with version", () => { + // given + const version = "0.1.0"; + const label = "Checking Comments"; + + // when + const message = formatLazyCodexHookStatusMessage(version, label); + + // then + assert.equal(message, "LazyCodex(0.1.0): Checking Comments"); +}); + +test("#given loose legacy status label #when normalizing #then removes OMO wording and title-cases label", () => { + // given + const version = "0.1.0"; + const label = " checking OMO comments "; + + // when + const normalized = normalizeLazyCodexHookStatusLabel(label); + const message = formatLazyCodexHookStatusMessage(version, label); + + // then + assert.equal(normalized, "Checking Comments"); + assert.equal(message, "LazyCodex(0.1.0): Checking Comments"); +}); + +test("#given aggregate comment-checker hook #when status is inspected #then it uses LazyCodex comments label", async () => { + // given + const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version; + const aggregateHooks = await readJson("hooks/hooks.json"); + + // when + const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion); + const commentCheckerHook = hooks.find((hook) => hook.id === "hooks/hooks.json:PostToolUse:0:0"); + + // then + assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage("0.1.0", "Checking Comments")); + assert.doesNotMatch(JSON.stringify(aggregateHooks), /checking\s+OMO\s+comments/i); +}); + +test("#given aggregate and component hooks #when status messages are inspected #then all use the LazyCodex formatter", async () => { + // given + const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version; + const aggregateHooks = await readJson("hooks/hooks.json"); + const componentManifests = await readComponentHookManifests(); + + // when + const commandHooks = [ + ...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion), + ...componentManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)), + ]; + const expectedLabels = new Map([...AGGREGATE_EXPECTED_LABELS, ...COMPONENT_EXPECTED_LABELS]); + const mismatches = commandHooks + .map((hook) => { + const label = expectedLabels.get(hook.id); + const expected = label === undefined ? undefined : formatLazyCodexHookStatusMessage(hook.version, label); + const parsed = parseLazyCodexHookStatusMessage(hook.statusMessage); + return { ...hook, expected, parsed }; + }) + .filter((hook) => hook.expected === undefined || hook.statusMessage !== hook.expected || hook.parsed === null) + .map((hook) => `${hook.id}: expected ${hook.expected ?? ""} but got ${hook.statusMessage}`); + + // then + assert.deepEqual(mismatches, []); + assert.deepEqual( + commandHooks.map((hook) => hook.id).sort(), + [...expectedLabels.keys()].sort(), + ); + for (const hook of commandHooks) { + assert.doesNotMatch(hook.statusMessage, /\bOMO\b/i); + } +}); diff --git a/packages/omo-codex/plugin/test/sync-skills.test.mjs b/packages/omo-codex/plugin/test/sync-skills.test.mjs new file mode 100644 index 000000000..735460620 --- /dev/null +++ b/packages/omo-codex/plugin/test/sync-skills.test.mjs @@ -0,0 +1,178 @@ +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { sharedSkillsRootPath } from "@oh-my-opencode/shared-skills"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const repoRoot = join(root, "..", "..", ".."); + +const expectedSkills = [ + "comment-checker", + "debugging", + "frontend-ui-ux", + "init-deep", + "lsp", + "programming", + "refactor", + "remove-ai-slops", + "review-work", + "rules", + "start-work", + "ulw-loop", + "ulw-plan", +]; + +const componentSkillSources = [ + ["comment-checker", "components/comment-checker/skills/comment-checker"], + ["lsp", "components/lsp/skills/lsp"], + ["rules", "components/rules/skills/rules"], + ["ulw-loop", "components/ulw-loop/skills/ulw-loop"], +]; + +const codexCompatibilityEndMarkers = [ + "When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n", + "When translating `load_skills=[...]`, name the skills inside the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n", +]; + +function removeCodexCompatibilityGuidance(content) { + const start = content.indexOf("## Codex Harness Tool Compatibility\n\n"); + if (start === -1) return content; + const endMarker = codexCompatibilityEndMarkers.find((marker) => content.indexOf(marker, start) !== -1); + assert.notEqual(endMarker, undefined, "Codex compatibility guidance block is missing its terminator"); + const end = content.indexOf(endMarker, start); + assert.notEqual(end, -1, "Codex compatibility guidance block is missing its terminator"); + return `${content.slice(0, start)}${content.slice(end + endMarker.length)}`; +} + +test("#given synced aggregate Codex skills #when inspected #then component and shared skills are present", async () => { + // given + const skillsRoot = join(root, "skills"); + + // when + const skillNames = (await readdir(skillsRoot, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + // then + assert.deepEqual(skillNames, expectedSkills); + for (const skillName of expectedSkills) { + const content = await readFile(join(skillsRoot, skillName, "SKILL.md"), "utf8"); + assert.match(content, /^---\n/); + } +}); + +test("#given aggregate Codex skills #when source wiring is inspected #then shared skills are imported from the shared-skills package", async () => { + // given + const pluginPackageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8")); + const sharedPackageJson = JSON.parse(await readFile(join(root, "..", "..", "shared-skills", "package.json"), "utf8")); + const rootPackageJson = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf8")); + const syncScript = await readFile(join(root, "scripts", "sync-skills.mjs"), "utf8"); + + // when + const sharedSkillDependency = pluginPackageJson.dependencies?.["@oh-my-opencode/shared-skills"]; + const rootPackageFiles = rootPackageJson.files ?? []; + + // then + assert.equal(sharedPackageJson.exports?.["."], "./index.mjs"); + assert.equal(sharedPackageJson.files?.includes("skills"), true); + assert.equal(rootPackageFiles.includes("packages/shared-skills/package.json"), true); + assert.equal(rootPackageFiles.includes("packages/shared-skills/index.mjs"), true); + assert.equal(rootPackageFiles.includes("packages/shared-skills/skills"), true); + assert.equal(sharedSkillDependency, "file:../../shared-skills"); + assert.match(syncScript, /from "@oh-my-opencode\/shared-skills"/); + assert.doesNotMatch(syncScript, /shared-skills",\s*"skills"/); +}); + +test("#given shared skill package source #when aggregate Codex shared skills are inspected #then generated copies have no hand-authored drift", async () => { + // given + const sharedSkillsRoot = sharedSkillsRootPath(); + const aggregateSkillsRoot = join(root, "skills"); + const sharedSkillNames = (await readdir(sharedSkillsRoot, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + // when / then + for (const skillName of sharedSkillNames) { + const sharedContent = await readFile(join(sharedSkillsRoot, skillName, "SKILL.md"), "utf8"); + const aggregateContent = await readFile(join(aggregateSkillsRoot, skillName, "SKILL.md"), "utf8"); + assert.equal( + removeCodexCompatibilityGuidance(aggregateContent), + removeCodexCompatibilityGuidance(sharedContent), + `${skillName} drifted from shared-skills`, + ); + } +}); + +test("#given component skill sources #when aggregate Codex component skills are inspected #then generated copies have no hand-authored drift", async () => { + // given + const aggregateSkillsRoot = join(root, "skills"); + + // when / then + for (const [skillName, sourcePath] of componentSkillSources) { + const sourceContent = await readFile(join(root, sourcePath, "SKILL.md"), "utf8"); + const aggregateContent = await readFile(join(aggregateSkillsRoot, skillName, "SKILL.md"), "utf8"); + assert.equal( + removeCodexCompatibilityGuidance(aggregateContent), + removeCodexCompatibilityGuidance(sourceContent), + `${skillName} drifted from its component skill source`, + ); + } +}); + +test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then ulw-loop surfaces the ulw-loop alias", async () => { + // given + const skillRoot = join(root, "skills", "ulw-loop"); + + // when + const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8"); + const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8"); + + // then + assert.match(skill, /^---\nname: ulw-loop\n/m); + assert.match(skill, /Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps\./); + assert.match(interfaceMetadata, /display_name: "ulw loop"/); + assert.doesNotMatch(interfaceMetadata, /ulw-loop \/ ulw-loop/); + assert.match(interfaceMetadata, /short_description: "Goal-like ultrawork loop for systematic decomposition"/); + assert.match(interfaceMetadata, /default_prompt: "Use \$ulw-loop/); +}); + +test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then ulw-loop remains discoverable as an alias", async () => { + // given + const skillRoot = join(root, "skills", "ulw-loop"); + + // when + const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8"); + + // then + assert.match(interfaceMetadata, /search_terms:/); + assert.match(interfaceMetadata, /- "ulw-loop"/); +}); + +test("#given synced aggregate Codex skills #when they contain OpenCode orchestration examples #then Codex tool compatibility guidance is injected", async () => { + // given + const skillsRoot = join(root, "skills"); + const opencodeOnlyToolPattern = /\b(?:call_omo_agent|background_output|team_[a-z_]+|task)\s*\(/; + + // when + const skillNames = (await readdir(skillsRoot, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + // then + for (const skillName of skillNames) { + const content = await readFile(join(skillsRoot, skillName, "SKILL.md"), "utf8"); + if (!opencodeOnlyToolPattern.test(content)) continue; + + const compatibilityIndex = content.indexOf("## Codex Harness Tool Compatibility"); + assert.notEqual(compatibilityIndex, -1, `${skillName} is missing Codex compatibility guidance`); + assert.ok( + compatibilityIndex < content.search(opencodeOnlyToolPattern), + `${skillName} must explain Codex tool translation before OpenCode-only examples`, + ); + } +});