Skip to content
Draft
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
20 changes: 20 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ confidence whoami --output plain # Force plain text
confidence events list | jq '.data[].name' # Pipe-friendly JSON
```

## SQL Preview and Job Status

These commands call the regional Metrics HTTP API directly. No MCP installation is required.

```bash
confidence data sql preview --from-file query.json --json
confidence data sql preview --from-file query.json --wait --timeout 60 --json
confidence data sql status --name sqlJobs/example --json
```

`query.json` contains the request body:

```json
{ "query": "SELECT 1" }
```

Preview starts a warehouse query and can incur costs. It is not an offline SQL check or warehouse-table discovery. By default it returns the job reference immediately. `--wait` polls pending, running, retry-pending and throttled states; `--timeout` is a positive number of seconds (maximum 3600, default 60) starting after preview creation. Waiting failures return a nonzero exit status and retain the job reference. A successful `status` inspection exits successfully even when the job itself failed.

The CLI never automatically retries preview or cancels the remote job when local waiting ends. Inspect the returned job reference before deciding what to do next. A lost preview response has an unknown outcome and may have started a job. `--profile` selects authentication; `--dry-run` validates and previews local input without authentication, HTTP calls, writes or telemetry. Input accepts only `query`; unsupported fields are rejected.

## License

[Apache License 2.0](../../LICENSE)
51 changes: 51 additions & 0 deletions packages/cli/__tests__/commands/data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import yargs from 'yargs';
import { prepareAuthTokens } from '@spotify-confidence/testing/auth';
import { dataCommand } from '@commands/index.js';

function run(args: string[]) {
let output = '';
const sut = yargs(args).scriptName('confidence').command(dataCommand).help().exitProcess(false);
return sut
.parseAsync(args, (_error: Error | undefined, _argv: unknown, text: string) => {
output = text;
})
.then(() => output);
}

describe('data command group', () => {
it('is registered in the real root help alongside existing commands', () => {
using _auth = prepareAuthTokens('none');
const sut = fileURLToPath(new URL('../../bin/cli.ts', import.meta.url));

const output = execFileSync(process.execPath, ['--import', 'tsx', sut, '--help'], {
encoding: 'utf8',
env: { ...process.env, CONFIDENCE_TELEMETRY: 'false' },
});

expect(output).toContain('confidence data');
expect(output).toContain('confidence warehouse');
expect(output).toContain('confidence flags');
});

it('shows help without authentication or placeholder API subcommands', async () => {
using _auth = prepareAuthTokens('none');
const sut = run;

const output = await sut(['data', '--help']);

expect(output).toContain('confidence data <command>');
expect(output).not.toMatch(/onboard|discover|proposal|workflow/iu);
});

it('shows guidance when invoked without a subcommand', async () => {
const sut = run;
expect(await sut(['data'])).toContain('confidence data --help');
});

it('rejects unimplemented subcommands', async () => {
const sut = run;
expect(await sut(['data', 'invented'])).toContain('Unknown argument: invented');
});
});
226 changes: 226 additions & 0 deletions packages/cli/__tests__/commands/sql.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { http, HttpResponse, server } from '@spotify-confidence/testing';
import { prepareAuthTokens } from '@spotify-confidence/testing/auth';
import { createProjectDir, writeJsonFile } from '@spotify-confidence/testing/scaffold';
import { sqlCommand } from '@commands/data/sql.js';
import { captureOutput } from '../helpers/capture.js';
import { createRunner } from '../helpers/run-command.js';

const sut = createRunner(sqlCommand);
const base = 'https://metrics.eu.confidence.dev/v1';

function mockPreview(job = 'sqlJobs/preview-1') {
server.use(http.post(`${base}/sqlQuery:preview`, () => HttpResponse.json({ job })));
}

it('executes an explicit preview once and returns its job reference', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });
const queries: unknown[] = [];
server.use(
http.post(`${base}/sqlQuery:preview`, async ({ request }) => {
queries.push(await request.json());
return HttpResponse.json({ job: 'sqlJobs/preview-1' });
}),
);

// Act
await sut(['sql', 'preview', '--from-file', file, '--json']);

// Assert
expect(queries).toEqual([{ query: 'SELECT 1' }]);
expect(JSON.parse(output.stdout).data).toEqual({ job: 'sqlJobs/preview-1' });
});

it('inspects a failed job successfully without starting a new query', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using output = captureOutput();
server.use(
http.get(`${base}/sqlJobs/preview-1`, () =>
HttpResponse.json({
name: 'sqlJobs/preview-1',
state: 'FAILED',
failedQueryResult: { error: 'Permission denied', reason: 'PERMISSION' },
}),
),
);

// Act
await sut(['sql', 'status', '--name', 'sqlJobs/preview-1', '--json']);

// Assert
expect(JSON.parse(output.stdout).data.failedQueryResult.reason).toBe('PERMISSION');
expect(output.stderr).toBe('');
});

it('waits through pending states until completion without replaying preview', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });
mockPreview();
let reads = 0;
server.use(
http.get(`${base}/sqlJobs/preview-1`, () =>
HttpResponse.json({
name: 'sqlJobs/preview-1',
state: reads++ === 0 ? 'PENDING_RETRY' : 'COMPLETED',
}),
),
);

// Act
await sut(['sql', 'preview', '--from-file', file, '--wait', '--timeout', '5', '--json']);

// Assert
expect(JSON.parse(output.stdout).data.state).toBe('COMPLETED');
expect(reads).toBe(2);
});

it('reports terminal query errors when waiting', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });
mockPreview();
server.use(
http.get(`${base}/sqlJobs/preview-1`, () =>
HttpResponse.json({
name: 'sqlJobs/preview-1',
state: 'FAILED',
failedQueryResult: { error: 'Missing permissions' },
}),
),
);

// Act
await sut(['sql', 'preview', '--from-file', file, '--wait']);

// Assert
expect(output.stderr).toContain('Missing permissions');
expect(output.stderr).toContain('sqlJobs/preview-1');
expect(process.exitCode).toBe(1);
});

it('preserves the job reference on a bounded wait timeout', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });
mockPreview();
server.use(
http.get(`${base}/sqlJobs/preview-1`, () =>
HttpResponse.json({
name: 'sqlJobs/preview-1',
state: 'THROTTLED',
}),
),
);

// Act
await sut(['sql', 'preview', '--from-file', file, '--wait', '--timeout', '0.05']);

// Assert
expect(output.stderr).toContain('timed out');
expect(output.stderr).toContain('sqlJobs/preview-1');
expect(process.exitCode).toBe(1);
});

it('validates dry-run input without authentication or API calls', async () => {
// Arrange
using _auth = prepareAuthTokens('none');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });

// Act
await sut(['sql', 'preview', '--from-file', file, '--dry-run', '--json']);

// Assert
expect(JSON.parse(output.stdout).data.dryRun).toBe(true);
expect(output.stderr).toBe('');
});

it.each(['sqlJobs/../other', 'https://example.com/sqlJobs/a', 'sqlJobs/a%2fb', 'factTables/a'])(
'rejects an invalid job name %s before network access',
async (name) => {
using _auth = prepareAuthTokens('none');
using output = captureOutput();

await sut(['sql', 'status', '--name', name, '--dry-run']);

expect(output.stderr).toContain('Resource name');
expect(process.exitCode).toBe(1);
},
);

it('rejects unknown preview input instead of dropping fields', async () => {
// Arrange
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1', warehouse: 'other' });

// Act
await sut(['sql', 'preview', '--from-file', file, '--dry-run']);

// Assert
expect(output.stderr).toContain('accepts only');
expect(process.exitCode).toBe(1);
});

it('reports a lost preview response as unknown without retrying', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });
let calls = 0;
server.use(
http.post(`${base}/sqlQuery:preview`, () => {
calls++;
return HttpResponse.error();
}),
);

// Act
await sut(['sql', 'preview', '--from-file', file, '--json']);

// Assert
expect(calls).toBe(1);
expect(output.stderr).toContain('outcome is unknown');
expect(process.exitCode).toBe(1);
});

it('does not start a preview without authentication', async () => {
using _auth = prepareAuthTokens('none');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });

await sut(['sql', 'preview', '--from-file', file]);

expect(output.stderr).toContain('Not logged in');
expect(process.exitCode).toBe(1);
});

it('reports malformed preview responses rather than following arbitrary URLs', async () => {
// Arrange
using _auth = prepareAuthTokens('valid');
using project = createProjectDir('empty');
using output = captureOutput();
const file = writeJsonFile(project.path, 'query.json', { query: 'SELECT 1' });
mockPreview('https://example.com/steal');

// Act
await sut(['sql', 'preview', '--from-file', file, '--wait']);

// Assert
expect(output.stderr).toContain('Resource name');
expect(process.exitCode).toBe(1);
});
Loading
Loading