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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ stackql_anthropic_admin_provider/ docs-driven provider: provider-dev/{source,
npm install # patches provider-utils docgen via postinstall
npm run build # anthropic: prepass → split → normalize+scrub → analyze → generate → post-pass → guards
npm run build-admin # anthropic_admin: same chain from normalize onward
npm run test-meta-routes # SHOW/DESCRIBE walk, zero errors required (STACKQL=/path/to/stackql)
npm run test-meta-routes # SHOW/DESCRIBE walk, zero errors required (auto-downloads stackql if absent)
npm run smoke # wire-contract-enforcing mock suite
npm run smoke-admin
npm run docgen && npm run docgen-admin # regenerate website docs
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"generate": "node node_modules/@stackql/provider-utils/bin/provider-dev-utils.mjs generate --input-dir stackql_anthropic_provider/provider-dev/source/split --output-dir stackql_anthropic_provider/provider-dev/openapi/src/anthropic --config-path stackql_anthropic_provider/provider-dev/config/all_services.csv --provider-id anthropic --servers '[{\"url\":\"https://api.anthropic.com\"}]' --provider-config '{\"auth\":{\"type\":\"custom\",\"location\":\"header\",\"name\":\"x-api-key\",\"credentialsenvvar\":\"ANTHROPIC_API_KEY\"}}' --naive-req-body-translate --overwrite && node factory/post-pass.mjs --provider-root stackql_anthropic_provider/provider-dev/openapi/src/anthropic/v00.00.00000",
"guards": "node factory/guards.mjs --provider anthropic --provider-root stackql_anthropic_provider/provider-dev/openapi/src/anthropic/v00.00.00000 --spec stackql_anthropic_provider/provider-dev/downloaded/anthropic-openapi.yml --exclusions factory/exclusions.yaml",
"build": "npm run prepass && npm run split && npm run normalize && npm run analyze && npm run generate && npm run guards",
"test-meta-routes": "STACKQL=${STACKQL:-stackql} node stackql_anthropic_provider/bin/test-meta-routes.cjs anthropic",
"test-meta-routes": "node stackql_anthropic_provider/bin/test-meta-routes.cjs anthropic",
"smoke": "node stackql_anthropic_provider/tests/smoke.cjs",
"smoke-live": "node stackql_anthropic_provider/tests/smoke.cjs --live",
"build-admin": "rm -rf stackql_anthropic_admin_provider/provider-dev/source/split && mkdir -p stackql_anthropic_admin_provider/provider-dev/source/split && cp stackql_anthropic_admin_provider/provider-dev/source/*.yaml stackql_anthropic_admin_provider/provider-dev/source/split/ && node node_modules/@stackql/provider-utils/bin/provider-dev-utils.mjs normalize --api-dir stackql_anthropic_admin_provider/provider-dev/source/split && node factory/scrub-unions.mjs --api-dir stackql_anthropic_admin_provider/provider-dev/source/split && node node_modules/@stackql/provider-utils/bin/provider-dev-utils.mjs analyze --input-dir stackql_anthropic_admin_provider/provider-dev/source/split --output-dir stackql_anthropic_admin_provider/provider-dev/config && node node_modules/@stackql/provider-utils/bin/provider-dev-utils.mjs generate --input-dir stackql_anthropic_admin_provider/provider-dev/source/split --output-dir stackql_anthropic_admin_provider/provider-dev/openapi/src/anthropic_admin --config-path stackql_anthropic_admin_provider/provider-dev/config/all_services.csv --provider-id anthropic_admin --servers '[{\"url\":\"https://api.anthropic.com\"}]' --provider-config '{\"auth\":{\"type\":\"custom\",\"location\":\"header\",\"name\":\"x-api-key\",\"credentialsenvvar\":\"ANTHROPIC_ADMIN_KEY\"}}' --naive-req-body-translate --overwrite && node factory/post-pass.mjs --provider-root stackql_anthropic_admin_provider/provider-dev/openapi/src/anthropic_admin/v00.00.00000 && node factory/guards.mjs --provider anthropic_admin --provider-root stackql_anthropic_admin_provider/provider-dev/openapi/src/anthropic_admin/v00.00.00000",
Expand Down
77 changes: 73 additions & 4 deletions stackql_anthropic_admin_provider/bin/test-meta-routes.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,74 @@ async function stopExistingServer() {
process.exit(1);
}

async function startServer() {
const bin = process.env.STACKQL || path.join(baseDir, 'stackql');
if (!fs.existsSync(bin)) {
console.error(`[server] ERROR: stackql binary not found at ${bin} (set STACKQL to override)`);
// Resolve the stackql binary to an ABSOLUTE path. fs.existsSync resolves
// bare names against the CWD while spawn() resolves them against PATH, so
// a relative candidate must be absolutized before spawning (a bare
// `STACKQL=stackql` once passed the exists-check via a repo-root file and
// then died in spawn with an unhandled ENOENT).
//
// Order: $STACKQL (path or PATH-resolved command) → <provider>/stackql →
// ./stackql → `stackql` on PATH → download the latest release into the
// provider dir (same fallback bin/start-server.sh has always had).
function resolveStackql() {
const tryPath = (p) => {
if (!p) return null;
const abs = path.resolve(p);
return fs.existsSync(abs) && fs.statSync(abs).isFile() ? abs : null;
};
const fromPathLookup = (cmd) => {
try {
const found = execSync(
process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`,
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
).split('\n')[0].trim();
return found ? found : null;
} catch (e) {
return null;
}
};

const envBin = process.env.STACKQL;
if (envBin) {
const resolved = envBin.includes(path.sep) || envBin.includes('/')
? tryPath(envBin)
: (tryPath(envBin) || fromPathLookup(envBin));
if (resolved) return resolved;
console.warn(`[server] STACKQL='${envBin}' does not resolve to a binary - falling back`);
}
const local = tryPath(path.join(baseDir, 'stackql')) || tryPath(path.join(process.cwd(), 'stackql'));
if (local) return local;
const onPath = fromPathLookup('stackql');
if (onPath) return onPath;

// Last resort: download the latest release (linux/darwin, amd64/arm64).
if (process.platform !== 'linux' && process.platform !== 'darwin') {
console.error('[server] ERROR: stackql binary not found (set STACKQL, or place ./stackql in the provider dir)');
process.exit(1);
}
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
const url = `https://releases.stackql.io/stackql/latest/stackql_${process.platform}_${arch}.zip`;
slog(`stackql binary not found - downloading ${url}`);
try {
execSync(
`curl -sSL -o stackql.zip "${url}" && unzip -o stackql.zip stackql && rm -f stackql.zip && chmod +x stackql`,
{ cwd: baseDir, stdio: ['ignore', 'inherit', 'inherit'] },
);
} catch (e) {
console.error(`[server] ERROR: stackql download failed: ${e.message}`);
process.exit(1);
}
const downloaded = tryPath(path.join(baseDir, 'stackql'));
if (!downloaded) {
console.error('[server] ERROR: stackql download did not produce a usable binary');
process.exit(1);
}
slog(`downloaded stackql to ${downloaded}`);
return downloaded;
}

async function startServer() {
const bin = resolveStackql();
const regPath = path.join(baseDir, 'provider-dev', 'openapi');
const reg = JSON.stringify({
url: `file://${regPath}`,
Expand All @@ -190,6 +252,12 @@ async function startServer() {
serverChild = spawn(bin, [`--registry=${reg}`, `--pgsrv.port=${port}`, 'srv'], {
stdio: ['ignore', out, out],
});
// Without this handler a failed spawn (ENOENT, EACCES) is an unhandled
// 'error' event that crashes node with a raw stack trace.
serverChild.on('error', (err) => {
console.error(`[server] ERROR: failed to start '${bin}': ${err.message}`);
process.exit(1);
});
serverChild.on('exit', (code, signal) => {
if (!serverShuttingDown) {
console.error(`[server] ERROR: server exited unexpectedly (code=${code} signal=${signal}) - see ${serverLogPath}`);
Expand All @@ -209,6 +277,7 @@ async function startServer() {
function stopServer() {
if (!serverChild || serverShuttingDown) return;
serverShuttingDown = true;
if (serverChild.pid === undefined) return; // spawn never succeeded
slog(`stopping server (pid ${serverChild.pid})`);
try {
serverChild.kill('SIGTERM');
Expand Down
6 changes: 3 additions & 3 deletions stackql_anthropic_admin_provider/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ wire-contract-enforcing mock, which also enforces the disjoint
`sk-ant-admin...` key space on `/v1/organizations/*`):

```bash
STACKQL=/path/to/stackql node bin/test-meta-routes.cjs anthropic_admin
STACKQL=/path/to/stackql node ../stackql_anthropic_provider/tests/smoke.cjs \
node bin/test-meta-routes.cjs anthropic_admin # STACKQL=/path/to/stackql to override
node ../stackql_anthropic_provider/tests/smoke.cjs \
--manifest tests/manifest.yaml # mock mode
STACKQL=/path/to/stackql node ../stackql_anthropic_provider/tests/smoke.cjs \
node ../stackql_anthropic_provider/tests/smoke.cjs \
--manifest tests/manifest.yaml --live # READ-ONLY live subset
```

Expand Down
77 changes: 73 additions & 4 deletions stackql_anthropic_provider/bin/test-meta-routes.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,74 @@ async function stopExistingServer() {
process.exit(1);
}

async function startServer() {
const bin = process.env.STACKQL || path.join(baseDir, 'stackql');
if (!fs.existsSync(bin)) {
console.error(`[server] ERROR: stackql binary not found at ${bin} (set STACKQL to override)`);
// Resolve the stackql binary to an ABSOLUTE path. fs.existsSync resolves
// bare names against the CWD while spawn() resolves them against PATH, so
// a relative candidate must be absolutized before spawning (a bare
// `STACKQL=stackql` once passed the exists-check via a repo-root file and
// then died in spawn with an unhandled ENOENT).
//
// Order: $STACKQL (path or PATH-resolved command) → <provider>/stackql →
// ./stackql → `stackql` on PATH → download the latest release into the
// provider dir (same fallback bin/start-server.sh has always had).
function resolveStackql() {
const tryPath = (p) => {
if (!p) return null;
const abs = path.resolve(p);
return fs.existsSync(abs) && fs.statSync(abs).isFile() ? abs : null;
};
const fromPathLookup = (cmd) => {
try {
const found = execSync(
process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`,
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
).split('\n')[0].trim();
return found ? found : null;
} catch (e) {
return null;
}
};

const envBin = process.env.STACKQL;
if (envBin) {
const resolved = envBin.includes(path.sep) || envBin.includes('/')
? tryPath(envBin)
: (tryPath(envBin) || fromPathLookup(envBin));
if (resolved) return resolved;
console.warn(`[server] STACKQL='${envBin}' does not resolve to a binary - falling back`);
}
const local = tryPath(path.join(baseDir, 'stackql')) || tryPath(path.join(process.cwd(), 'stackql'));
if (local) return local;
const onPath = fromPathLookup('stackql');
if (onPath) return onPath;

// Last resort: download the latest release (linux/darwin, amd64/arm64).
if (process.platform !== 'linux' && process.platform !== 'darwin') {
console.error('[server] ERROR: stackql binary not found (set STACKQL, or place ./stackql in the provider dir)');
process.exit(1);
}
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
const url = `https://releases.stackql.io/stackql/latest/stackql_${process.platform}_${arch}.zip`;
slog(`stackql binary not found - downloading ${url}`);
try {
execSync(
`curl -sSL -o stackql.zip "${url}" && unzip -o stackql.zip stackql && rm -f stackql.zip && chmod +x stackql`,
{ cwd: baseDir, stdio: ['ignore', 'inherit', 'inherit'] },
);
} catch (e) {
console.error(`[server] ERROR: stackql download failed: ${e.message}`);
process.exit(1);
}
const downloaded = tryPath(path.join(baseDir, 'stackql'));
if (!downloaded) {
console.error('[server] ERROR: stackql download did not produce a usable binary');
process.exit(1);
}
slog(`downloaded stackql to ${downloaded}`);
return downloaded;
}

async function startServer() {
const bin = resolveStackql();
const regPath = path.join(baseDir, 'provider-dev', 'openapi');
const reg = JSON.stringify({
url: `file://${regPath}`,
Expand All @@ -190,6 +252,12 @@ async function startServer() {
serverChild = spawn(bin, [`--registry=${reg}`, `--pgsrv.port=${port}`, 'srv'], {
stdio: ['ignore', out, out],
});
// Without this handler a failed spawn (ENOENT, EACCES) is an unhandled
// 'error' event that crashes node with a raw stack trace.
serverChild.on('error', (err) => {
console.error(`[server] ERROR: failed to start '${bin}': ${err.message}`);
process.exit(1);
});
serverChild.on('exit', (code, signal) => {
if (!serverShuttingDown) {
console.error(`[server] ERROR: server exited unexpectedly (code=${code} signal=${signal}) - see ${serverLogPath}`);
Expand All @@ -209,6 +277,7 @@ async function startServer() {
function stopServer() {
if (!serverChild || serverShuttingDown) return;
serverShuttingDown = true;
if (serverChild.pid === undefined) return; // spawn never succeeded
slog(`stopping server (pid ${serverChild.pid})`);
try {
serverChild.kill('SIGTERM');
Expand Down
6 changes: 3 additions & 3 deletions stackql_anthropic_provider/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ hard-fails on any error, duplicate (resource, verb) signature, or
zero-column selectable resource:

```bash
STACKQL=/path/to/stackql node bin/test-meta-routes.cjs anthropic
node bin/test-meta-routes.cjs anthropic # STACKQL=/path/to/stackql to override
```

## Smoke (manifest-driven)

```bash
STACKQL=/path/to/stackql node tests/smoke.cjs # mock mode
STACKQL=/path/to/stackql node tests/smoke.cjs --live # live subset
node tests/smoke.cjs # mock mode
node tests/smoke.cjs --live # live subset
node tests/smoke.cjs --only agents_insert,agents_archive # cherry-pick
```

Expand Down
62 changes: 61 additions & 1 deletion stackql_anthropic_provider/tests/smoke.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,67 @@ const only = (argOf('--only', '') || '').split(',').filter(Boolean);

const manifest = YAML.parse(fs.readFileSync(manifestPath, 'utf8'));
const cfg = manifest.config || {};
const stackqlBin = argOf('--stackql', process.env.STACKQL || cfg.stackql || path.join(baseDir, 'stackql'));

// Resolve the stackql binary to an ABSOLUTE path. fs.existsSync resolves
// bare names against the CWD while spawn() resolves them against PATH, so
// relative candidates must be absolutized before spawning. Order:
// --stackql / $STACKQL / config.stackql (path or PATH-resolved command) →
// <provider>/stackql → ./stackql → `stackql` on PATH → download the latest
// release into the provider dir.
function resolveStackql() {
const { execSync } = require('child_process');
const tryPath = (p) => {
if (!p) return null;
const abs = path.resolve(p);
return fs.existsSync(abs) && fs.statSync(abs).isFile() ? abs : null;
};
const fromPathLookup = (cmd) => {
try {
const found = execSync(
process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`,
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
).split('\n')[0].trim();
return found ? found : null;
} catch (e) {
return null;
}
};
for (const candidate of [argOf('--stackql', null), process.env.STACKQL, cfg.stackql]) {
if (!candidate) continue;
const resolved = candidate.includes(path.sep) || candidate.includes('/')
? tryPath(path.resolve(path.dirname(manifestPath), candidate)) || tryPath(candidate)
: (tryPath(candidate) || fromPathLookup(candidate));
if (resolved) return resolved;
console.warn(`stackql candidate '${candidate}' does not resolve to a binary - falling back`);
}
const local = tryPath(path.join(baseDir, 'stackql')) || tryPath(path.join(process.cwd(), 'stackql'));
if (local) return local;
const onPath = fromPathLookup('stackql');
if (onPath) return onPath;
if (process.platform !== 'linux' && process.platform !== 'darwin') {
console.error('stackql binary not found (set STACKQL, or place ./stackql in the provider dir)');
process.exit(2);
}
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
const url = `https://releases.stackql.io/stackql/latest/stackql_${process.platform}_${arch}.zip`;
console.log(`stackql binary not found - downloading ${url}`);
try {
execSync(
`curl -sSL -o stackql.zip "${url}" && unzip -o stackql.zip stackql && rm -f stackql.zip && chmod +x stackql`,
{ cwd: baseDir, stdio: ['ignore', 'inherit', 'inherit'] },
);
} catch (e) {
console.error(`stackql download failed: ${e.message}`);
process.exit(2);
}
const downloaded = tryPath(path.join(baseDir, 'stackql'));
if (!downloaded) {
console.error('stackql download did not produce a usable binary');
process.exit(2);
}
return downloaded;
}
const stackqlBin = resolveStackql();
const queriesDir = path.resolve(path.dirname(manifestPath), cfg.queries_dir || 'queries');
const mockPort = cfg.mock_port || 8990;
const fatalPatterns = [...DEFAULT_FATAL, ...(cfg.fatal_patterns || [])];
Expand Down
Loading