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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ $ pip install ./path-to-wheel-file.whl

Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.

`./scripts/bootstrap` installs the locked Steady package and its platform
binary. `./scripts/test` starts it automatically when no mock server is running.
Neither `./scripts/test` nor `./scripts/mock` downloads Node tools. If the local
tool is missing, rerun bootstrap. `TEST_API_BASE_URL` and an already-running
mock server remain supported. To start the server yourself:

```sh
$ ./scripts/mock
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"node": "22.22.3"
},
"devDependencies": {
"@stdy/cli": "0.22.1",
"pyright": "1.1.399"
}
}
56 changes: 56 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 11 additions & 8 deletions scripts/mock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env bash

set -e
set -euo pipefail

cd "$(dirname "$0")/.."

if [[ -n "$1" && "$1" != '--'* ]]; then
if [[ -n "${1:-}" && "$1" != '--'* ]]; then
URL="$1"
shift
elif [[ -f .castiron.stats.yml && -f api_reference/openapi.transformed.yml ]]; then
Expand All @@ -24,17 +24,19 @@ fi
echo "==> Starting mock server with URL ${URL}"

# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
npm exec --package=@stdy/cli@0.22.1 -- steady --version
if [ "${1:-}" == "--daemon" ]; then
# Check the already-installed tool before starting the readiness timeout.
./scripts/run-steady --version

npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
./scripts/run-steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
server_pid=$!
trap 'kill "$server_pid" 2>/dev/null || true' EXIT

# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
attempts=0
while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
if ! kill -0 $! 2>/dev/null; then
if ! kill -0 "$server_pid" 2>/dev/null; then
echo
cat .stdy.log
exit 1
Expand All @@ -51,6 +53,7 @@ if [ "$1" == "--daemon" ]; then
done

echo
trap - EXIT
else
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
exec ./scripts/run-steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
8 changes: 8 additions & 0 deletions scripts/run-steady
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
if ! command -v node >/dev/null 2>&1; then
echo 'Node.js is missing. Follow CONTRIBUTING.md, then run ./scripts/bootstrap.' >&2
exit 1
fi
exec node scripts/utils/node-tooling.cjs steady "$@"
5 changes: 3 additions & 2 deletions scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ YELLOW='\033[0;33m'
NC='\033[0m' # No Color

function steady_is_running() {
curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
}

kill_server_on_port() {
Expand Down Expand Up @@ -43,7 +43,8 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo -e " \$ ${YELLOW}./scripts/bootstrap${NC}"
echo -e " \$ ${YELLOW}./scripts/mock path/to/your.openapi.yml${NC}"
echo

exit 1
Expand Down
30 changes: 21 additions & 9 deletions scripts/utils/node-tooling.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,33 @@ if (process.versions.node !== manifest.engines.node) {

const [tool, ...args] = process.argv.slice(2);
if (tool === '--check-node') process.exit(0);
if (tool !== 'pyright') fail(`Unknown repository tool: ${tool}`);
const tools = {
pyright: { package: 'pyright', entry: 'index.js' },
steady: { package: '@stdy/cli', entry: 'steady.js' },
};
if (!Object.hasOwn(tools, tool)) fail(`Unknown repository tool: ${tool}`);
const selected = tools[tool];

const directory = path.join(root, 'node_modules', tool);
const directory = path.join(root, 'node_modules', selected.package);
let installed;
try {
installed = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
} catch {
fail(`Missing local ${tool}.`);
}
if (installed.version !== manifest.devDependencies[tool]) {
fail(`Expected ${tool} ${manifest.devDependencies[tool]}, found ${installed.version}.`);
if (installed.version !== manifest.devDependencies[selected.package]) {
fail(`Expected ${tool} ${manifest.devDependencies[selected.package]}, found ${installed.version}.`);
}
const entry = path.join(directory, 'index.js');
const entry = path.join(directory, selected.entry);
if (!fs.existsSync(entry)) fail(`Missing local ${tool} executable.`);
// Calling Node explicitly also avoids upstream 1.1.399's CRLF shebang issue.
const result = spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit' });
if (result.error) fail(`Could not start ${tool}: ${result.error.message}`);
process.exit(result.status ?? 1);
if (tool === 'steady') {
// The upstream wrapper owns its native child and forwards termination signals.
// Run it in this process so mock-server cleanup reaches that child.
process.argv = [process.execPath, entry, ...args];
require(entry);
} else {
// Calling Node explicitly also avoids upstream 1.1.399's CRLF shebang issue.
const result = spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit' });
if (result.error) fail(`Could not start ${tool}: ${result.error.message}`);
process.exit(result.status ?? 1);
}
47 changes: 46 additions & 1 deletion tests/node-tooling.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const repository = path.resolve(__dirname, '..');
function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openai-node-tooling-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
for (const file of ['package.json', 'scripts/bootstrap-node', 'scripts/run-pyright', 'scripts/utils/node-tooling.cjs']) {
for (const file of ['package.json', 'scripts/bootstrap-node', 'scripts/run-pyright', 'scripts/run-steady', 'scripts/test', 'scripts/utils/node-tooling.cjs']) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.copyFileSync(path.join(repository, file), path.join(root, file));
}
Expand Down Expand Up @@ -80,6 +80,51 @@ test('bootstrap refuses a wrong pnpm without attempting an install', (t) => {
assert.match(result.stderr, /Expected pnpm/);
});

test('Steady is local-only and receives the original arguments', (t) => {
const root = fixture(t);
const missing = run(root, ['steady', '--version']);
assert.equal(missing.status, 1);
assert.match(missing.stderr, /Missing local steady/);
const directory = path.join(root, 'node_modules/@stdy/cli');
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(path.join(directory, 'package.json'), JSON.stringify({ version: '0.22.1' }));
fs.writeFileSync(path.join(directory, 'steady.js'), 'console.log(JSON.stringify(process.argv.slice(2)));');
const result = run(root, ['steady', 'spec with spaces.yml', '--version']);
assert.equal(result.status, 0);
assert.deepEqual(JSON.parse(result.stdout), ['spec with spaces.yml', '--version']);
});

function executable(root, file, content) {
const target = path.join(root, file);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, '#!/bin/sh\nset -e\n' + content + '\n', { mode: 0o755 });
}

for (const mode of ['start', 'running', 'override']) {
test(`scripts/test preserves ${mode} mock-server behavior and both Python lanes`, (t) => {
const root = fixture(t);
const log = path.join(root, 'calls');
const ready = path.join(root, 'ready');
if (mode === 'running') fs.writeFileSync(ready, '');
executable(root, 'bin/curl', '[ -f "$READY" ]');
executable(root, 'bin/lsof', 'exit 0');
executable(root, 'bin/uv', 'printf "uv %s\\n" "$*" >> "$CALLS"');
executable(root, 'scripts/mock', 'printf "mock %s\\n" "$*" >> "$CALLS"\n: > "$READY"');
executable(root, 'scripts/test-pydantic-v1', 'printf "v1 %s\\n" "$*" >> "$CALLS"');
const result = spawnSync('/bin/bash', ['scripts/test', '-q', 'tests/test_client.py'], {
cwd: root, encoding: 'utf8', env: {
...process.env, PATH: path.join(root, 'bin') + ':/usr/bin:/bin', CALLS: log, READY: ready,
TEST_API_BASE_URL: mode === 'override' ? 'http://example.invalid' : '',
},
});
assert.equal(result.status, 0, result.stderr);
const calls = fs.readFileSync(log, 'utf8');
assert.equal(calls.includes('mock --daemon'), mode === 'start');
assert.match(calls, /uv run --locked --all-extras pytest -q tests\/test_client.py/);
assert.match(calls, /v1 -q tests\/test_client.py/);
});
}

test('Node dependency policy stays fail-closed', () => {
const policy = fs.readFileSync(path.join(repository, 'pnpm-workspace.yaml'), 'utf8');
for (const setting of [
Expand Down
Loading