mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 12:33:05 +01:00
Reviewed and verified at 901c4c4bdc.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
233 lines
8.3 KiB
TypeScript
233 lines
8.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
clearAllHeartbeatRunRuntimeStatuses,
|
|
clearHeartbeatRunRuntimeStatus,
|
|
getHeartbeatRunRuntimeStatus,
|
|
MAX_HEARTBEAT_RUN_RUNTIME_STATUS_MESSAGE_CHARS,
|
|
setHeartbeatRunRuntimeStatus,
|
|
sweepExpiredHeartbeatRunRuntimeStatuses,
|
|
touchHeartbeatRunRuntimeStatus,
|
|
} from "./heartbeat-run-runtime-status.js";
|
|
|
|
describe("heartbeat run runtime status store", () => {
|
|
afterEach(() => {
|
|
clearAllHeartbeatRunRuntimeStatuses();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("stores scoped ephemeral status and expires stale entries", () => {
|
|
const updatedAt = new Date("2026-06-24T00:00:00.000Z");
|
|
const status = setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
phase: "config_sync",
|
|
message: `Syncing workspace with apiKey: "sk-test-secret" ${"x".repeat(300)}`,
|
|
currentToolName: `bash apiKey: "sk-tool-secret" ${"x".repeat(120)}`,
|
|
lastAssistantSnippet: `Reading apiKey: "sk-snippet-secret" ${"x".repeat(300)}`,
|
|
lastEventAt: new Date("2026-06-24T00:00:05.000Z"),
|
|
updatedAt,
|
|
});
|
|
|
|
expect(status?.message).toContain("***REDACTED***");
|
|
expect(status?.message.length).toBeLessThanOrEqual(MAX_HEARTBEAT_RUN_RUNTIME_STATUS_MESSAGE_CHARS);
|
|
expect(status?.currentToolName).toContain("***REDACTED***");
|
|
expect(status?.lastAssistantSnippet).toContain("***REDACTED***");
|
|
expect(status?.lastEventAt).toEqual(new Date("2026-06-24T00:00:05.000Z"));
|
|
expect(getHeartbeatRunRuntimeStatus("run-1", {
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
now: new Date("2026-06-24T00:00:30.000Z"),
|
|
})).toMatchObject({
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
phase: "config_sync",
|
|
currentToolName: expect.stringContaining("***REDACTED***"),
|
|
lastAssistantSnippet: expect.stringContaining("***REDACTED***"),
|
|
lastEventAt: new Date("2026-06-24T00:00:05.000Z"),
|
|
});
|
|
expect(getHeartbeatRunRuntimeStatus("run-1", { companyId: "other-company" })).toBeNull();
|
|
expect(getHeartbeatRunRuntimeStatus("run-1", {
|
|
companyId: "company-1",
|
|
now: new Date("2026-06-24T00:02:00.001Z"),
|
|
})).toBeNull();
|
|
expect(getHeartbeatRunRuntimeStatus("run-1")).toBeNull();
|
|
});
|
|
|
|
it("clears status explicitly", () => {
|
|
setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: null,
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
phase: "finalize",
|
|
message: "Finalizing workspace",
|
|
});
|
|
|
|
expect(clearHeartbeatRunRuntimeStatus("run-1")).toBe(true);
|
|
expect(getHeartbeatRunRuntimeStatus("run-1")).toBeNull();
|
|
});
|
|
|
|
it("touch refreshes timestamps while preserving the existing status context", () => {
|
|
setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
phase: "run_activity",
|
|
message: "Using Bash",
|
|
currentToolName: "Bash",
|
|
lastAssistantSnippet: "Running the tests",
|
|
updatedAt: new Date("2026-06-24T00:00:00.000Z"),
|
|
lastEventAt: new Date("2026-06-24T00:00:00.000Z"),
|
|
});
|
|
|
|
const touched = touchHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
at: new Date("2026-06-24T00:00:45.000Z"),
|
|
});
|
|
|
|
expect(touched).toMatchObject({
|
|
runId: "run-1",
|
|
phase: "run_activity",
|
|
message: "Using Bash",
|
|
currentToolName: "Bash",
|
|
lastAssistantSnippet: "Running the tests",
|
|
updatedAt: new Date("2026-06-24T00:00:45.000Z"),
|
|
lastEventAt: new Date("2026-06-24T00:00:45.000Z"),
|
|
});
|
|
expect(getHeartbeatRunRuntimeStatus("run-1", {
|
|
companyId: "company-1",
|
|
now: new Date("2026-06-24T00:02:00.000Z"),
|
|
})).toMatchObject({ message: "Using Bash" });
|
|
});
|
|
|
|
it("touch does not move timestamps backwards", () => {
|
|
setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: null,
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
phase: "run_activity",
|
|
message: "Using Bash",
|
|
updatedAt: new Date("2026-06-24T00:01:00.000Z"),
|
|
lastEventAt: new Date("2026-06-24T00:01:00.000Z"),
|
|
});
|
|
|
|
const touched = touchHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: null,
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
at: new Date("2026-06-24T00:00:30.000Z"),
|
|
});
|
|
|
|
expect(touched).toMatchObject({
|
|
updatedAt: new Date("2026-06-24T00:01:00.000Z"),
|
|
lastEventAt: new Date("2026-06-24T00:01:00.000Z"),
|
|
});
|
|
});
|
|
|
|
it("touch creates a fallback run_activity status when none is live", () => {
|
|
const created = touchHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
at: new Date("2026-06-24T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(created).toMatchObject({
|
|
runId: "run-1",
|
|
phase: "run_activity",
|
|
message: "Receiving agent output",
|
|
updatedAt: new Date("2026-06-24T00:00:00.000Z"),
|
|
lastEventAt: new Date("2026-06-24T00:00:00.000Z"),
|
|
});
|
|
|
|
// An expired entry is replaced with a fresh fallback rather than revived.
|
|
const expiredTouch = touchHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: "issue-1",
|
|
agentId: "agent-1",
|
|
runId: "run-1",
|
|
at: new Date("2026-06-24T00:05:00.000Z"),
|
|
});
|
|
expect(expiredTouch).toMatchObject({
|
|
phase: "run_activity",
|
|
message: "Receiving agent output",
|
|
updatedAt: new Date("2026-06-24T00:05:00.000Z"),
|
|
});
|
|
});
|
|
|
|
it("cleans unread abandoned statuses on later output without polling or expiring fresh context", () => {
|
|
vi.useFakeTimers({ toFake: ["Date"] });
|
|
const start = new Date("2026-10-30T00:00:00Z");
|
|
vi.setSystemTime(start);
|
|
const write = (runId: string) => setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1", issueId: null, agentId: "agent-1", runId,
|
|
phase: "run_activity", message: "Using Bash",
|
|
});
|
|
write("abandoned");
|
|
write("active");
|
|
vi.setSystemTime(start.getTime() + 60_000);
|
|
touchHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1", issueId: null, agentId: "agent-1", runId: "active",
|
|
});
|
|
vi.setSystemTime(start.getTime() + 90_000);
|
|
write("boundary");
|
|
expect(getHeartbeatRunRuntimeStatus("abandoned")).toMatchObject({ message: "Using Bash" });
|
|
expect(clearHeartbeatRunRuntimeStatus("boundary")).toBe(true);
|
|
vi.setSystemTime(start.getTime() + 90_001);
|
|
touchHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1", issueId: null, agentId: "agent-1", runId: "active",
|
|
});
|
|
expect(clearHeartbeatRunRuntimeStatus("abandoned")).toBe(false);
|
|
expect(getHeartbeatRunRuntimeStatus("active")).toMatchObject({ message: "Using Bash" });
|
|
setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1", issueId: null, agentId: "agent-1", runId: "old-output",
|
|
phase: "run_activity", message: "Delayed output", updatedAt: start,
|
|
});
|
|
vi.setSystemTime(start.getTime() + 90_002);
|
|
write("next-output");
|
|
expect(clearHeartbeatRunRuntimeStatus("old-output")).toBe(true);
|
|
vi.setSystemTime(start.getTime() + 180_002);
|
|
write("new");
|
|
expect(clearHeartbeatRunRuntimeStatus("active")).toBe(false);
|
|
expect(getHeartbeatRunRuntimeStatus("new")).toMatchObject({ message: "Using Bash" });
|
|
});
|
|
|
|
it("sweeps expired statuses without touching fresh entries", () => {
|
|
setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: null,
|
|
agentId: "agent-1",
|
|
runId: "stale-run",
|
|
phase: "git_sync",
|
|
message: "Syncing stale workspace",
|
|
updatedAt: new Date("2026-06-24T00:00:00.000Z"),
|
|
});
|
|
setHeartbeatRunRuntimeStatus({
|
|
companyId: "company-1",
|
|
issueId: null,
|
|
agentId: "agent-1",
|
|
runId: "fresh-run",
|
|
phase: "git_sync",
|
|
message: "Syncing fresh workspace",
|
|
updatedAt: new Date("2026-06-24T00:01:00.000Z"),
|
|
});
|
|
|
|
const now = new Date("2026-06-24T00:01:31.000Z");
|
|
expect(sweepExpiredHeartbeatRunRuntimeStatuses(now)).toBe(1);
|
|
expect(getHeartbeatRunRuntimeStatus("stale-run")).toBeNull();
|
|
expect(getHeartbeatRunRuntimeStatus("fresh-run", { now })).toMatchObject({ runId: "fresh-run" });
|
|
});
|
|
});
|