Spaces:
Running
Running
File size: 5,643 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 140 141 142 143 144 145 | import { UserContext } from '../src/core/context';
import {
NOOP_RUNTIME_TELEMETRY,
observeRuntimeOperation,
RuntimeOperation,
RuntimeTelemetry,
RuntimeTelemetryScope,
startRuntimeOperation,
} from '../src/core/telemetry';
import { runtimeErrorCategory } from '../src/core/telemetry';
import { SelectQuery } from '../src/core/ast';
import { EntitySchema } from '../src/sql/core';
import { SQLiteTeaQLClient } from '../src/sql/sqlite';
class RecordingTelemetry implements RuntimeTelemetry {
readonly events: Array<{ phase: string; operation?: RuntimeOperation; value?: unknown }> = [];
start(operation: RuntimeOperation): RuntimeTelemetryScope {
this.events.push({ phase: 'start', operation });
return {
success: value => { this.events.push({ phase: 'success', value }); },
failure: value => { this.events.push({ phase: 'failure', value }); },
};
}
}
test('classifies native error types without inspecting messages', () => {
class DatabaseTimeoutError extends Error {}
class PermissionError extends Error {}
class UnknownTeaQLError extends Error {}
expect(runtimeErrorCategory(new DatabaseTimeoutError('secret'))).toBe('timeout');
expect(runtimeErrorCategory(new PermissionError('secret'))).toBe('authorization');
expect(runtimeErrorCategory(new UnknownTeaQLError('timeout in message'))).toBe('internal');
});
test('records one balanced successful lifecycle and removes forbidden attributes', async () => {
const telemetry = new RecordingTelemetry();
const result = await observeRuntimeOperation(
telemetry,
{
family: 'query',
name: 'School.list',
attributes: {
'teaql.entity.type': 'School',
'teaql.entity.id': 'must-not-leave-runtime',
},
},
async () => ['school'],
rows => ({ attributes: { 'teaql.result.cardinality': rows.length } }),
);
expect(result).toEqual(['school']);
expect(telemetry.events.map(event => event.phase)).toEqual(['start', 'success']);
expect(telemetry.events[0].operation?.attributes).toEqual({
'teaql.operation.family': 'query',
'teaql.operation.name': 'School.list',
'teaql.entity.type': 'School',
});
});
test('records failure and rethrows the original application error', async () => {
const telemetry = new RecordingTelemetry();
const original = new Error('database unavailable');
await expect(observeRuntimeOperation(
telemetry,
{ family: 'provider', name: 'sqlite.query' },
async () => { throw original; },
)).rejects.toBe(original);
expect(telemetry.events.map(event => event.phase)).toEqual(['start', 'failure']);
});
test('adapter failures never change application behavior', async () => {
const broken: RuntimeTelemetry = { start: () => { throw new Error('adapter failed'); } };
await expect(observeRuntimeOperation(
broken,
{ family: 'cache', name: 'continuous_page.get' },
async () => 42,
)).resolves.toBe(42);
const completionFailure: RuntimeTelemetry = {
start: () => ({
success: () => { throw new Error('export failed'); },
failure: () => { throw new Error('export failed'); },
}),
};
await expect(observeRuntimeOperation(
completionFailure,
{ family: 'mutation', name: 'School.create' },
async () => 'saved',
)).resolves.toBe('saved');
});
test('completion is idempotent and no-op telemetry is usable', () => {
const telemetry = new RecordingTelemetry();
const scope = startRuntimeOperation(telemetry, { family: 'audit', name: 'School.audit' });
scope.success();
scope.failure(new Error('late'));
expect(telemetry.events.map(event => event.phase)).toEqual(['start', 'success']);
expect(() => NOOP_RUNTIME_TELEMETRY.start({ family: 'tfp', name: 'client' }).success()).not.toThrow();
});
test('SQLite operations emit query, mutation, provider, relation and audit lifecycles', async () => {
const schemas: Record<string, EntitySchema> = {
School: {
table: 'school_data',
columns: {
id: { columnName: 'id', logicalType: 'integer', decode: 'string' },
version: { columnName: 'version', logicalType: 'integer', decode: 'number' },
},
relations: {
students: { targetEntity: 'Student', localKey: 'id', foreignKey: 'schoolId', many: true },
},
},
Student: {
table: 'student_data',
columns: {
id: { columnName: 'id', logicalType: 'integer', decode: 'string' },
version: { columnName: 'version', logicalType: 'integer', decode: 'number' },
schoolId: { columnName: 'school_id', logicalType: 'integer', decode: 'string' },
},
},
};
const telemetry = new RecordingTelemetry();
const client = new SQLiteTeaQLClient(':memory:', schemas)
.setRuntimeTelemetry(telemetry)
.setAuditSink(async () => undefined);
await new UserContext().insertResource('dataService', client).ensureSchema();
await client.executeMutation({
entity: 'School', action: 'Create', id: '1', payload: {}, comment: 'seed school',
});
await client.executeQuery(new SelectQuery('School')
.purpose('telemetry conformance').comment('load schools')
.relationQuery('students', new SelectQuery('Student')));
const starts = telemetry.events
.filter(event => event.phase === 'start')
.map(event => event.operation?.family);
expect(starts).toEqual(expect.arrayContaining([
'mutation', 'provider', 'audit', 'query', 'relation_load',
]));
const terminalCount = telemetry.events.filter(event => event.phase !== 'start').length;
expect(terminalCount).toBe(telemetry.events.filter(event => event.phase === 'start').length);
await client.close();
});
|