mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
18635e7c7d
The first-keyword check (`WITH` → SELECT path) was wrong for CTEs that precede mutations (e.g. `WITH stale AS (...) DELETE FROM t WHERE ...`). These would be routed through `db.prepare().all()` instead of executing the mutation. Use better-sqlite3's `stmt.reader` property, which asks SQLite's own parser whether the statement returns data. Single mutations go through `stmt.run()`; compound statements (which `prepare()` rejects) fall back to `db.exec()`. Add a regression test for WITH...DELETE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import { spawnSync } from 'child_process';
|
|
|
|
import Database from 'better-sqlite3';
|
|
|
|
/**
|
|
* Smoke tests for the q.ts sqlite-CLI replacement wrapper.
|
|
*
|
|
* Verifies the two modes (SELECT prints rows in sqlite3 default "list"
|
|
* format; mutation runs via db.exec) and a few edge cases that real
|
|
* skill invocations rely on.
|
|
*/
|
|
|
|
const Q = path.resolve(__dirname, 'q.ts');
|
|
|
|
describe('scripts/q.ts', () => {
|
|
let tempDir: string;
|
|
let dbPath: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'q-test-'));
|
|
dbPath = path.join(tempDir, 'test.db');
|
|
const db = new Database(dbPath);
|
|
db.exec(`
|
|
CREATE TABLE t (id INTEGER, name TEXT, note TEXT);
|
|
INSERT INTO t (id, name, note) VALUES (1, 'alice', 'hi'), (2, 'bob', NULL);
|
|
`);
|
|
db.close();
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function run(sql: string): { stdout: string; stderr: string; status: number } {
|
|
const r = spawnSync('pnpm', ['exec', 'tsx', Q, dbPath, sql], {
|
|
encoding: 'utf-8',
|
|
cwd: path.resolve(__dirname, '..'),
|
|
});
|
|
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', status: r.status ?? -1 };
|
|
}
|
|
|
|
it('SELECT prints pipe-separated rows in default order', () => {
|
|
const r = run('SELECT id, name FROM t ORDER BY id');
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout.trim()).toBe('1|alice\n2|bob');
|
|
});
|
|
|
|
it('SELECT renders NULL as empty string (matches sqlite3 default mode)', () => {
|
|
const r = run('SELECT id, note FROM t ORDER BY id');
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout.trim()).toBe('1|hi\n2|');
|
|
});
|
|
|
|
it('SELECT with no rows prints nothing', () => {
|
|
const r = run("SELECT id FROM t WHERE name = 'nobody'");
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toBe('');
|
|
});
|
|
|
|
it('INSERT runs via db.exec and persists', () => {
|
|
const r = run("INSERT INTO t (id, name) VALUES (3, 'carol')");
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toBe('');
|
|
|
|
const db = new Database(dbPath, { readonly: true });
|
|
const row = db.prepare('SELECT name FROM t WHERE id = 3').get() as { name: string };
|
|
db.close();
|
|
expect(row.name).toBe('carol');
|
|
});
|
|
|
|
it('compound mutation statements execute together', () => {
|
|
const r = run("DELETE FROM t WHERE id = 1; INSERT INTO t (id, name) VALUES (9, 'zed');");
|
|
expect(r.status).toBe(0);
|
|
|
|
const db = new Database(dbPath, { readonly: true });
|
|
const ids = (db.prepare('SELECT id FROM t ORDER BY id').all() as { id: number }[]).map(
|
|
(r) => r.id,
|
|
);
|
|
db.close();
|
|
expect(ids).toEqual([2, 9]);
|
|
});
|
|
|
|
it('WITH...DELETE is treated as a mutation, not a query', () => {
|
|
const r = run("WITH stale AS (SELECT id FROM t WHERE name = 'alice') DELETE FROM t WHERE id IN (SELECT id FROM stale)");
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toBe('');
|
|
|
|
const db = new Database(dbPath, { readonly: true });
|
|
const rows = db.prepare('SELECT name FROM t').all() as { name: string }[];
|
|
db.close();
|
|
expect(rows).toEqual([{ name: 'bob' }]);
|
|
});
|
|
|
|
it('exits 2 with usage when args are missing', () => {
|
|
const r = spawnSync('pnpm', ['exec', 'tsx', Q], {
|
|
encoding: 'utf-8',
|
|
cwd: path.resolve(__dirname, '..'),
|
|
});
|
|
expect(r.status).toBe(2);
|
|
expect(r.stderr).toMatch(/Usage/);
|
|
});
|
|
});
|