Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/storage/src/__tests__/telemetry-repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ describe('FileTelemetryRepo', () => {
}
});

test('load accepts legacy telemetry files with only known array sections', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-legacy-'));
try {
await writeFile(join(root, 'telemetry.json'), JSON.stringify({ usageRecords: [] }) + '\n', 'utf8');
const repo = createTelemetryRepo(root);

await repo.load();

assert.deepEqual(repo.logs({ range: 'all' }), { rows: [], total: 0 });
assert.deepEqual(repo.listPricingOverrides(), []);
} finally {
await flushWrites();
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
}
});

test('load rejects corrupt telemetry.json without overwriting usage history bytes', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-corrupt-'));
try {
Expand All @@ -194,6 +210,42 @@ describe('FileTelemetryRepo', () => {
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
}
});

test('load rejects wrong telemetry schema without overwriting bytes', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-wrong-schema-'));
try {
const wrongShape = JSON.stringify({ reminders: [] }, null, 2) + '\n';
await writeFile(join(root, 'telemetry.json'), wrongShape, 'utf8');
const repo = createTelemetryRepo(root);

await assert.rejects(
() => repo.load(),
/expected known telemetry sections/,
);
assert.equal(await readFile(join(root, 'telemetry.json'), 'utf8'), wrongShape);
} finally {
await flushWrites();
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
}
});

test('load rejects known telemetry sections with non-array values', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-bad-section-'));
try {
const wrongShape = JSON.stringify({ usageRecords: {} }, null, 2) + '\n';
await writeFile(join(root, 'telemetry.json'), wrongShape, 'utf8');
const repo = createTelemetryRepo(root);

await assert.rejects(
() => repo.load(),
/usageRecords must be an array/,
);
assert.equal(await readFile(join(root, 'telemetry.json'), 'utf8'), wrongShape);
} finally {
await flushWrites();
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
}
});
});

async function withRepo(fn: (repo: ReturnType<typeof createTelemetryRepo>) => Promise<void>): Promise<void> {
Expand Down
26 changes: 22 additions & 4 deletions packages/storage/src/telemetry-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,33 @@ function emptyFile(): TelemetryFile {
}

function normalizeFile(input: unknown): TelemetryFile {
if (!input || typeof input !== 'object') return emptyFile();
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new Error('Invalid telemetry file: expected an object');
}
const value = input as Partial<TelemetryFile>;
const hasKnownSection =
'usageRecords' in value ||
'toolInvocations' in value ||
'pricingOverrides' in value;
if (!hasKnownSection) {
throw new Error('Invalid telemetry file: expected known telemetry sections');
}
assertOptionalArraySection(value, 'usageRecords');
assertOptionalArraySection(value, 'toolInvocations');
assertOptionalArraySection(value, 'pricingOverrides');
return {
usageRecords: Array.isArray(value.usageRecords) ? value.usageRecords.map(normalizeLlmCallRecord) : [],
toolInvocations: Array.isArray(value.toolInvocations) ? value.toolInvocations : [],
pricingOverrides: Array.isArray(value.pricingOverrides) ? value.pricingOverrides : [],
usageRecords: value.usageRecords ? value.usageRecords.map(normalizeLlmCallRecord) : [],
toolInvocations: value.toolInvocations ?? [],
pricingOverrides: value.pricingOverrides ?? [],
};
}

function assertOptionalArraySection<T extends object>(value: T, key: keyof T): void {
if (key in value && !Array.isArray(value[key])) {
throw new Error(`Invalid telemetry file: ${String(key)} must be an array`);
}
}

function normalizeLlmCallRecord(input: unknown): PersistedLlmCallRecord {
const row = input as Partial<PersistedLlmCallRecord>;
const inputTokens = finiteNumber(row.inputTokens) ?? 0;
Expand Down