browser-sqlite-playground / source /teaql-ts /tests /sql-mask-lifecycle.test.ts
philipgreat's picture
Deploy traceable order management playground
aa89cb5 verified
Raw History Blame Contribute Delete
20.4 kB
import { UserContext } from '../src/core/context';
import { SelectQuery } from '../src/core/ast';
import { PLAINTEXT_LOG_ENV, PLAINTEXT_LOG_ACK, projectSQLLog } from '../src/core/log-privacy';
import { AbstractSQLTeaQLClient, EntitySchema, SQLExecutionMetadata, SQLExecutionEvidenceStore,
TextDiagnosticSQLLogSink, SqlQueryResult } from '../src/sql/core';
import { SQLiteDriver } from '../src/sql/sqlite';
import { GraphMutationSession, MutationIntent } from '../src/core/request-intent';
const schema: Record<string, EntitySchema> = { Customer: {
table: 'customer_data', auditMaskFields: ['display_name'], columns: {
id: { columnName: 'id', logicalType: 'integer', decode: 'string', logPolicy: 'plain' },
version: { columnName: 'version', logicalType: 'integer', decode: 'number', logPolicy: 'plain' },
displayName: { columnName: 'display_name', logicalType: 'text', decode: 'native', logPolicy: 'plain' },
publicAddress: { columnName: 'public_address', logicalType: 'text', decode: 'native', logPolicy: 'plain' },
passwordHash: { columnName: 'password_hash', logicalType: 'text', decode: 'native', logPolicy: 'plain' },
},
} };
class Driver extends SQLiteDriver {
fault?: Error;
readbackFault?: Error;
readbackRows?: number;
failStreamAfter?: number;
closedCursor = false;
values: unknown[] = [];
async query(sql: string, values: any[] = []): Promise<SqlQueryResult> {
if (sql.startsWith('SELECT') && sql.includes('customer_data') && sql.includes('WHERE "id" = ?')) {
if (this.readbackFault) throw this.readbackFault;
if (this.readbackRows !== undefined) {
const result = await super.query(sql, values);
return { rowCount: this.readbackRows, rows: Array.from({ length: this.readbackRows }, () => result.rows[0]) };
}
}
if (this.fault && sql.includes('customer_data')) {
this.values = [...values];
throw this.fault;
}
return super.query(sql, values);
}
async *stream(sql: string, values: any[] = []): AsyncIterable<any> {
this.values = [...values];
let seen = 0;
try {
for await (const row of super.stream(sql, values)) {
if (this.failStreamAfter === seen++) throw this.fault;
yield row;
}
} finally { this.closedCursor = true; }
}
}
class Client extends AbstractSQLTeaQLClient {
constructor(driver: Driver) { super(driver, schema); }
}
const previous = process.env[PLAINTEXT_LOG_ENV];
beforeEach(() => { delete process.env[PLAINTEXT_LOG_ENV]; });
afterEach(() => {
if (previous === undefined) delete process.env[PLAINTEXT_LOG_ENV];
else process.env[PLAINTEXT_LOG_ENV] = previous;
});
async function fixture() {
const driver = new Driver(':memory:');
const client = new Client(driver).setDiagnosticSQLLogSink(undefined);
await new UserContext().insertResource('dataService', client).ensureSchema();
for (let id = 1; id <= 3; id++) await client.executeMutation({
entity: 'Customer', action: 'Create', id: String(id),
payload: { displayName: 'Riverside', publicAddress: '1 Runtime Road', passwordHash: 'PASSWORD-CANARY' },
comment: 'seed lifecycle fixture',
});
const logs: SQLExecutionMetadata[] = [], output: string[] = [];
const evidence = new SQLExecutionEvidenceStore();
client.setRuntimeTelemetrySink(evidence).setDiagnosticSQLLogSink({ write(entry) {
logs.push(entry); new TextDiagnosticSQLLogSink(line => output.push(line)).write(entry);
} });
return { driver, client, logs, output, evidence };
}
function query() {
return new SelectQuery('Customer').filter({ displayName: { $eq: 'Riverside' },
publicAddress: { $eq: '1 Runtime Road' }, passwordHash: { $eq: 'PASSWORD-CANARY' } })
.limit(10).comment('what: stream matching customers').purpose('why: verify lifecycle masking');
}
function verify(f: Awaited<ReturnType<typeof fixture>>, outcome: string, count?: number) {
expect(f.logs).toHaveLength(1);
const entry = f.logs[0];
expect(entry.executionOutcome).toBe(outcome);
expect(entry.resultCount).toBe(count);
expect(entry.tracePath.length).toBeGreaterThan(0);
const all = JSON.stringify([f.logs, f.output, f.evidence.snapshot()]);
expect(all).not.toContain('Riverside');
expect(all).not.toContain('PASSWORD-CANARY');
expect(all).not.toContain('DRIVER-CANARY');
expect(entry.debugSQL).toContain('Ri*****de');
expect(entry.debugSQL).toContain('1 Runtime Road');
expect(f.output.join('\n')).toContain(`outcome=${outcome}`);
}
it.each(['complete', 'cancel', 'consumer-error', 'provider-error'])(
'logs masked SQL once when the stream ends: %s', async mode => {
const f = await fixture();
const fault = new Error('Riverside DRIVER-CANARY PASSWORD-CANARY');
if (mode === 'provider-error') { f.driver.fault = fault; f.driver.failStreamAfter = 1; }
try {
const consume = async () => {
for await (const rows of f.client.executeForStream(query(), 1)) {
expect(rows[0].displayName).toBe('Riverside');
if (mode === 'cancel') break;
if (mode === 'consumer-error') throw fault;
}
};
if (mode.endsWith('error')) await expect(consume()).rejects.toBe(fault);
else await consume();
verify(f, mode === 'complete' ? 'success' : mode === 'provider-error' ? 'failure' : 'cancelled', mode === 'complete' ? 3 : 1);
expect(f.driver.closedCursor).toBe(true);
expect(f.driver.values).toContain('Riverside');
expect(f.driver.values).toContain('PASSWORD-CANARY');
} finally { await f.client.close(); }
});
it('records an empty stream and honors the query logging switch', async () => {
const f = await fixture();
try {
const empty = query().filter({ id: { $eq: '999' } });
for await (const rows of f.client.executeForStream(empty, 2)) expect(rows).toHaveLength(0);
expect(f.logs).toHaveLength(1);
expect(f.logs[0].resultCount).toBe(0);
f.logs.length = 0; f.evidence.enableAll(); f.client.setQueryLoggingEnabled(false);
for await (const _rows of f.client.executeForStream(query(), 2)) { /* consume */ }
expect(f.logs).toHaveLength(0);
expect(f.evidence.snapshot()).toHaveLength(1);
expect(f.evidence.snapshot()[0].resultCount).toBe(3);
} finally { await f.client.close(); }
});
it('records failed list SQL without logging or changing the original driver error', async () => {
const f = await fixture();
const fault = new Error('Riverside DRIVER-CANARY PASSWORD-CANARY');
f.driver.fault = fault;
try {
await expect(f.client.executeQuery(query())).rejects.toBe(fault);
verify(f, 'failure');
expect(f.logs[0].affectedRows).toBeUndefined();
} finally { await f.client.close(); }
});
it.each(['Create', 'Update', 'Delete'])('records failed %s with audit reason, not exception payload', async action => {
const f = await fixture();
const fault = new Error('Riverside DRIVER-CANARY PASSWORD-CANARY');
f.driver.fault = fault;
try {
const targetID = action === 'Create' ? '4' : '1';
await expect(f.client.executeMutation({ entity: 'Customer', action, id: targetID,
version: 1, payload: { displayName: 'Riverside', publicAddress: '1 Runtime Road', passwordHash: 'PASSWORD-CANARY' },
comment: `verify failed mutation ${targetID}` })).rejects.toBe(fault);
expect(f.logs).toHaveLength(1);
expect(f.logs[0].executionOutcome).toBe('failure');
expect(f.logs[0].auditReason).toBe('verify failed mutation [REDACTED]');
expect(f.logs[0].affectedRows).toBeUndefined();
const all = JSON.stringify([f.logs, f.output, f.evidence.snapshot()]);
expect(all).not.toMatch(/Riverside|PASSWORD-CANARY|DRIVER-CANARY/);
expect(f.logs[0].debugSQL).not.toContain('?');
} finally { await f.client.close(); }
});
it('does not redact a structural row count when the target ID is short', async () => {
const f = await fixture();
try {
await f.client.executeMutation({entity:'Customer',action:'Update',id:'1',version:1,
payload:{publicAddress:'Changed Road'},comment:'update target 1'});
expect(f.logs.map(entry => entry.operation)).toEqual(['update', 'select']);
expect(f.logs[0].auditReason).toBe('update target [REDACTED]');
expect(f.logs[0].resultSummary).toBe('1 rows affected');
expect(f.logs[0].parameters).toContain('1');
} finally { await f.client.close(); }
});
it.each(['Create', 'Update', 'Delete'])(
'scrubs the target id from successful %s audit reason but keeps the typed id', async action => {
const f = await fixture();
try {
if (action !== 'Create') {
await f.client.executeMutation({
entity: 'Customer', action: 'Create', id: '1001',
payload: { displayName: 'Seed', publicAddress: 'Seed Road' },
comment: 'seed audit target',
});
}
const mutation = {
entity: 'Customer', action, id: '1001', version: 1,
payload: action === 'Delete' ? {} : { displayName: 'Changed' },
comment: `${action.toLowerCase()} target 1001`,
};
await f.client.executeMutation(mutation);
const auditTrace = f.client.auditTrace;
const audit = auditTrace[auditTrace.length - 1];
expect(audit?.id).toBe('1001');
expect(audit?.reason).toBe(`${action.toLowerCase()} target [REDACTED]`);
const sqlEntry = f.logs[f.logs.length - 1];
expect(sqlEntry.auditReason).toBe(`${action.toLowerCase()} target [REDACTED]`);
expect(JSON.stringify(sqlEntry.tracePath)).not.toContain('1001');
expect(sqlEntry).not.toHaveProperty('targetID');
expect(sqlEntry.parameters).toContain('1001');
expect(sqlEntry.mutationLineage?.[0].entityId).toBe('1001');
expect(sqlEntry.mutationLineage?.[0].detail).toBe(`${action.toLowerCase()} target [REDACTED]`);
expect(Object.isFrozen(sqlEntry.mutationLineage?.[0])).toBe(true);
expect(mutation.id).toBe('1001');
expect(mutation.comment).toContain('1001');
} finally { await f.client.close(); }
});
it('does not report prefetched but undelivered rows on a stream failure', async () => {
const f = await fixture();
const fault = new Error('DRIVER-CANARY');
f.driver.fault = fault; f.driver.failStreamAfter = 1;
try {
const consume = async () => { for await (const _rows of f.client.executeForStream(query(), 3)) { /* consume */ } };
await expect(consume()).rejects.toBe(fault);
verify(f, 'failure', 0);
expect(f.driver.closedCursor).toBe(true);
} finally { await f.client.close(); }
});
it('keeps credentials private and labels each debug stream/failure record', async () => {
const f = await fixture();
process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK;
try {
for await (const _rows of f.client.executeForStream(query(), 1)) break;
f.driver.fault = new Error('DRIVER-CANARY');
await expect(f.client.executeQuery(query())).rejects.toBe(f.driver.fault);
expect(f.logs).toHaveLength(2);
for (const entry of f.logs) {
expect(entry.debugSQL).toContain('Riverside');
expect(entry.debugSQL).toContain('EXPLICIT OPT-IN');
expect(entry.debugSQL).toContain('NOT REPLAYABLE');
expect(JSON.stringify(entry)).not.toMatch(/PASSWORD-CANARY|DRIVER-CANARY/);
}
} finally { await f.client.close(); }
});
it('does not replace a driver error with a diagnostic-sink error', async () => {
const f = await fixture();
const fault = new Error('DRIVER-CANARY');
f.driver.fault = fault;
f.client.setDiagnosticSQLLogSink({ write() { throw new Error('SINK-FAILURE'); } });
try {
await expect(f.client.executeQuery(query())).rejects.toBe(fault);
f.driver.failStreamAfter = 0;
const consume = async () => { for await (const _rows of f.client.executeForStream(query(), 1)) { /* consume */ } };
await expect(consume()).rejects.toBe(fault);
expect(f.driver.closedCursor).toBe(true);
} finally { await f.client.close(); }
});
it('keeps successful SQL and graph mutation independent of broken diagnostic sinks', async () => {
const f = await fixture();
let evidenceCalls = 0;
let diagnosticCalls = 0;
f.client.setRuntimeTelemetrySink({ record(entry) {
evidenceCalls++;
expect(JSON.stringify(entry)).not.toContain('PASSWORD-CANARY');
throw new Error('EVIDENCE-SINK-FAILURE');
} }).setDiagnosticSQLLogSink({ write(entry) {
diagnosticCalls++;
expect(JSON.stringify(entry)).not.toContain('PASSWORD-CANARY');
throw new Error('DIAGNOSTIC-SINK-FAILURE');
} });
try {
await expect(f.client.executeMutation({
entity: 'Customer', action: 'Create', id: '4',
payload: { displayName: 'Riverside', publicAddress: '1 Runtime Road',
passwordHash: 'PASSWORD-CANARY' },
comment: 'what: create customer despite logging failure',
})).resolves.toMatchObject({ success: true, id: '4' });
const rows = await f.client.executeQuery(query());
expect(rows).toHaveLength(4);
const driverFault = new Error('DRIVER-CANARY');
f.driver.fault = driverFault;
await expect(f.client.executeQuery(query())).rejects.toBe(driverFault);
expect(evidenceCalls).toBeGreaterThan(0);
expect(diagnosticCalls).toBeGreaterThan(0);
} finally { await f.client.close(); }
});
it.each(['Create', 'Update', 'Delete'].flatMap(action => ['error','empty','multiple'].map(mode => ({action,mode}))))(
'retains independent write/readback facts for $action/$mode', async ({action,mode}) => {
const f = await fixture();
const fault = new Error('DRIVER-CANARY Riverside PASSWORD-CANARY');
if (mode === 'error') f.driver.readbackFault = fault;
else f.driver.readbackRows = mode === 'empty' ? 0 : 2;
try {
const mutation = { entity:'Customer', action, id:action === 'Create' ? '4':'1', version:1,
payload:{displayName:'Riverside',passwordHash:'PASSWORD-CANARY'},
comment: action === 'Delete' ? 'what: delete customer' : 'what: write Riverside PASSWORD-CANARY' };
const pending = f.client.executeMutation(mutation);
if (mode === 'error') await expect(pending).rejects.toBe(fault);
else await expect(pending).rejects.toThrow('could not be read back');
expect(f.logs).toHaveLength(2);
expect(f.logs[0].executionOutcome).toBe('success');
expect(f.logs[0].affectedRows).toBe(1);
expect(f.logs[1].operation).toBe('select');
expect(f.logs[1].executionOutcome).toBe(mode === 'error' ? 'failure':'success');
expect(f.logs[1].resultCount).toBe(mode === 'error' ? undefined : mode === 'empty' ? 0:2);
expect(f.logs[1].affectedRows).toBeUndefined();
expect(f.logs[1].auditReason).toContain('what:');
expect(f.logs[1].debugSQL).toContain('SELECT');
expect(f.logs[1].debugSQL).not.toContain('?');
expect(JSON.stringify([f.logs,f.output,f.evidence.snapshot()])).not.toMatch(/Riverside|PASSWORD-CANARY|DRIVER-CANARY/);
f.driver.readbackFault = undefined; f.driver.readbackRows = undefined;
const rows = await f.driver.query('SELECT * FROM customer_data');
expect(rows.rowCount).toBe(3);
expect(rows.rows[0].version).toBe(1);
expect(rows.rows[0].display_name).toBe('Riverside');
} finally { await f.client.close(); }
});
it('preserves original error when the readback diagnostic sink fails', async () => {
const f = await fixture();
const fault = new Error('READBACK-FAILURE');
f.driver.readbackFault = fault;
let observed = 0;
f.client.setDiagnosticSQLLogSink({write(entry) { observed++; if(entry.operation === 'select') throw new Error('SINK-FAILURE'); }});
try {
await expect(f.client.executeMutation({entity:'Customer',action:'Update',id:'1',version:1,payload:{displayName:'Riverside'},comment:'verify failed readback'})).rejects.toBe(fault);
expect(observed).toBe(2);
expect((await f.driver.query('SELECT version FROM customer_data')).rows[0].version).toBe(1);
} finally { await f.client.close(); }
});
it.each(['debug','disabled','success'])('handles readback mode %s without changing business values', async mode => {
const f = await fixture();
if (mode === 'debug') process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK;
if (mode === 'disabled') f.client.setQueryLoggingEnabled(false).setMutationLoggingEnabled(false);
if (mode !== 'success') f.driver.readbackFault = new Error('READBACK-FAILURE');
try {
const pending = f.client.executeMutation({entity:'Customer',action:'Update',id:'1',version:1,
payload:{displayName:'Riverside',passwordHash:'PASSWORD-CANARY'},comment:'what: update Riverside PASSWORD-CANARY'});
if (mode === 'success') {
const result = await pending;
expect(result.persistedRecord?.displayName).toBe('Riverside');
expect(result.persistedRecord?.passwordHash).toBe('PASSWORD-CANARY');
expect(f.logs.map(entry => entry.operation)).toEqual(['update', 'select']);
} else await expect(pending).rejects.toBe(f.driver.readbackFault);
if (mode === 'disabled') {
expect(f.logs).toHaveLength(0);
expect(f.evidence.snapshot().map(entry => entry.executionOutcome)).toEqual(['success', 'failure']);
}
if (mode === 'debug') {
expect(f.logs).toHaveLength(2);
for (const entry of f.logs) {
expect(entry.auditReason).toContain('Riverside');
expect(entry.debugSQL).toContain('EXPLICIT OPT-IN');
expect(JSON.stringify(entry)).not.toContain('PASSWORD-CANARY');
}
delete process.env[PLAINTEXT_LOG_ENV];
for (const entry of f.logs) {
const safe = projectSQLLog(entry);
expect(safe.auditReason).toContain('what: update');
expect(JSON.stringify(safe)).not.toMatch(/Riverside|PASSWORD-CANARY/);
// Copying a record loses the private safe alternative. Unknown inherited
// intent must then fail closed rather than survive in plaintext.
expect(JSON.stringify(projectSQLLog({...entry}))).not.toMatch(/Riverside|PASSWORD-CANARY/);
}
}
} finally { await f.client.close(); }
});
it.each(['write','readback'])('retains partial graph SQL facts and rolls back after %s failure', async phase => {
const f = await fixture();
const fault = new Error('GRAPH-FAILURE');
const create = (id:string, graph?: GraphMutationSession) => {
const mutation = {entity:'Customer',action:'Create',id,
payload:{displayName:'Riverside',passwordHash:'PASSWORD-CANARY'},comment:'what: insert Riverside PASSWORD-CANARY'};
return f.client.executeMutation(graph ? graph.request(mutation) : mutation);
};
try {
await expect(f.client.executeGraphSave(new MutationIntent('what: insert Riverside PASSWORD-CANARY'), async graph => {
await create('4', graph);
if(phase === 'write') f.driver.fault = fault;
else f.driver.readbackFault = fault;
await create('5', graph);
await create('6', graph);
})).rejects.toBe(fault);
expect(f.logs.map(entry => entry.executionOutcome)).toEqual(phase === 'write'
? ['success','success','failure'] : ['success','success','success','failure']);
expect(f.logs[1].operation).toBe('select');
expect(f.logs[1].resultCount).toBe(1);
expect(JSON.stringify([f.logs,f.output,f.evidence.snapshot()])).not.toMatch(/Riverside|PASSWORD-CANARY|GRAPH-FAILURE/);
f.driver.fault = undefined; f.driver.readbackFault = undefined;
expect((await f.driver.query('SELECT * FROM customer_data')).rowCount).toBe(3);
f.logs.length = 0;
await create('7');
expect(f.logs.map(entry => entry.operation)).toEqual(['insert', 'select']);
expect((await f.driver.query('SELECT * FROM customer_data')).rowCount).toBe(4);
} finally { await f.client.close(); }
});
it.each(['AbortError','TimeoutError'])('records readback cancellation reported by the driver: %s', async name => {
const f = await fixture();
const fault = new Error('CANCEL-CANARY'); fault.name = name;
f.driver.readbackFault = fault;
try {
await expect(f.client.executeMutation({entity:'Customer',action:'Update',id:'1',version:1,
payload:{displayName:'Riverside'},comment:'what: update Riverside'})).rejects.toBe(fault);
expect(f.logs.map(entry=>entry.executionOutcome)).toEqual(['success','cancelled']);
expect(f.logs[1].resultCount).toBeUndefined();
expect(JSON.stringify(f.logs)).not.toMatch(/Riverside|CANCEL-CANARY/);
expect((await f.driver.query('SELECT version FROM customer_data')).rows[0].version).toBe(1);
} finally { await f.client.close(); }
});