Spaces:
Running
Running
File size: 5,387 Bytes
aa89cb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import { UserContext } from '../src/core/context';
import {
DelegatingMutationPolicyRegistry,
MutationPolicyError,
} from '../src/core/mutation-policy';
import {
BrowserSQLiteDriver,
BrowserSQLiteTeaQLClient,
BrowserSQLiteWorkerLike,
} from '../src/sql/browser-sqlite';
import {
BrowserSQLiteRequest,
BrowserSQLiteResponse,
} from '../src/sql/browser-sqlite-protocol';
class FakeWorker implements BrowserSQLiteWorkerLike {
readonly requests: BrowserSQLiteRequest[] = [];
terminated = false;
private readonly messageListeners = new Set<(event: MessageEvent<BrowserSQLiteResponse>) => void>();
private readonly errorListeners = new Set<(event: ErrorEvent) => void>();
postMessage(message: BrowserSQLiteRequest): void {
this.requests.push(message);
queueMicrotask(() => {
let result: unknown;
if (message.operation === 'query') {
const sql = String(message.payload?.sql ?? '');
result = sql.startsWith('SELECT')
? { rows: [{ id: 1, name: 'Local' }], rowCount: 1 }
: { rows: [], rowCount: 0 };
}
const event = { data: {
type: 'teaql-browser-sqlite-response', requestId: message.requestId, result,
} } as MessageEvent<BrowserSQLiteResponse>;
for (const listener of this.messageListeners) listener(event);
});
}
addEventListener(type: 'message' | 'error', listener: any): void {
if (type === 'message') this.messageListeners.add(listener);
else this.errorListeners.add(listener);
}
removeEventListener(type: 'message' | 'error', listener: any): void {
if (type === 'message') this.messageListeners.delete(listener);
else this.errorListeners.delete(listener);
}
terminate(): void { this.terminated = true; }
}
describe('BrowserSQLiteDriver', () => {
test('defaults to a deterministic in-memory database', async () => {
const worker = new FakeWorker();
const driver = await BrowserSQLiteDriver.open(worker);
expect(worker.requests[0]).toMatchObject({
operation: 'open',
payload: { storage: 'memory', databaseName: 'teaql-browser.sqlite3' },
});
await driver.close();
expect(worker.terminated).toBe(true);
});
test('supports explicit OPFS and buffered stream compatibility', async () => {
const worker = new FakeWorker();
const driver = await BrowserSQLiteDriver.open(worker, {
storage: 'opfs', databaseName: 'demo.sqlite3', terminateWorkerOnClose: false,
});
const rows: unknown[] = [];
for await (const row of driver.stream('SELECT id, name FROM demo')) rows.push(row);
expect(rows).toEqual([{ id: 1, name: 'Local' }]);
expect(worker.requests[0].payload).toMatchObject({ storage: 'opfs', databaseName: 'demo.sqlite3' });
await driver.close();
expect(worker.terminated).toBe(false);
});
test('serializes a transaction and rolls it back on failure', async () => {
const worker = new FakeWorker();
const driver = await BrowserSQLiteDriver.open(worker);
await expect(driver.transaction(async session => {
await session.query('UPDATE demo SET name = ?', ['Changed']);
throw new Error('reject mutation');
})).rejects.toThrow('reject mutation');
expect(worker.requests.filter(request => request.operation === 'query')
.map(request => request.payload?.sql)).toEqual([
'BEGIN IMMEDIATE', 'UPDATE demo SET name = ?', 'ROLLBACK',
]);
await driver.close();
});
test('reset invalidates schema state and explicitly reconciles it again', async () => {
const worker = new FakeWorker();
const client = await BrowserSQLiteTeaQLClient.open(worker, {});
const context = new UserContext().insertResource('dataService', client);
client.setUserContext(context);
await context.ensureSchema();
await client.reset(context);
expect(worker.requests.filter(request => request.operation === 'reset')).toHaveLength(1);
expect(worker.requests.filter(request =>
request.operation === 'query' && String(request.payload?.sql).includes('teaql_id_space'),
)).toHaveLength(2);
await client.close();
});
test('rejects path-like database names', async () => {
await expect(BrowserSQLiteDriver.open(new FakeWorker(), {
databaseName: '../outside.sqlite3',
})).rejects.toThrow('simple file name');
});
test.each(['memory', 'opfs'] as const)(
'%s profile applies Mutation Policy before sending SQL to the worker',
async storage => {
const worker = new FakeWorker();
const client = await BrowserSQLiteTeaQLClient.open(worker, {}, { storage });
const context = new UserContext().withMutationPolicyRegistry(
new DelegatingMutationPolicyRegistry(() => ({
identity: { policyId: 'browser-demo', version: '1', fingerprint: 'sha256:demo' },
review: () => ({ verdict: 'deny', code: 'DEMO_DENIED', message: 'disabled' }),
})),
);
client.setUserContext(context);
const queriesBefore = worker.requests.filter(request => request.operation === 'query').length;
await expect(client.executeMutation({
entity: 'Demo', action: 'Create', payload: { name: 'blocked' }, comment: 'demo',
})).rejects.toThrow(MutationPolicyError);
expect(worker.requests.filter(request => request.operation === 'query')).toHaveLength(queriesBefore);
await client.close();
},
);
});
|