mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
Preserve scope on manual heartbeat invokes (#5323)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The agent live-run route lets operators trigger a manual heartbeat invocation so an agent can pick up a specific issue or step out of band > - The current route flow drops the caller's scope (issue/run context) when forwarding the manual invoke into the heartbeat service, so the resulting run loses the targeting the operator specified > - This pull request threads the operator-supplied scope through the manual invoke path on both the server route and the UI client, with a regression test that confirms the scope round-trips > - The benefit is manual heartbeat invokes from the live-run UI actually pick up the scoped issue/run instead of falling through to the agent's default routine ## What Changed - `server/src/routes/agents.ts`: forward the operator-supplied scope into the manual invoke heartbeat service call - `server/src/__tests__/agent-live-run-routes.test.ts`: new test verifying the manual invoke path preserves scope - `ui/src/api/agents.ts`: pass scope through the live-run client API ## Verification - `pnpm vitest run --no-coverage server/src/__tests__/agent-live-run-routes.test.ts` - `pnpm typecheck` clean ## Risks Low. The change is purely additive on the route surface — handlers that did not previously pass scope continue to work; handlers that did pass it now have it preserved instead of dropped. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — new test covers the preserved-scope path - [x] If this change affects the UI, I have included before/after screenshots — N/A (internal API change, no visible UI shift) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
9fb0c73e0a
commit
83e7ecc58e
3 changed files with 151 additions and 24 deletions
|
|
@ -12,6 +12,7 @@ const mockHeartbeatService = vi.hoisted(() => ({
|
|||
getActiveRunIssueSummaryForAgent: vi.fn(),
|
||||
getRunLogAccess: vi.fn(),
|
||||
readLog: vi.fn(),
|
||||
wakeup: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockIssueService = vi.hoisted(() => ({
|
||||
|
|
@ -26,6 +27,8 @@ const mockInstanceSettingsService = vi.hoisted(() => ({
|
|||
listCompanyIds: vi.fn(),
|
||||
}));
|
||||
|
||||
const routeAgentId = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
function registerModuleMocks() {
|
||||
vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js"));
|
||||
|
||||
|
|
@ -210,6 +213,14 @@ describe("agent live run routes", () => {
|
|||
content: "chunk",
|
||||
nextOffset: 5,
|
||||
});
|
||||
mockHeartbeatService.wakeup.mockResolvedValue({
|
||||
id: "run-1",
|
||||
companyId: "company-1",
|
||||
agentId: "agent-1",
|
||||
status: "queued",
|
||||
invocationSource: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a compact active run payload for issue polling", async () => {
|
||||
|
|
@ -524,4 +535,66 @@ describe("agent live run routes", () => {
|
|||
expect(res.body).toHaveLength(4);
|
||||
expect(db.select).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("passes scoped wake fields through the legacy heartbeat invoke route", async () => {
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
(baseUrl) => request(baseUrl)
|
||||
.post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`)
|
||||
.send({
|
||||
reason: "issue_assigned",
|
||||
payload: {
|
||||
issueId: "issue-1",
|
||||
taskId: "issue-1",
|
||||
taskKey: "issue-1",
|
||||
},
|
||||
forceFreshSession: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(202);
|
||||
// The legacy /heartbeat/invoke endpoint forwards only the wake fields the
|
||||
// caller actually supplied so empty-body callers (e.g. e2e suites) match
|
||||
// the original fixed-arg `heartbeat.invoke()` shape exactly. When the
|
||||
// caller supplies reason / payload / forceFreshSession those are
|
||||
// forwarded; idempotencyKey is omitted unless explicitly set.
|
||||
expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(routeAgentId, {
|
||||
source: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
reason: "issue_assigned",
|
||||
payload: {
|
||||
issueId: "issue-1",
|
||||
taskId: "issue-1",
|
||||
taskKey: "issue-1",
|
||||
},
|
||||
requestedByActorType: "user",
|
||||
requestedByActorId: "local-board",
|
||||
contextSnapshot: {
|
||||
triggeredBy: "board",
|
||||
actorId: "local-board",
|
||||
forceFreshSession: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("calls heartbeat.wakeup with the legacy minimal shape when the body is empty", async () => {
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
(baseUrl) => request(baseUrl)
|
||||
.post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`)
|
||||
.send({}),
|
||||
);
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(202);
|
||||
expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(routeAgentId, {
|
||||
source: "on_demand",
|
||||
triggerDetail: "manual",
|
||||
requestedByActorType: "user",
|
||||
requestedByActorId: "local-board",
|
||||
contextSnapshot: {
|
||||
triggeredBy: "board",
|
||||
actorId: "local-board",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue