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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## Unreleased

- Fix surface imports recording incorrect run bounds for unordered samples or accepting invalid intermediate timestamps; normalize imported run timestamps to UTC.
2 changes: 2 additions & 0 deletions docs/data-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ Raw Telegram QA artifacts stay in the OpenClaw repo artifact directory unless ex
Release RSS backfills only write `resources` onto an existing Telegram or Discord row and its copied `result.json`. The backfill command asserts the stored RTT `p50` and `p95` values are unchanged before it rewrites that version's JSONL file. RSS is process-level data around the sampled command, not isolated channel transport memory.

RPC rows come only from `scripts/measure-rpc-rtt.mjs`, which starts an isolated loopback Gateway, warms each method once, and measures persistent WebSocket calls such as `health` and `config.get`. Channel request/reply timings are not valid RPC measurements. Control UI rows should be imported with explicit scenario RTT measurements or `control-ui.*` performance events.

Surface imports validate every sample's start and finish timestamps before writing data. A finish may equal its start but must not precede it. Run bounds cover the earliest start through the latest finish, regardless of sample order, and are stored in UTC; sample order and RTT measurements are preserved.
23 changes: 16 additions & 7 deletions scripts/import-surface-rtt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -453,13 +453,22 @@ async function main() {
samples.push(await readSample(entries[index], index + 1, surface.id, scenarioId));
}

const startedAt = samples[0].summary.startedAt;
const finishedAt = samples.at(-1).summary.finishedAt;
const startedAtMs = Date.parse(startedAt);
const finishedAtMs = Date.parse(finishedAt);
if (!Number.isFinite(startedAtMs) || !Number.isFinite(finishedAtMs)) {
throw new Error("Surface RTT sample timestamps must be parseable.");
}
const sampleRunBounds = samples.map((sample) => {
const startedAtMs = Date.parse(sample.summary.startedAt);
const finishedAtMs = Date.parse(sample.summary.finishedAt);
if (
!Number.isFinite(startedAtMs) ||
!Number.isFinite(finishedAtMs) ||
finishedAtMs < startedAtMs
) {
throw new Error(`Surface RTT sample ${sample.index} timestamps must form a valid interval.`);
}
return { startedAtMs, finishedAtMs };
});
const startedAtMs = Math.min(...sampleRunBounds.map((bounds) => bounds.startedAtMs));
const finishedAtMs = Math.max(...sampleRunBounds.map((bounds) => bounds.finishedAtMs));
const startedAt = new Date(startedAtMs).toISOString();
const finishedAt = new Date(finishedAtMs).toISOString();

const runId = buildRunId(startedAt, surface.id, scenarioId, args.spec);
const seen = await existingSurfaceRunIds(surface.id);
Expand Down
73 changes: 73 additions & 0 deletions scripts/surface-rtt.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,76 @@ test("artifact-root input is mutually exclusive with sample-paths TSV input", as
/mutually exclusive/u,
);
});

async function importRpcIntervals(workspace, intervals) {
const paths = [];
for (const [index, [startedAt, finishedAt]] of intervals.entries()) {
const summaryPath = path.join(workspace, `sample-${index + 1}`, "qa-suite-summary.json");
await writeJson(summaryPath, {
counts: { total: 1, passed: 1, failed: 0 },
run: { startedAt, finishedAt },
scenarios: [{
id: "rpc-gateway-smoke",
status: "pass",
rttMeasurement: { finalMatchedReplyRttMs: index + 1, source: "gateway-rpc" },
}],
});
paths.push(summaryPath);
}
const samplesPath = path.join(workspace, "samples.tsv");
await fs.writeFile(samplesPath, `${paths.join("\n")}\n`);
await execFileAsync(process.execPath, [
IMPORT_SCRIPT, samplesPath, "--surface", "rpc", "--spec", "openclaw@main",
"--version", "2026.5.16+intervals", "--require-pass",
], { cwd: workspace });
const [row] = await readJsonl(path.join(workspace, "data/surfaces/rpc/2026.5.16+intervals.jsonl"));
return row;
}

test("surface run bounds cover every sample regardless of TSV order", async (t) => {
const workspace = await makeWorkspace();
t.after(() => fs.rm(workspace, { recursive: true, force: true }));
const row = await importRpcIntervals(workspace, [
["2026-05-16T00:00:10.000Z", "2026-05-16T00:00:11.000Z"],
["2026-05-16T00:00:00.000Z", "2026-05-16T00:00:20.000Z"],
["2026-05-16T00:00:02.000Z", "2026-05-16T00:00:03.000Z"],
]);
assert.equal(row.run.startedAt, "2026-05-16T00:00:00.000Z");
assert.equal(row.run.finishedAt, "2026-05-16T00:00:20.000Z");
assert.equal(row.run.durationMs, 20_000);
assert.deepEqual(row.rtt.warmSamples, [1, 2, 3]);
assert.deepEqual(JSON.parse(await fs.readFile(path.join(workspace, row.artifacts.resultPath))), row);
});

test("surface run timestamps and duplicate detection use canonical UTC", async (t) => {
const workspace = await makeWorkspace();
t.after(() => fs.rm(workspace, { recursive: true, force: true }));
const row = await importRpcIntervals(workspace, [
["2026-05-15T17:00:00-07:00", "2026-05-15T17:00:02-07:00"],
]);
assert.equal(row.run.startedAt, "2026-05-16T00:00:00.000Z");
assert.equal(row.run.finishedAt, "2026-05-16T00:00:02.000Z");
await assert.rejects(importRpcIntervals(workspace, [
["2026-05-16T00:00:00.000Z", "2026-05-16T00:00:02.000Z"],
]), /Surface RTT run already imported/u);
});

test("surface imports reject invalid intermediate intervals before writing rows", async (t) => {
for (const interval of [
["not-a-date", "2026-05-16T00:00:02.000Z"],
["2026-05-16T00:00:01.000Z", "not-a-date"],
["2026-05-16T00:00:02.000Z", "2026-05-16T00:00:01.000Z"],
]) {
await t.test(interval.join(" to "), async (t) => {
const workspace = await makeWorkspace();
t.after(() => fs.rm(workspace, { recursive: true, force: true }));
await assert.rejects(importRpcIntervals(workspace, [
["2026-05-16T00:00:00.000Z", "2026-05-16T00:00:01.000Z"],
interval,
["2026-05-16T00:00:03.000Z", "2026-05-16T00:00:04.000Z"],
]), /Surface RTT sample 2 timestamps must form a valid interval/u);
await assert.rejects(fs.access(path.join(workspace, "data")), { code: "ENOENT" });
await assert.rejects(fs.access(path.join(workspace, "runs")), { code: "ENOENT" });
});
}
});