- Replace better-sqlite3 with bun:sqlite (native, no native addon build)
- Change all spawn('node') to spawn('bun') for agent processes
- Update package.json scripts: node→bun, tsx→bun, npm→bun
- Add bun-types for tsc compatibility
- Add bun:sqlite→better-sqlite3 shim for vitest (tests run on Node.js)
- Update Dockerfile: install bun alongside Node.js (CLIs need Node)
- Update setup/platform.ts: getNodePath() resolves bun binary
- Remove better-sqlite3 from production dependencies (devDep only for tests)
262 lines
6.8 KiB
TypeScript
262 lines
6.8 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
|
import { Database } from 'bun:sqlite';
|
|
|
|
/**
|
|
* Tests for the register step.
|
|
*
|
|
* Verifies: parameterized SQL (no injection), file templating,
|
|
* apostrophe in names, .env updates.
|
|
*/
|
|
|
|
function createTestDb(): Database {
|
|
const db = new Database(':memory:');
|
|
db.exec(`CREATE TABLE IF NOT EXISTS registered_groups (
|
|
jid TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
folder TEXT NOT NULL,
|
|
trigger_pattern TEXT NOT NULL,
|
|
added_at TEXT NOT NULL,
|
|
agent_config TEXT,
|
|
requires_trigger INTEGER DEFAULT 1,
|
|
is_main INTEGER DEFAULT 0,
|
|
agent_type TEXT NOT NULL DEFAULT 'claude-code',
|
|
work_dir TEXT,
|
|
PRIMARY KEY (jid, agent_type),
|
|
UNIQUE (folder, agent_type)
|
|
)`);
|
|
return db;
|
|
}
|
|
|
|
describe('parameterized SQL registration', () => {
|
|
let db: Database;
|
|
|
|
beforeEach(() => {
|
|
db = createTestDb();
|
|
});
|
|
|
|
it('registers a group with parameterized query', () => {
|
|
db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
).run(
|
|
'123@g.us',
|
|
'Test Group',
|
|
'test-group',
|
|
'@Andy',
|
|
'2024-01-01T00:00:00.000Z',
|
|
1,
|
|
);
|
|
|
|
const row = db
|
|
.prepare('SELECT * FROM registered_groups WHERE jid = ?')
|
|
.get('123@g.us') as {
|
|
jid: string;
|
|
name: string;
|
|
folder: string;
|
|
trigger_pattern: string;
|
|
requires_trigger: number;
|
|
};
|
|
|
|
expect(row.jid).toBe('123@g.us');
|
|
expect(row.name).toBe('Test Group');
|
|
expect(row.folder).toBe('test-group');
|
|
expect(row.trigger_pattern).toBe('@Andy');
|
|
expect(row.requires_trigger).toBe(1);
|
|
});
|
|
|
|
it('handles apostrophes in group names safely', () => {
|
|
const name = "O'Brien's Group";
|
|
|
|
db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
).run(
|
|
'456@g.us',
|
|
name,
|
|
'obriens-group',
|
|
'@Andy',
|
|
'2024-01-01T00:00:00.000Z',
|
|
0,
|
|
);
|
|
|
|
const row = db
|
|
.prepare('SELECT name FROM registered_groups WHERE jid = ?')
|
|
.get('456@g.us') as {
|
|
name: string;
|
|
};
|
|
|
|
expect(row.name).toBe(name);
|
|
});
|
|
|
|
it('prevents SQL injection in JID field', () => {
|
|
const maliciousJid = "'; DROP TABLE registered_groups; --";
|
|
|
|
db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
).run(maliciousJid, 'Evil', 'evil', '@Andy', '2024-01-01T00:00:00.000Z', 1);
|
|
|
|
// Table should still exist and have the row
|
|
const count = db
|
|
.prepare('SELECT COUNT(*) as count FROM registered_groups')
|
|
.get() as {
|
|
count: number;
|
|
};
|
|
expect(count.count).toBe(1);
|
|
|
|
const row = db.prepare('SELECT jid FROM registered_groups').get() as {
|
|
jid: string;
|
|
};
|
|
expect(row.jid).toBe(maliciousJid);
|
|
});
|
|
|
|
it('handles requiresTrigger=false', () => {
|
|
db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
).run(
|
|
'dc:789',
|
|
'Personal',
|
|
'main',
|
|
'@Andy',
|
|
'2024-01-01T00:00:00.000Z',
|
|
0,
|
|
);
|
|
|
|
const row = db
|
|
.prepare('SELECT requires_trigger FROM registered_groups WHERE jid = ?')
|
|
.get('dc:789') as { requires_trigger: number };
|
|
|
|
expect(row.requires_trigger).toBe(0);
|
|
});
|
|
|
|
it('stores is_main flag', () => {
|
|
db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
|
|
).run(
|
|
'dc:789',
|
|
'Personal',
|
|
'discord_main',
|
|
'@Andy',
|
|
'2024-01-01T00:00:00.000Z',
|
|
0,
|
|
1,
|
|
);
|
|
|
|
const row = db
|
|
.prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
|
|
.get('dc:789') as { is_main: number };
|
|
|
|
expect(row.is_main).toBe(1);
|
|
});
|
|
|
|
it('defaults is_main to 0', () => {
|
|
db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
).run(
|
|
'123@g.us',
|
|
'Some Group',
|
|
'discord_some-group',
|
|
'@Andy',
|
|
'2024-01-01T00:00:00.000Z',
|
|
1,
|
|
);
|
|
|
|
const row = db
|
|
.prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
|
|
.get('123@g.us') as { is_main: number };
|
|
|
|
expect(row.is_main).toBe(0);
|
|
});
|
|
|
|
it('upserts on conflict', () => {
|
|
const stmt = db.prepare(
|
|
`INSERT OR REPLACE INTO registered_groups
|
|
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
);
|
|
|
|
stmt.run(
|
|
'123@g.us',
|
|
'Original',
|
|
'main',
|
|
'@Andy',
|
|
'2024-01-01T00:00:00.000Z',
|
|
1,
|
|
);
|
|
stmt.run(
|
|
'123@g.us',
|
|
'Updated',
|
|
'main',
|
|
'@Bot',
|
|
'2024-02-01T00:00:00.000Z',
|
|
0,
|
|
);
|
|
|
|
const rows = db.prepare('SELECT * FROM registered_groups').all();
|
|
expect(rows).toHaveLength(1);
|
|
|
|
const row = rows[0] as {
|
|
name: string;
|
|
trigger_pattern: string;
|
|
requires_trigger: number;
|
|
};
|
|
expect(row.name).toBe('Updated');
|
|
expect(row.trigger_pattern).toBe('@Bot');
|
|
expect(row.requires_trigger).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('file templating', () => {
|
|
it('replaces assistant name in CLAUDE.md content', () => {
|
|
let content = '# Andy\n\nYou are Andy, a personal assistant.';
|
|
|
|
content = content.replace(/^# Andy$/m, '# Nova');
|
|
content = content.replace(/You are Andy/g, 'You are Nova');
|
|
|
|
expect(content).toBe('# Nova\n\nYou are Nova, a personal assistant.');
|
|
});
|
|
|
|
it('handles names with special regex characters', () => {
|
|
let content = '# Andy\n\nYou are Andy.';
|
|
|
|
const newName = 'C.L.A.U.D.E';
|
|
content = content.replace(/^# Andy$/m, `# ${newName}`);
|
|
content = content.replace(/You are Andy/g, `You are ${newName}`);
|
|
|
|
expect(content).toContain('# C.L.A.U.D.E');
|
|
expect(content).toContain('You are C.L.A.U.D.E.');
|
|
});
|
|
|
|
it('updates .env ASSISTANT_NAME line', () => {
|
|
let envContent = 'SOME_KEY=value\nASSISTANT_NAME="Andy"\nOTHER=test';
|
|
|
|
envContent = envContent.replace(
|
|
/^ASSISTANT_NAME=.*$/m,
|
|
'ASSISTANT_NAME="Nova"',
|
|
);
|
|
|
|
expect(envContent).toContain('ASSISTANT_NAME="Nova"');
|
|
expect(envContent).toContain('SOME_KEY=value');
|
|
});
|
|
|
|
it('appends ASSISTANT_NAME to .env if not present', () => {
|
|
let envContent = 'SOME_KEY=value\n';
|
|
|
|
if (!envContent.includes('ASSISTANT_NAME=')) {
|
|
envContent += '\nASSISTANT_NAME="Nova"';
|
|
}
|
|
|
|
expect(envContent).toContain('ASSISTANT_NAME="Nova"');
|
|
});
|
|
});
|