mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
Scope workspace readiness to blocker issues
References PAP-10990. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
43edabe37d
commit
a5bae7012d
9 files changed
+287
-75
No files matched your search
@@ -2931,6 +2931,102 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
async function seedSharedWorkspaceDependency() {
|
||||
const companyId = randomUUID();
|
||||
const assigneeAgentId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const blockerId = randomUUID();
|
||||
const dependentId = randomUUID();
|
||||
const foreignIssueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: assigneeAgentId,
|
||||
companyId,
|
||||
name: "QA",
|
||||
role: "qa",
|
||||
status: "active",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Shared workspace project",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Shared workspace",
|
||||
sourceType: "local_path",
|
||||
visibility: "default",
|
||||
isPrimary: true,
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "Shared exec workspace",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
});
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: blockerId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Predecessor",
|
||||
status: "done",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
},
|
||||
{
|
||||
id: dependentId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Dependent",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
assigneeAgentId,
|
||||
},
|
||||
{
|
||||
id: foreignIssueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Foreign in-flight issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
},
|
||||
]);
|
||||
await svc.update(dependentId, { blockedByIssueIds: [blockerId] });
|
||||
|
||||
return {
|
||||
companyId,
|
||||
assigneeAgentId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId,
|
||||
blockerId,
|
||||
dependentId,
|
||||
foreignIssueId,
|
||||
};
|
||||
}
|
||||
|
||||
it("persists blocked-by relations and exposes both blockedBy and blocks summaries", async () => {
|
||||
const companyId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
@@ -3088,86 +3184,82 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
]);
|
||||
});
|
||||
|
||||
it("gates dependents on the workspace-finalize barrier when a done blocker's execution workspace has not synced back", async () => {
|
||||
const companyId = randomUUID();
|
||||
const assigneeAgentId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
it("treats done blockers on a shared workspace as ready while a foreign issue is in-flight", async () => {
|
||||
const {
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
blockerId,
|
||||
dependentId,
|
||||
foreignIssueId,
|
||||
} = await seedSharedWorkspaceDependency();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: assigneeAgentId,
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
name: "QA",
|
||||
role: "qa",
|
||||
status: "active",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Shared workspace project",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Shared workspace",
|
||||
sourceType: "local_path",
|
||||
visibility: "default",
|
||||
isPrimary: true,
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "Shared exec workspace",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
executionWorkspaceId,
|
||||
issueId: foreignIssueId,
|
||||
phase: "worktree_prepare",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:00:00.000Z"),
|
||||
});
|
||||
|
||||
const blockerId = randomUUID();
|
||||
const dependentId = randomUUID();
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: blockerId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Predecessor",
|
||||
status: "done",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
},
|
||||
{
|
||||
await expect(svc.listWakeableBlockedDependents(blockerId)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: dependentId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Dependent",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
assigneeAgentId,
|
||||
},
|
||||
blockerIssueIds: [blockerId],
|
||||
}),
|
||||
]);
|
||||
await svc.update(dependentId, { blockedByIssueIds: [blockerId] });
|
||||
await expect(svc.getDependencyReadiness(dependentId)).resolves.toMatchObject({
|
||||
isDependencyReady: true,
|
||||
pendingFinalizeBlockerIssueIds: [],
|
||||
unresolvedBlockerIssueIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
// A run touched the workspace (prepare phase) but has not yet recorded
|
||||
it("ignores unattributed pre-backfill workspace operations when checking blocker readiness", async () => {
|
||||
const {
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
blockerId,
|
||||
dependentId,
|
||||
} = await seedSharedWorkspaceDependency();
|
||||
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
issueId: null,
|
||||
phase: "worktree_prepare",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:00:00.000Z"),
|
||||
});
|
||||
|
||||
await expect(svc.listWakeableBlockedDependents(blockerId)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: dependentId,
|
||||
blockerIssueIds: [blockerId],
|
||||
}),
|
||||
]);
|
||||
await expect(svc.getDependencyReadiness(dependentId)).resolves.toMatchObject({
|
||||
isDependencyReady: true,
|
||||
pendingFinalizeBlockerIssueIds: [],
|
||||
unresolvedBlockerIssueIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("gates dependents on the blocker's own workspace-finalize barrier until sync-back succeeds", async () => {
|
||||
const {
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
blockerId,
|
||||
dependentId,
|
||||
assigneeAgentId,
|
||||
} = await seedSharedWorkspaceDependency();
|
||||
|
||||
// The blocker touched its workspace but has not yet recorded
|
||||
// workspace_finalize — the dependent must NOT wake.
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
issueId: blockerId,
|
||||
phase: "worktree_prepare",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:00:00.000Z"),
|
||||
@@ -3184,6 +3276,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
issueId: blockerId,
|
||||
phase: "workspace_finalize",
|
||||
status: "failed",
|
||||
startedAt: new Date("2026-05-23T22:05:00.000Z"),
|
||||
@@ -3195,6 +3288,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
issueId: blockerId,
|
||||
phase: "workspace_finalize",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:10:00.000Z"),
|
||||
|
||||
@@ -149,6 +149,7 @@ function createWorkspaceOperationRecorderDouble() {
|
||||
companyId: "company-1",
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: "run-1",
|
||||
issueId: null,
|
||||
phase: input.phase,
|
||||
command: input.command ?? null,
|
||||
cwd: input.cwd ?? null,
|
||||
|
||||
Reference in new issue
Block a user