test(omo-codex): batch 52 (4 files)
This commit is contained in:
@@ -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
|
||||||
@@ -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 }}
|
||||||
@@ -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(/^<ultrawork-mode>/);
|
||||||
|
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<string, unknown> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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<string, string>;
|
||||||
|
readonly files: readonly string[];
|
||||||
|
readonly scripts: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<string, string> {
|
||||||
|
return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user