Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# governance — Testing standard

Node's built-in runner via `tsx --test`, zero test dependencies (matches the SDK's zero runtime
deps). Files: `src/**/*.test.ts` next to the unit.

- `npm test -w packages/<pkg>` runs through `scripts/test-gate.mjs` (machine-wide
`LUA_TEST_SLOTS` semaphore shared with the other Lua repos + heap cap; pass-through in CI) with
`--test-concurrency=4` — the runner otherwise forks CPU-1 processes.
- Check one change with one file: `npx tsx --test src/<file>.test.ts` from the package.
- The `src/**/*.test.ts` glob is expanded by npm's shell without globstar: it matches **one**
directory level. Keep test files at `src/*.test.ts` or `src/<dir>/*.test.ts`, never deeper —
a deeper file is silently skipped.
- Pure functions only: no I/O, no timers, no network in this repo's tests. If a test needs
time, inject a clock.
- `.only`/`{ only: true }` never lands; `{ skip: "<reason>" }` carries its reason inline.
- One behaviour per `test()`; assert with `node:assert/strict`.
1 change: 1 addition & 0 deletions node_modules
8 changes: 5 additions & 3 deletions packages/governance-platform/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "governance-sdk-platform",
"version": "0.1.3",
"description": "Platform storage layer for governance-sdk — auto-migrating schema, typed queries for org settings and policy tiers",
"description": "Platform storage layer for governance-sdk \u2014 auto-migrating schema, typed queries for org settings and policy tiers",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -19,7 +19,7 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "tsx --test src/*.test.ts",
"test": "node ../../scripts/test-gate.mjs tsx --test --test-concurrency=4 src/*.test.ts",
"lint": "tsc --noEmit"
},
"author": "Lua <hello@heylua.ai> (https://heylua.ai)",
Expand Down Expand Up @@ -51,7 +51,9 @@
"pg": ">=8.0.0"
},
"peerDependenciesMeta": {
"pg": { "optional": true }
"pg": {
"optional": true
}
},
"devDependencies": {
"tsx": "^4.22.4",
Expand Down
52 changes: 38 additions & 14 deletions packages/governance/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "governance-sdk",
"version": "0.20.0",
"description": "AI Agent Governance for TypeScript — policy enforcement, scoring, compliance, and audit for AI agents",
"description": "AI Agent Governance for TypeScript \u2014 policy enforcement, scoring, compliance, and audit for AI agents",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -211,7 +211,7 @@
"scripts": {
"build": "tsc && node scripts/fix-esm-imports.mjs",
"dev": "tsc --watch",
"test": "tsx --test src/*.test.ts src/**/*.test.ts",
"test": "node ../../scripts/test-gate.mjs tsx --test --test-concurrency=4 src/*.test.ts src/**/*.test.ts",
"lint": "tsc --noEmit",
"prepublishOnly": "node ../../scripts/sync-readme.mjs && tsc"
},
Expand Down Expand Up @@ -276,18 +276,42 @@
"pg": ">=8.0.0"
},
"peerDependenciesMeta": {
"@mastra/core": { "optional": true },
"ai": { "optional": true },
"langchain": { "optional": true },
"openai-agents": { "optional": true },
"pg": { "optional": true },
"@modelcontextprotocol/sdk": { "optional": true },
"@aws-sdk/client-bedrock-agent-runtime": { "optional": true },
"genkit": { "optional": true },
"llamaindex": { "optional": true },
"@anthropic-ai/sdk": { "optional": true },
"@mistralai/mistralai": { "optional": true },
"ollama": { "optional": true }
"@mastra/core": {
"optional": true
},
"ai": {
"optional": true
},
"langchain": {
"optional": true
},
"openai-agents": {
"optional": true
},
"pg": {
"optional": true
},
"@modelcontextprotocol/sdk": {
"optional": true
},
"@aws-sdk/client-bedrock-agent-runtime": {
"optional": true
},
"genkit": {
"optional": true
},
"llamaindex": {
"optional": true
},
"@anthropic-ai/sdk": {
"optional": true
},
"@mistralai/mistralai": {
"optional": true
},
"ollama": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^25.4.0",
Expand Down
58 changes: 58 additions & 0 deletions scripts/test-gate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node
// Machine-wide gate for test runs. Usage: node scripts/test-gate.mjs <cmd> [args...]
//
// Outside CI it (1) waits for a free slot (LUA_TEST_SLOTS, default 2) shared by
// every repo/terminal on this machine, and (2) caps each Node worker's heap
// (LUA_TEST_HEAP_MB, default 2048) unless NODE_OPTIONS already sets one.
// Several agent terminals running suites at once then queue instead of stacking
// 7-13 workers each until the box swaps. In CI it is a plain pass-through.
import { spawn } from 'node:child_process';
import { closeSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir, userInfo } from 'node:os';
import { join } from 'node:path';

const [cmd, ...args] = process.argv.slice(2);
if (!cmd) { console.error('usage: test-gate <cmd> [args...]'); process.exit(2); }

const env = { ...process.env };
if (!env.CI) {
const heap = env.LUA_TEST_HEAP_MB || '2048';
if (!/max-old-space-size/.test(env.NODE_OPTIONS || '')) {
env.NODE_OPTIONS = `${env.NODE_OPTIONS || ''} --max-old-space-size=${heap}`.trim();
}
}

const slots = env.CI ? Infinity : Number(env.LUA_TEST_SLOTS || 2);
const dir = join(tmpdir(), `lua-test-gate-${userInfo().username}`);
const mine = join(dir, `${process.pid}.lock`);
const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };

function tryAcquire() {
if (slots === Infinity) return true;
mkdirSync(dir, { recursive: true });
const held = [];
for (const f of readdirSync(dir)) {
const p = join(dir, f);
let pid = NaN, what = '';
try { [pid, what] = readFileSync(p, 'utf8').split('\n'); pid = Number(pid); } catch { continue; }
if (alive(pid)) held.push(what); else { try { unlinkSync(p); } catch {} }
}
if (held.length >= slots) return held;
closeSync(openSync(mine, 'wx'));
writeFileSync(mine, `${process.pid}\n${process.cwd()} $ ${[cmd, ...args].join(' ')}\n`);
return true;
}

let waited = 0;
for (let r = tryAcquire(); r !== true; r = tryAcquire()) {
if (waited === 0) console.error(`[test-gate] ${slots} slot(s) busy, waiting:\n ${r.join('\n ')}`);
await new Promise((res) => setTimeout(res, 2000));
waited += 2;
}
if (waited) console.error(`[test-gate] slot acquired after ${waited}s`);

const release = () => { if (slots !== Infinity) { try { unlinkSync(mine); } catch {} } };
const child = spawn(cmd, args, { stdio: 'inherit', env, shell: process.platform === 'win32' });
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => child.kill(sig));
child.on('exit', (code, sig) => { release(); process.exit(code ?? (sig ? 1 : 0)); });
process.on('exit', release);
Loading