mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
fix(server): record the true actor source for a Cloud-initiated task-drain termination
The task-drain audit record used getActorInfo alone, which folds an unlisted actor source into "session". A Cloud-initiated termination carries the "cloud_control" source, so the arm-time and outcome activity records misclassified a destructive, instance-wide stop as an interactive session. Read the verified actor source from the request directly for that one case, and correct the test assertions that pinned the wrong value. Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c2c6ba269d
commit
31e2d0b818
2 files changed
+43
-2
No files matched your search
@@ -473,8 +473,46 @@ describe("the termination option through the production chain", () => {
|
||||
([, input]: [unknown, { action: string }]) => input.action === "instance.task_drain.started",
|
||||
);
|
||||
expect(startCall?.[1].details.initiatingActor).toMatchObject({
|
||||
actorSource: "session",
|
||||
actorSource: "cloud_control",
|
||||
cloudControlRequestId: expect.stringMatching(/^drain-req-live-\d+$/),
|
||||
});
|
||||
});
|
||||
|
||||
it("the_timer_outcome_record_carries_the_verified_cloud_control_actor", async () => {
|
||||
// Fake only the timer functions, so the signed assertion still verifies
|
||||
// against the real clock while the test controls when the deadline fires.
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||
try {
|
||||
const terminateAt = new Date(Date.now() + 30_000);
|
||||
mockHeartbeatService.computeTaskDrain.mockReturnValue({
|
||||
startedAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
terminateActiveTasks: true,
|
||||
terminateAt,
|
||||
});
|
||||
mockHeartbeatService.applyTaskDrain.mockReturnValue(1);
|
||||
mockHeartbeatService.isTaskDrainGenerationLive.mockReturnValue(true);
|
||||
const app = createTaskDrainApp();
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/instance/task-drain")
|
||||
.set(CLOUD_CONTROL_HEADER, freshAssertion("task-drain:start"))
|
||||
.send({ ttlMs: 60_000, terminateActiveTasks: true });
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
mockLogActivity.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
|
||||
const outcomeCall = mockLogActivity.mock.calls.find(
|
||||
([, input]: [unknown, { action: string }]) =>
|
||||
input.action === "instance.task_drain.active_tasks_terminated",
|
||||
);
|
||||
expect(outcomeCall?.[1].details.initiatingActor).toMatchObject({
|
||||
actorSource: "cloud_control",
|
||||
cloudControlRequestId: expect.stringMatching(/^drain-req-live-\d+$/),
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -150,7 +150,10 @@ function buildTaskDrainActorSnapshot(req: Request): TaskDrainActorSnapshot {
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
agentApiKeyId: actor.agentApiKeyId,
|
||||
actorSource: actor.actorSource,
|
||||
// getActorInfo folds every source it does not name into "session", so a
|
||||
// Cloud-initiated request needs its own check here to keep the true
|
||||
// source on the durable record.
|
||||
actorSource: req.actor?.source === "cloud_control" ? "cloud_control" : actor.actorSource,
|
||||
cloudControlRequestId: req.cloudControlRequestId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user