Interval change keeps unreported samples for the reporter (closes #124)
check / check (push) Canceled after 0s

The interval menu cleared every host's history, and the reporter builds
each report from that history, so the samples recorded since the last
delivered report were never sent. The menu now resets each host with
HostState.reset, which moves the history's entries to clearedHistory,
keeping the last 100 it has cleared, and buildReport reads both. A host
still pending after the reset is sent with the status of its last check.
The per-host marks are unchanged, so a sample a delivered report had is
not sent again, and the page's history still starts over. Reporter is
exported for the new unit tests.

Model: opus-5-5
This commit is contained in:
2026-10-14 10:04:32 +00:00
parent 406e073aee
commit 4d33e484ff
4 changed files with 148 additions and 15 deletions
+93
View File
@@ -14,6 +14,7 @@ import {
latencyClass,
latencyHex,
measureLatency,
Reporter,
tick,
} from "../../web/src/main.js";
@@ -378,6 +379,98 @@ test("a check that fails in the first round, which is discarded, writes nothing
assert.deepEqual(consoleLines("info"), []);
});
// The reports Reporter sent in this test, each as the hosts it sent.
function reportedHosts() {
return fetch.mock.calls.map(
(call) => JSON.parse(call.arguments[1].body).hosts,
);
}
// In the next two tests every report is delivered, and only Target has
// checks to report. target.reset() does to Target what the interval menu
// does to each target.
// The interval changes after the first report. The second report is sent
// before Target's first check since then, and the third after it. A fourth
// would hold no check the first three did not, so it is not sent.
test("checks recorded before an interval change and not yet reported go in the next report, with the status of the last check, and no check is reported twice", async (t) => {
const state = new AppState([
{ name: "Target", url: "https://target.test" },
]);
const target = state.local[0];
const reporter = new Reporter(state, "client", CONFIG.reportInterval);
t.mock.method(globalThis, "fetch", async () => ({ ok: true }));
target.pushSample(1000, { latency: 10, error: null });
await reporter.flush();
target.pushSample(2000, { latency: 20, error: null });
target.pushSample(3000, { latency: null, error: "timeout" });
target.reset();
await reporter.flush();
target.pushSample(4000, { latency: 40, error: null });
await reporter.flush();
await reporter.flush();
const name = "Target";
const url = "https://target.test";
assert.deepEqual(reportedHosts(), [
[
{
name,
url,
status: "online",
history: [{ t: 1000, latency: 10, error: null }],
},
],
[
{
name,
url,
status: "error",
history: [
{ t: 2000, latency: 20, error: null },
{ t: 3000, latency: null, error: "timeout" },
],
},
],
[
{
name,
url,
status: "online",
history: [{ t: 4000, latency: 40, error: null }],
},
],
]);
// The page's history starts over.
assert.deepEqual(target.history, [
{ timestamp: 4000, latency: 40, error: null },
]);
});
// The interval changes twice, after 60 checks each time, before the first
// report. Of the 120 checks the interval menu cleared, the report has the
// last 100 (CONFIG.maxHistoryPoints).
test("of the checks interval changes cleared before they were reported, the next report has the last 100", async (t) => {
const state = new AppState([
{ name: "Target", url: "https://target.test" },
]);
const target = state.local[0];
const reporter = new Reporter(state, "client", CONFIG.reportInterval);
t.mock.method(globalThis, "fetch", async () => ({ ok: true }));
for (let n = 1; n <= 120; n++) {
target.pushSample(n * 1000, { latency: 10, error: null });
if (n % 60 === 0) target.reset();
}
await reporter.flush();
const sent = reportedHosts()[0][0].history;
assert.equal(sent.length, 100);
assert.equal(sent[0].t, 21000);
assert.equal(sent.at(-1).t, 120000);
});
// The page shows &lt; &gt; &quot; &amp; and &#39; in a row's markup as
// < > " & and '.
test(`a target whose name and URL hold < > " & and ' shows those characters in its row`, () => {