Keep agent-created follow-ups in run workspace
Reviewed and merged for PAP-10871/PAP-10873.\n\nVerification:\n- pnpm vitest run server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts\n- git diff --check origin/master...HEAD\n- GitHub PR checks green before merge
This commit is contained in:
@@ -818,6 +818,67 @@ describe("agent issue mutation checkout ownership", () => {
|
||||
expect(mockIssueService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defaults agent-created root follow-up issues to inherit the current run workspace", async () => {
|
||||
const app = await createApp(
|
||||
ownerActor(),
|
||||
createRunContextDb({
|
||||
issueId,
|
||||
executionWorkspaceId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
}),
|
||||
);
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/issues`)
|
||||
.send({
|
||||
title: "Follow-up in same worktree",
|
||||
projectId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||||
});
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(mockIssueService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
title: "Follow-up in same worktree",
|
||||
inheritExecutionWorkspaceFromIssueId: issueId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves explicit workspace choices on agent-created root issues", async () => {
|
||||
const app = await createApp(
|
||||
ownerActor(),
|
||||
createRunContextDb({
|
||||
issueId,
|
||||
executionWorkspaceId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
}),
|
||||
);
|
||||
|
||||
const explicitExecutionWorkspaceId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/issues`)
|
||||
.send({
|
||||
title: "Explicit different workspace",
|
||||
executionWorkspaceId: explicitExecutionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
});
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(mockIssueService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
title: "Explicit different workspace",
|
||||
executionWorkspaceId: explicitExecutionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
}),
|
||||
);
|
||||
expect(mockIssueService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.not.objectContaining({
|
||||
inheritExecutionWorkspaceFromIssueId: issueId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows board users to set explicit cheap issue assignee profile overrides", async () => {
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
|
||||
@@ -984,6 +984,42 @@ export function issueRoutes(
|
||||
return resolveActorSourceTrustForIssue({ db, issue, actor });
|
||||
}
|
||||
|
||||
function hasExplicitIssueWorkspaceCreateSelection(input: Record<string, unknown>) {
|
||||
return input.parentId !== undefined ||
|
||||
input.inheritExecutionWorkspaceFromIssueId !== undefined ||
|
||||
input.projectWorkspaceId !== undefined ||
|
||||
input.executionWorkspaceId !== undefined ||
|
||||
input.executionWorkspacePreference !== undefined ||
|
||||
input.executionWorkspaceSettings !== undefined;
|
||||
}
|
||||
|
||||
async function resolveRunIssueWorkspaceInheritanceSource(
|
||||
companyId: string,
|
||||
actor: ReturnType<typeof getActorInfo>,
|
||||
): Promise<string | null> {
|
||||
if (actor.actorType !== "agent" || !actor.agentId || !actor.runId) return null;
|
||||
const run = await db
|
||||
.select({
|
||||
agentId: heartbeatRuns.agentId,
|
||||
contextSnapshot: heartbeatRuns.contextSnapshot,
|
||||
})
|
||||
.from(heartbeatRuns)
|
||||
.where(and(
|
||||
eq(heartbeatRuns.id, actor.runId),
|
||||
eq(heartbeatRuns.companyId, companyId),
|
||||
))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
if (!run || run.agentId !== actor.agentId) return null;
|
||||
const context = run.contextSnapshot && typeof run.contextSnapshot === "object"
|
||||
? run.contextSnapshot as Record<string, unknown>
|
||||
: null;
|
||||
if (!context || !readNonEmptyString(context.executionWorkspaceId)) return null;
|
||||
const paperclipIssue = context.paperclipIssue && typeof context.paperclipIssue === "object"
|
||||
? context.paperclipIssue as Record<string, unknown>
|
||||
: null;
|
||||
return readNonEmptyString(context.issueId) ?? readNonEmptyString(paperclipIssue?.id);
|
||||
}
|
||||
|
||||
async function resolveAgentTrustForIssue(
|
||||
input: {
|
||||
agentId: string | null | undefined;
|
||||
@@ -4205,9 +4241,16 @@ export function issueRoutes(
|
||||
companyId,
|
||||
req.body.assigneeAgentId as string | null | undefined,
|
||||
);
|
||||
const actor = getActorInfo(req);
|
||||
const runWorkspaceInheritanceSourceIssueId = hasExplicitIssueWorkspaceCreateSelection(req.body)
|
||||
? null
|
||||
: await resolveRunIssueWorkspaceInheritanceSource(companyId, actor);
|
||||
const createBody = {
|
||||
...req.body,
|
||||
...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
|
||||
...(runWorkspaceInheritanceSourceIssueId
|
||||
? { inheritExecutionWorkspaceFromIssueId: runWorkspaceInheritanceSourceIssueId }
|
||||
: {}),
|
||||
};
|
||||
if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, { companyId }, createBody))) return;
|
||||
if (req.body.assigneeAgentId || req.body.assigneeUserId) {
|
||||
@@ -4224,7 +4267,6 @@ export function issueRoutes(
|
||||
}
|
||||
await assertIssueEnvironmentSelection(companyId, createBody.executionWorkspaceSettings?.environmentId);
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const executionPolicy = applyActorMonitorScheduledBy(
|
||||
normalizeIssueExecutionPolicy(createBody.executionPolicy),
|
||||
actor.actorType,
|
||||
|
||||
Reference in New Issue
Block a user