magnitude-test
Version:
A TypeScript client for running automated UI tests through the Magnitude testing platform
134 lines (133 loc) • 4.61 kB
JavaScript
import logger from '@/logger';
import { calculateCost } from '@/util';
function statusFromState(state) {
return state.failure
? 'failed'
: (state.doneAt ? 'passed' : (state.startedAt ? 'running' : 'pending'));
}
function testToMeta(test) {
return {
id: test.id,
title: test.title,
file: test.filepath
};
}
function buildTestContext(state) {
const now = Date.now();
const startedAt = state.startedAt ?? null;
const doneAt = state.doneAt ?? null;
const elapsedMs = startedAt ? (doneAt ? doneAt - startedAt : now - startedAt) : null;
let steps = 0;
let checks = 0;
let ctxParts = {};
let totalInputTokens = 0;
let totalOutputTokens = 0;
let cost;
if (state.modelUsage && state.modelUsage.length > 0) {
for (const usage of state.modelUsage) {
totalInputTokens += usage.inputTokens;
totalOutputTokens += usage.outputTokens;
}
const firstModelEntry = state.modelUsage[0];
if (firstModelEntry && firstModelEntry.llm) {
const modelKey = typeof firstModelEntry.llm === 'string' ? firstModelEntry.llm : JSON.stringify(firstModelEntry.llm);
cost = calculateCost(modelKey, totalInputTokens, totalOutputTokens);
}
}
if (Array.isArray(state.stepsAndChecks)) {
for (const item of state.stepsAndChecks) {
if (item.variant === 'check')
checks++;
else
steps++;
}
const latest = state.stepsAndChecks[state.stepsAndChecks.length - 1];
if (latest) {
if (latest.variant === 'step') {
if (latest.description) {
ctxParts.step = latest.description;
}
if (Array.isArray(latest.actions) && latest.actions.length > 0) {
const lastAction = latest.actions[latest.actions.length - 1];
if (lastAction?.pretty) {
ctxParts.action = lastAction.pretty;
}
}
if (latest.status === 'failed') {
ctxParts.stepStatus = 'failed';
}
if (latest.thoughts && latest.thoughts.length > 0) {
ctxParts.thought = latest.thoughts[latest.thoughts.length - 1].text;
}
}
else {
if (latest.description) {
ctxParts.check = latest.description;
if (latest.status === 'passed' || latest.status === 'failed') {
ctxParts.checkStatus = latest.status;
}
}
}
}
}
return {
steps,
checks,
startedAt: startedAt ?? undefined,
doneAt: doneAt ?? undefined,
elapsedMs: elapsedMs ?? undefined,
tokens: totalInputTokens > 0 || totalOutputTokens > 0 ? {
input: totalInputTokens,
output: totalOutputTokens,
cost: cost
} : undefined,
context: ctxParts
};
}
export class DebugRenderer {
plain;
constructor(plain = false) {
this.plain = plain;
}
stop() {
return new Promise((resolve, reject) => {
logger.flush((err) => {
if (err)
reject(err);
resolve();
});
});
}
onTestStateUpdated(test, state) {
const status = statusFromState(state);
const testMeta = testToMeta(test);
const { steps, checks, startedAt, doneAt, elapsedMs, tokens, context } = buildTestContext(state);
if (this.plain) {
let message = `${testMeta.file} "${testMeta.title || testMeta.id}": ${status}`;
if (status === 'running') {
if (context.step)
message += ` - step: ${context.step}`;
if (context.action)
message += ` / ${context.action}`;
}
if (status === 'failed' && state.failure) {
message += `\n failure: ${state.failure.message || state.failure}`;
}
console.log(message);
}
const payload = {
test: testMeta,
state: {
status,
startedAt,
doneAt,
elapsedMs
},
progress: { steps, checks },
tokens,
context,
err: state.failure || undefined
};
logger.debug(payload, 'test state');
}
}