diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index c7a1e08e..dae5a90e 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -90,7 +90,7 @@ jobs: TMP: ${{ steps.windows-temp.outputs.path || runner.temp }} TMPDIR: ${{ steps.windows-temp.outputs.path || runner.temp }} CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: ${{ runner.environment == 'github-hosted' && runner.os == 'Windows' && 'true' || 'false' }} - run: pnpm --dir sdk/typescript run test + run: pnpm --dir sdk/typescript run ${{ runner.os == 'Windows' && 'test:windows' || 'test' }} - name: Check formatting run: pnpm --dir sdk/typescript run format diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index d99211ed..f23bf08b 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -49,6 +49,7 @@ "lint": "tsc --noEmit", "prepack": "node --run build", "test": "bun test --timeout 30000 ./tests-ts", + "test:windows": "node scripts/run-windows-tests.mjs", "test:package": "node scripts/smoke-package.mjs", "types": "pnpm run generate:models:check && tsc --noEmit" }, diff --git a/sdk/typescript/scripts/run-windows-tests.mjs b/sdk/typescript/scripts/run-windows-tests.mjs new file mode 100644 index 00000000..8f87a082 --- /dev/null +++ b/sdk/typescript/scripts/run-windows-tests.mjs @@ -0,0 +1,79 @@ +import { spawn } from "node:child_process"; +import { readdir, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const testDirectory = fileURLToPath(new URL("../tests-ts/", import.meta.url)); +const testFiles = ( + await Promise.all( + (await readdir(testDirectory)) + .filter((name) => name.endsWith(".test.ts")) + .map(async (name) => ({ + name, + size: (await stat(join(testDirectory, name))).size, + })), + ) +).sort( + (left, right) => + right.size - left.size || left.name.localeCompare(right.name), +); +const configuredWorkers = Number.parseInt( + process.env["CODEX_SECURITY_WINDOWS_TEST_WORKERS"] ?? "4", + 10, +); +if (!Number.isSafeInteger(configuredWorkers) || configuredWorkers < 1) { + throw new Error("CODEX_SECURITY_WINDOWS_TEST_WORKERS must be positive"); +} +const workerCount = Math.min(configuredWorkers, testFiles.length); +const groups = Array.from({ length: workerCount }, () => ({ + files: [], + size: 0, +})); +for (const testFile of testFiles) { + const group = groups.reduce((smallest, candidate) => + candidate.size < smallest.size ? candidate : smallest, + ); + group.files.push(testFile.name); + group.size += testFile.size; +} +const preload = process.env["CODEX_SECURITY_TEST_PRELOAD"]; +const running = new Set(); +let failed = false; +let interrupted = false; + +const stop = () => { + interrupted = true; + for (const child of running) child.kill(); +}; +process.once("SIGINT", stop); +process.once("SIGTERM", stop); + +const runTestGroup = (files) => + new Promise((resolve) => { + const argumentsList = ["test", "--timeout", "30000"]; + if (preload !== undefined) argumentsList.push("--preload", preload); + argumentsList.push(...files.map((file) => join(testDirectory, file))); + const child = spawn("bun", argumentsList, { + stdio: "inherit", + windowsHide: true, + }); + running.add(child); + child.once("error", (error) => { + running.delete(child); + console.error(`Could not start test worker: ${error.message}`); + resolve(false); + }); + child.once("close", (code, signal) => { + running.delete(child); + resolve(code === 0 && signal === null); + }); + }); + +console.error( + `Running ${testFiles.length} Windows test files with ${workerCount} workers.`, +); +const results = await Promise.all( + groups.map(({ files }) => runTestGroup(files)), +); +failed = results.some((passed) => !passed); +if (failed || interrupted) process.exitCode = 1; diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index 128ec67b..8d9277ec 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -82,7 +82,12 @@ describe("TypeScript package skeleton", () => { expect(packageJson.scripts.test).toBe( "bun test --timeout 30000 ./tests-ts", ); - expect(ciWorkflow).toContain("run: pnpm --dir sdk/typescript run test\n"); + expect(packageJson.scripts["test:windows"]).toBe( + "node scripts/run-windows-tests.mjs", + ); + expect(ciWorkflow).toContain( + "run: pnpm --dir sdk/typescript run ${{ runner.os == 'Windows' && 'test:windows' || 'test' }}\n", + ); expect(ciWorkflow).not.toContain("--timeout 60000"); });