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
11 changes: 10 additions & 1 deletion apps/server/src/engine/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,11 @@ export class AgentService {
: this.matchesPrice(text, Number(monitor.value));
const previouslyMatched = Boolean(task.state.matched);
const shouldNotify = matched && (monitor.condition === "change" || !previouslyMatched);
// A condition becoming true again is a new event, even with identical page text.
// Commit its sequence with the outcome so publication retries still deduplicate.
const alertSequence =
Number(task.state.alertSequence ?? 0) +
(shouldNotify && monitor.condition !== "change" ? 1 : 0);
const nextCheckAt = new Date(Date.now() + monitor.intervalMinutes * 60000).toISOString();
await ctx.guard();
// Worker lease is checked before each publication; monitor control also invalidates that lease.
Expand Down Expand Up @@ -1101,12 +1106,16 @@ export class AgentService {
lastHash: currentHash,
resumingMonitor: false,
matched,
alertSequence,
failures: 0,
notice: shouldNotify
? {
title: monitor.title,
body: `Condition met at ${observation.url}: ${text.slice(0, 240)}`,
key: `monitor:${monitor.id}:${currentHash}`,
key:
monitor.condition === "change"
? `monitor:${monitor.id}:${currentHash}`
: `monitor:${monitor.id}:${alertSequence}:${currentHash}`,
}
: null,
},
Expand Down
48 changes: 48 additions & 0 deletions tests/monitor-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,54 @@ test("a page change stays alertable when the task outcome is lost after the base
assert.match(found[0].body, /One table at 7 pm/);
});

for (const scenario of [
{
condition: "contains",
value: "available now",
pages: ["Sold out", "Available now", "Sold out", "Available now"],
expectedAlerts: 2,
},
{
condition: "price_below",
value: "50",
pages: ["Price: $80", "Price: $40", "Price: $80", "Price: $40"],
expectedAlerts: 2,
},
]) {
test(`${scenario.condition} alerts on recurring changes while deduplicating outcome replay`, async () => {
const monitor = await read<Monitor>(
"/monitors",
{
title: `Recurring ${scenario.condition}`,
url: "sample://availability",
condition: scenario.condition,
value: scenario.value,
intervalMinutes: 1,
},
201,
);
const alerts = async () =>
(await read<AgentNotification[]>("/notifications")).filter(
(item) => item.taskId === monitor.taskId,
);
for (const text of scenario.pages) {
await read("/sample-page", { text });
await read(`/monitors/${monitor.id}/control`, { action: "check" });
await server.agent.worker.tick();
// A restart can publish the same saved notice again; that is not a new alert.
await maintain();
await maintain();
}
assert.equal((await alerts()).length, scenario.expectedAlerts);
for (let i = 0; i < 2; i++) {
await read(`/monitors/${monitor.id}/control`, { action: "check" });
await server.agent.worker.tick();
}
assert.equal((await alerts()).length, scenario.expectedAlerts, "unchanged pages stay quiet");
await read(`/monitors/${monitor.id}/control`, { action: "stop" });
});
}

test("resuming a monitor fails without activating it when the task transition does not commit", async () => {
await read("/sample-page", { text: "No tables available" });
const monitor = await createMonitor("Resume availability");
Expand Down
Loading