fix(api): keep event cursor projection state internal

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-03 13:01:17 -06:00
1 parent c921d7c7c7
commit 4348020960
3 files changed
+39 -4

No files matched your search

@@ -384,6 +384,30 @@ describeEmbeddedPostgres("heartbeat run privacy routes", { concurrent: false },
}
});
it("keeps cursor projection state out of public run responses", async () => {
const fixture = await seedFixture();
const resultJson = { summary: "Maintenance complete", eventCursorProjectionSeq: "payload value" };
await db.update(heartbeatRuns).set({
eventCursorProjectionSeq: 41,
eventCursorRebuildPending: true,
resultJson,
}).where(eq(heartbeatRuns.id, fixture.maintenanceRunId));
const app = createBoardApp(fixture.companyId, fixture.ownerUserId);
const detail = await request(app).get(`/api/heartbeat-runs/${fixture.maintenanceRunId}`).expect(200);
const history = await request(app).get(`/api/companies/${fixture.companyId}/heartbeat-runs`).expect(200);
const cancelled = await request(app).post(`/api/heartbeat-runs/${fixture.maintenanceRunId}/cancel`).expect(200);
const listed = history.body.find((run: { id: string }) => run.id === fixture.maintenanceRunId);
for (const response of [detail.body, listed, cancelled.body]) {
expect(response).toMatchObject({ id: fixture.maintenanceRunId, status: "succeeded" });
expect(response).not.toHaveProperty("eventCursorProjectionSeq");
expect(response).not.toHaveProperty("eventCursorRebuildPending");
}
expect(detail.body.resultJson).toEqual(resultJson);
expect(cancelled.body.resultJson).toEqual(resultJson);
const [stored] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.maintenanceRunId));
expect(stored).toMatchObject({ eventCursorProjectionSeq: 41, eventCursorRebuildPending: true, resultJson });
});
it("filters private task links from a readable company run while preserving authorized links", async () => {
const fixture = await seedFixture();
const publicIssueId = randomUUID();
+5 -4
View File
@@ -299,6 +299,7 @@ import {
getHeartbeatRunReadDecisions,
canActorReadWorkspaceOperation,
redactHeartbeatRunListRow,
toPublicHeartbeatRun,
} from "../services/heartbeat-run-privacy.js";
import {
PaperclipRunnerProviderProfileError,
@@ -6330,7 +6331,7 @@ export function agentRoutes(
});
}
res.status(202).json(run);
res.status(202).json(toPublicHeartbeatRun(run));
};
router.post("/agents/:id/wakeup", validate(wakeAgentSchema), async (req, res) => {
@@ -6455,7 +6456,7 @@ export function agentRoutes(
});
}
res.status(202).json(run);
res.status(202).json(toPublicHeartbeatRun(run));
});
router.post("/agents/:id/claude-login", async (req, res) => {
@@ -7168,7 +7169,7 @@ export function agentRoutes(
run.companyId,
run.id,
redactCurrentUserValue(
{ ...decoratedRun, execution: await executionProjectionForRun(db, run.companyId, run.id), identityHistory: await listRunIdentityContexts(db, run.companyId, run.id), retryExhaustedReason, outputSilence: await heartbeat.buildRunOutputSilence(run) },
toPublicHeartbeatRun({ ...decoratedRun, execution: await executionProjectionForRun(db, run.companyId, run.id), identityHistory: await listRunIdentityContexts(db, run.companyId, run.id), retryExhaustedReason, outputSilence: await heartbeat.buildRunOutputSilence(run) }),
await getCurrentUserRedactionOptions(),
),
req.actor.type === "board" ? getActorInfo(req).actorId : null,
@@ -7205,7 +7206,7 @@ export function agentRoutes(
});
}
res.json(run ? await runRedactions.redactForRun(run.companyId, run.id, run,
res.json(run ? await runRedactions.redactForRun(run.companyId, run.id, toPublicHeartbeatRun(run),
getActorInfo(req).actorId) : null);
});
@@ -10,6 +10,16 @@ type RunIssueBinding = {
issueId?: string | null;
};
/** Keep derived ingestion state out of the public run response contract. */
export function toPublicHeartbeatRun<T extends Record<string, unknown>>(run: T) {
const {
eventCursorProjectionSeq: _eventCursorProjectionSeq,
eventCursorRebuildPending: _eventCursorRebuildPending,
...response
} = run;
return response;
}
export async function canActorReadHeartbeatRun(
db: Db,
access: {