Fix test failure and address Greptile review comments

- Fix "keeps alive" test: set lastOutputAt to current time so idle
  reaper doesn't kill the test run (seed used a stale date)
- Wire up lastOutputAt and startedAt params in test seed fixture
- Remove dead wasIdle variable in clearDetachedRunWarning
- Fix deferred lastOutputAt flush to use actual last output timestamp
  instead of wall-clock time at flush

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley 2026-03-26 23:33:41 -07:00
parent 942d023148
commit aa88db7238
2 changed files with 11 additions and 7 deletions

View file

@ -129,7 +129,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
processLossRetryCount: input?.processLossRetryCount ?? 0, processLossRetryCount: input?.processLossRetryCount ?? 0,
errorCode: input?.runErrorCode ?? null, errorCode: input?.runErrorCode ?? null,
error: input?.runError ?? null, error: input?.runError ?? null,
startedAt: now, startedAt: input?.startedAt ?? now,
lastOutputAt: input?.lastOutputAt ?? null,
updatedAt: new Date("2026-03-19T00:00:00.000Z"), updatedAt: new Date("2026-03-19T00:00:00.000Z"),
}); });
@ -159,6 +160,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
const { runId, wakeupRequestId } = await seedRunFixture({ const { runId, wakeupRequestId } = await seedRunFixture({
processPid: child.pid ?? null, processPid: child.pid ?? null,
includeIssue: false, includeIssue: false,
lastOutputAt: new Date(),
}); });
const heartbeat = heartbeatService(db); const heartbeat = heartbeatService(db);

View file

@ -1506,7 +1506,6 @@ export function heartbeatService(db: Db) {
.then((rows) => rows[0] ?? null); .then((rows) => rows[0] ?? null);
if (!updated) return null; if (!updated) return null;
const wasIdle = updated.errorCode === null; // errorCode was cleared
await appendRunEvent(updated, await nextRunEventSeq(updated.id), { await appendRunEvent(updated, await nextRunEventSeq(updated.id), {
eventType: "lifecycle", eventType: "lifecycle",
stream: "system", stream: "system",
@ -2509,27 +2508,30 @@ export function heartbeatService(db: Db) {
const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); const currentUserRedactionOptions = await getCurrentUserRedactionOptions();
let lastOutputAtFlushPending = false; let lastOutputAtFlushPending = false;
let lastOutputAtLatest: Date | null = null;
const onLog = async (stream: "stdout" | "stderr", chunk: string) => { const onLog = async (stream: "stdout" | "stderr", chunk: string) => {
const sanitizedChunk = redactCurrentUserText(chunk, currentUserRedactionOptions); const sanitizedChunk = redactCurrentUserText(chunk, currentUserRedactionOptions);
if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk); if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk);
if (stream === "stderr") stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk); if (stream === "stderr") stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk);
const ts = new Date().toISOString(); const ts = new Date().toISOString();
lastOutputAtLatest = new Date(ts);
// Batch lastOutputAt writes — flush at most once per 30 seconds to avoid DB churn // Batch lastOutputAt writes — flush at most once per 30 seconds to avoid DB churn
if (!lastOutputAtFlushPending) { if (!lastOutputAtFlushPending) {
lastOutputAtFlushPending = true; lastOutputAtFlushPending = true;
// Flush immediately on first output
await db.update(heartbeatRuns)
.set({ lastOutputAt: lastOutputAtLatest, updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId));
setTimeout(() => { setTimeout(() => {
lastOutputAtFlushPending = false; lastOutputAtFlushPending = false;
if (!lastOutputAtLatest) return;
db.update(heartbeatRuns) db.update(heartbeatRuns)
.set({ lastOutputAt: new Date(), updatedAt: new Date() }) .set({ lastOutputAt: lastOutputAtLatest, updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId)) .where(eq(heartbeatRuns.id, runId))
.then(() => {}) .then(() => {})
.catch((err) => logger.warn({ err, runId }, "failed to flush lastOutputAt")); .catch((err) => logger.warn({ err, runId }, "failed to flush lastOutputAt"));
}, 30_000); }, 30_000);
// Also flush immediately on first output
await db.update(heartbeatRuns)
.set({ lastOutputAt: new Date(ts), updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId));
} }
if (handle) { if (handle) {