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();
|
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 () => {
|
it("allows board users to set explicit cheap issue assignee profile overrides", async () => {
|
||||||
const app = await createApp(boardActor());
|
const app = await createApp(boardActor());
|
||||||
|
|
||||||
|
|||||||
@@ -984,6 +984,42 @@ export function issueRoutes(
|
|||||||
return resolveActorSourceTrustForIssue({ db, issue, actor });
|
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(
|
async function resolveAgentTrustForIssue(
|
||||||
input: {
|
input: {
|
||||||
agentId: string | null | undefined;
|
agentId: string | null | undefined;
|
||||||
@@ -4205,9 +4241,16 @@ export function issueRoutes(
|
|||||||
companyId,
|
companyId,
|
||||||
req.body.assigneeAgentId as string | null | undefined,
|
req.body.assigneeAgentId as string | null | undefined,
|
||||||
);
|
);
|
||||||
|
const actor = getActorInfo(req);
|
||||||
|
const runWorkspaceInheritanceSourceIssueId = hasExplicitIssueWorkspaceCreateSelection(req.body)
|
||||||
|
? null
|
||||||
|
: await resolveRunIssueWorkspaceInheritanceSource(companyId, actor);
|
||||||
const createBody = {
|
const createBody = {
|
||||||
...req.body,
|
...req.body,
|
||||||
...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
|
...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
|
||||||
|
...(runWorkspaceInheritanceSourceIssueId
|
||||||
|
? { inheritExecutionWorkspaceFromIssueId: runWorkspaceInheritanceSourceIssueId }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, { companyId }, createBody))) return;
|
if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, { companyId }, createBody))) return;
|
||||||
if (req.body.assigneeAgentId || req.body.assigneeUserId) {
|
if (req.body.assigneeAgentId || req.body.assigneeUserId) {
|
||||||
@@ -4224,7 +4267,6 @@ export function issueRoutes(
|
|||||||
}
|
}
|
||||||
await assertIssueEnvironmentSelection(companyId, createBody.executionWorkspaceSettings?.environmentId);
|
await assertIssueEnvironmentSelection(companyId, createBody.executionWorkspaceSettings?.environmentId);
|
||||||
|
|
||||||
const actor = getActorInfo(req);
|
|
||||||
const executionPolicy = applyActorMonitorScheduledBy(
|
const executionPolicy = applyActorMonitorScheduledBy(
|
||||||
normalizeIssueExecutionPolicy(createBody.executionPolicy),
|
normalizeIssueExecutionPolicy(createBody.executionPolicy),
|
||||||
actor.actorType,
|
actor.actorType,
|
||||||
|
|||||||
Reference in New Issue
Block a user