From 39986dce016ba1768fc592bbca7ed3d0bb62f8db Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 30 May 2026 19:12:08 +0900 Subject: [PATCH] test(omo-codex): batch 52 (4 files) --- .../components/rules/.github/workflows/ci.yml | 47 ++++ .../rules/.github/workflows/publish.yml | 51 ++++ .../ultrawork/test/codex-hook.test.ts | 230 ++++++++++++++++++ .../ultrawork/test/package-smoke.test.ts | 78 ++++++ 4 files changed, 406 insertions(+) create mode 100644 packages/omo-codex/plugin/components/rules/.github/workflows/ci.yml create mode 100644 packages/omo-codex/plugin/components/rules/.github/workflows/publish.yml create mode 100644 packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts create mode 100644 packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts diff --git a/packages/omo-codex/plugin/components/rules/.github/workflows/ci.yml b/packages/omo-codex/plugin/components/rules/.github/workflows/ci.yml new file mode 100644 index 000000000..6cc7a1653 --- /dev/null +++ b/packages/omo-codex/plugin/components/rules/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: test (${{ matrix.os }} ยท node ${{ matrix.node }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node: ["20", "22"] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Check + run: npm run check + + - name: Unit tests + run: npm test + + - name: Package smoke + run: npm pack --dry-run diff --git a/packages/omo-codex/plugin/components/rules/.github/workflows/publish.yml b/packages/omo-codex/plugin/components/rules/.github/workflows/publish.yml new file mode 100644 index 000000000..4214a99b2 --- /dev/null +++ b/packages/omo-codex/plugin/components/rules/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: publish + +on: + release: + types: [published] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node 22 + uses: actions/setup-node@v6 + with: + node-version: "22" + registry-url: https://registry.npmjs.org + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Check + run: npm run check + + - name: Unit tests + run: npm test + + - name: Package smoke + run: npm pack --dry-run + + - name: Publish to npm + run: | + if [ -z "$NODE_AUTH_TOKEN" ]; then + echo "NODE_AUTH_TOKEN is not configured; skipping npm publish." + exit 0 + fi + npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts b/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts new file mode 100644 index 000000000..56126ef0a --- /dev/null +++ b/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts @@ -0,0 +1,230 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { isUltraworkPrompt, runUserPromptSubmitHook } from "../src/codex-hook.js"; + +const tempDirectories: string[] = []; + +afterEach(() => { + for (const directory of tempDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +describe("codex ultrawork hook", () => { + it("#given ultrawork prompt #when hook runs #then emits directive as Codex hook JSON", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: "please ulw this change", + }; + + // when + const output = runUserPromptSubmitHook(payload); + const parsed = parseHookOutput(output); + + // then + expect(parsed.hookSpecificOutput.hookEventName).toBe("UserPromptSubmit"); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/^/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/First user-visible line this turn MUST be exactly:/); + }); + + it("#given identifier-like ulw #when hook runs #then does not emit directive", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: "refactor ulw_helper.ts", + }; + + // when + const output = runUserPromptSubmitHook(payload); + + // then + expect(output).toBe(""); + expect(isUltraworkPrompt("ulw_helper.ts")).toBe(false); + }); + + it("#given context-pressure recovery prompt with ulw #when hook runs #then does not add more context", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: [ + "Warning: Skill descriptions were shortened to fit the 2% skills context budget.", + "Warning: Long threads and multiple compactions can cause the model to be less accurate.", + "Context compacted", + "error context_too_large: Your input exceeds the context window of this model.", + "ulw tdd commit well", + ].join("\n"), + }; + + // when + const output = runUserPromptSubmitHook(payload); + + // then + expect(output).toBe(""); + }); + + it("#given context-pressure transcript with ulw prompt #when hook runs #then does not add more context", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: "please ulw this change", + transcript_path: writeContextPressureTranscript(), + }; + + // when + const output = runUserPromptSubmitHook(payload); + + // then + expect(output).toBe(""); + }); + + it("#given Codex canonical context-window transcript with ulw prompt #when hook runs #then does not add more context", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: "please ulw this change", + transcript_path: writeCodexContextWindowTranscript(), + }; + + // when + const output = runUserPromptSubmitHook(payload); + + // then + expect(output).toBe(""); + }); + + it("#given context-pressure recovery prompt without ulw #when hook runs #then stays quiet", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: [ + "Context compacted", + "Your input exceeds the context window of this model.", + "Please adjust your input and try again.", + ].join("\n"), + }; + + // when + const output = runUserPromptSubmitHook(payload); + + // then + expect(output).toBe(""); + }); + + it("#given malformed or empty input #when hook runs #then exits with empty output", () => { + // given + const inputs = [undefined, {}, { hook_event_name: "UserPromptSubmit", prompt: "" }] as const; + + // when + const outputs = inputs.map((input) => runUserPromptSubmitHook(input)); + + // then + expect(outputs).toEqual(["", "", ""]); + }); + + it("#given directive #when inspected #then keeps manual QA and cleanup invariants", () => { + // given + const payload = { + hook_event_name: "UserPromptSubmit", + prompt: "please ultrawork", + }; + + // when + const output = runUserPromptSubmitHook(payload); + const parsed = parseHookOutput(output); + + // then + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/# Manual-QA channels/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/TESTS ALONE NEVER PROVE DONE/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/1\. HTTP call/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/2\. tmux/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/3\. Browser use/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/4\. Computer use/); + expect(parsed.hookSpecificOutput.additionalContext).toMatch(/CLEANUP \(PAIRED/); + }); +}); + +interface UserPromptSubmitHookOutput { + readonly hookSpecificOutput: { + readonly hookEventName: "UserPromptSubmit"; + readonly additionalContext: string; + }; +} + +function parseHookOutput(output: string): UserPromptSubmitHookOutput { + const parsed: unknown = JSON.parse(output); + if (!isUserPromptSubmitHookOutput(parsed)) throw new TypeError("Expected UserPromptSubmit hook output"); + return parsed; +} + +function isUserPromptSubmitHookOutput(value: unknown): value is UserPromptSubmitHookOutput { + if (!isRecord(value)) return false; + const hookSpecificOutput = value["hookSpecificOutput"]; + return ( + isRecord(hookSpecificOutput) && + hookSpecificOutput["hookEventName"] === "UserPromptSubmit" && + typeof hookSpecificOutput["additionalContext"] === "string" + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function writeContextPressureTranscript(): string { + const root = mkdtempSync(path.join(tmpdir(), "codex-ultrawork-context-pressure-")); + tempDirectories.push(root); + const transcriptPath = path.join(root, "transcript.jsonl"); + writeFileSync( + transcriptPath, + [ + JSON.stringify({ + type: "message", + payload: { + content: "Context compacted", + }, + }), + JSON.stringify({ + type: "message", + payload: { + content: "Your input exceeds the context window of this model.", + }, + }), + "", + ].join("\n"), + ); + return transcriptPath; +} + +function writeCodexContextWindowTranscript(): string { + const root = mkdtempSync(path.join(tmpdir(), "codex-ultrawork-context-window-")); + tempDirectories.push(root); + const transcriptPath = path.join(root, "transcript.jsonl"); + writeFileSync( + transcriptPath, + [ + JSON.stringify({ + type: "message", + payload: { + content: { + error: { + code: "context_length_exceeded", + }, + }, + }, + }), + JSON.stringify({ + type: "message", + payload: { + content: + "Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.", + }, + }), + "", + ].join("\n"), + ); + return transcriptPath; +} diff --git a/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts b/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts new file mode 100644 index 000000000..a942b0ba5 --- /dev/null +++ b/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts @@ -0,0 +1,78 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +type PackageJson = { + readonly type: string; + readonly packageManager: string; + readonly bin: Record; + readonly files: readonly string[]; + readonly scripts: Record; +}; + +describe("codex ultrawork package metadata", () => { + it("#given package metadata #when inspected #then hook ships as built TypeScript", () => { + // given + const packageJson = readPackageJson("package.json"); + const hooksJson = readJson("hooks/hooks.json"); + const cliSource = readFileSync("src/cli.ts", "utf8"); + + // when + const packageFiles = packageJson.files; + const hookCommands = collectHookCommandsFromValue(hooksJson); + const pluginRoot = ["$", "{PLUGIN_ROOT}"].join(""); + + // then + expect(packageJson.type).toBe("module"); + expect(packageJson.packageManager).toBe("npm@11.12.1"); + expect(packageJson.bin["omo-ultrawork"]).toBe("./dist/cli.js"); + expect(packageJson.scripts["build"]).toBe("tsc -p tsconfig.build.json"); + expect(packageJson.scripts["test"]).toBe("vitest --run"); + expect(packageFiles).toContain("dist"); + expect(packageFiles).toContain("directive.md"); + expect(packageFiles).not.toContain("hooks/ultrawork-detector.py"); + expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true); + expect(hookCommands).toContain(`node "${pluginRoot}/dist/cli.js" hook user-prompt-submit`); + expect(hookCommands).not.toContainEqual(expect.stringMatching(/\bpython3?\b|ultrawork-detector\.py/)); + }); +}); + +function readJson(path: string): unknown { + return JSON.parse(readFileSync(path, "utf8")); +} + +function readPackageJson(path: string): PackageJson { + const parsed = readJson(path); + if (!isPackageJson(parsed)) throw new TypeError(`Invalid package metadata: ${path}`); + return parsed; +} + +function collectHookCommandsFromValue(value: unknown): readonly string[] { + if (typeof value === "string") return []; + if (Array.isArray(value)) return value.flatMap(collectHookCommandsFromValue); + if (!isRecord(value)) return []; + const ownCommand = typeof value["command"] === "string" ? [value["command"]] : []; + return [...ownCommand, ...Object.values(value).flatMap(collectHookCommandsFromValue)]; +} + +function isPackageJson(value: unknown): value is PackageJson { + return ( + isRecord(value) && + value["type"] === "module" && + value["packageManager"] === "npm@11.12.1" && + isStringRecord(value["bin"]) && + isStringArray(value["files"]) && + isStringRecord(value["scripts"]) + ); +} + +function isStringArray(value: unknown): value is readonly string[] { + return Array.isArray(value) && value.every((item) => typeof item === "string"); +} + +function isStringRecord(value: unknown): value is Record { + return isRecord(value) && Object.values(value).every((item) => typeof item === "string"); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +}