test: align workspace fixtures with retained source authorization

Seed active memberships for reused task workspaces, respect retained-source deletion order, and remove a provider-speed race from the E2E oracle while preserving deferred-preparation checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-09 21:19:25 -05:00
1 parent 449d66daa3
commit 4244cdb238
14 files changed
+90 -32

No files matched your search

@@ -246,7 +246,7 @@ const support = await getEmbeddedPostgresTestSupport();
await expect(call(f, "create_task", { title: "Exact scope is not a subtree", idempotencyKey: "exact-child" })).rejects.toThrow(/outside.*boundary/);
});
it("authorizes workspace-derived projects before creating children of a projectless root", async () => {
it("authorizes workspace sources without assigning projects to children of a projectless root", async () => {
const f = await server.fixture({ disableWakeOnDemand: true });
const settings = instanceSettingsService(server.db);
const previous = await settings.getExperimental();
@@ -287,7 +287,7 @@ const support = await getEmbeddedPostgresTestSupport();
if (path.endsWith("/children") && selection.inheritExecutionWorkspaceFromIssueId) continue;
const response = await create(path, selection);
expect(response.status, JSON.stringify(response.body)).toBe(201);
expect(response.body.projectId).toBe(f.projectId);
expect(response.body.projectId).toBe(selection.inheritExecutionWorkspaceFromIssueId ? f.projectId : null);
expect(response.body.executionPolicy.authorizationPolicy.trustBoundary.projectIds).toEqual([f.projectId]);
}
}
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import {
@@ -12,6 +13,7 @@ import {
import {
agents,
companies,
companyMemberships,
createDb,
heartbeatRuns,
agentWakeupRequests,
@@ -87,7 +89,7 @@ describe("durable inbound chat scheduler receipts", () => {
testedAt: new Date(0).toISOString(),
}),
});
}, 30_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(() => {
execute.mockClear();
for (const unregister of unregisterAuthorities.splice(0)) unregister();
@@ -111,6 +113,7 @@ describe("durable inbound chat scheduler receipts", () => {
defaultResponsibleUserId: "board-user",
requireBoardApprovalForNewAgents: false,
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "board-user", status: "active", membershipRole: "member" });
await db.insert(agents).values({
id: agentId,
companyId,
@@ -217,6 +217,14 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
runningProcesses.clear();
});
async function grantFixtureWorkspaceAccess(companyId: string, userIds: string[]) {
// Later admissions reauthorize the retained workspace for the responsible
// user, so synthetic human commenters need real company membership too.
await db.insert(companyMemberships).values(userIds.map(principalId => ({
companyId, principalId, principalType: "user", status: "active", membershipRole: "member",
})));
}
async function readGatewayWakePayload(
payload: Record<string, unknown>,
): Promise<Record<string, unknown>> {
@@ -504,6 +512,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values({
id: agentId,
@@ -1042,6 +1051,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values({
id: agentId,
@@ -1252,6 +1262,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user"]);
await db.insert(agents).values([
{
@@ -1690,6 +1701,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user", "external-user"]);
await db.insert(agents).values({
id: agentId,
@@ -2390,6 +2402,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user", "user-1"]);
await db.insert(agents).values({
id: agentId,
@@ -2983,6 +2996,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user", "user-1"]);
await db.insert(agents).values({
id: agentId,
@@ -3148,6 +3162,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user"]);
await db.insert(agents).values({
id: agentId,
@@ -3313,6 +3328,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => {
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values([
{
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { randomUUID } from "node:crypto";
import { and, eq, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
@@ -93,7 +94,7 @@ describeEmbeddedPostgres("heartbeat resolved dependency wake reconciliation", ()
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-heartbeat-issue-liveness-");
db = createDb(tempDb.connectionString);
}, 30_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(async () => {
// Dependency reconciliation heals missing wakes by enqueuing an
@@ -355,6 +356,7 @@ describeEmbeddedPostgres("heartbeat resolved dependency wake reconciliation", ()
defaultResponsibleUserId: "responsible-user",
requireBoardApprovalForNewAgents: false,
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(agents).values({
id: agentId,
companyId,
@@ -1,7 +1,8 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { agents, companies, createDb, heartbeatRuns, issues } from "@paperclipai/db";
import { agents, companies, companyMemberships, createDb, heartbeatRuns, issues } from "@paperclipai/db";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { heartbeatService } from "../services/heartbeat.js";
@@ -19,7 +20,7 @@ suite("native terminal-run cleanup admission", () => {
beforeAll(async () => {
temporary = await startEmbeddedPostgresTestDatabase("native-cleanup-admission-");
db = createDb(temporary.connectionString);
}, 60_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterAll(async () => {
await db?.$client.end({ timeout: 0 });
await temporary?.cleanup();
@@ -28,6 +29,7 @@ suite("native terminal-run cleanup admission", () => {
it("holds the same task until its native executor settles while allowing unrelated tasks", async () => {
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), otherIssueId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Cleanup admission", issuePrefix: "CLN", defaultResponsibleUserId: "owner" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "owner", status: "active", membershipRole: "member" });
await db.insert(agents).values({ id: agentId, companyId, name: "Worker", role: "engineer", status: "idle", adapterType: "process",
adapterConfig: {}, permissions: {}, runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: true, maxConcurrentRuns: 20 } } });
await db.insert(issues).values([issueId, otherIssueId].map(id => ({ id, companyId, title: "Recovery task", status: "todo", assigneeAgentId: agentId, responsibleUserId: "owner" })));
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import * as conversationContinuation from "../services/conversation-continuation.js";
import { environmentRuntimeService } from "../services/environment-runtime.js";
import * as workspaceRuntime from "../services/workspace-runtime.js";
@@ -515,7 +516,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
createdAt: now,
updatedAt: now,
});
}, 20_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(async () => {
vi.clearAllMocks();
@@ -682,6 +683,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
await db.delete(companySecretBindings);
await db.delete(companySecrets);
try {
await db.delete(companyMemberships);
await db.delete(companies);
break;
} catch (error) {
@@ -1176,6 +1178,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
requireBoardApprovalForNewAgents: false,
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(agents).values({
id: agentId,
companyId,
@@ -1462,6 +1465,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
requireBoardApprovalForNewAgents: false,
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(agents).values({
id: agentId,
companyId,
@@ -1611,7 +1615,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
it.each(["missing_default", "credential_not_shared", "database_error", "unmarked_http_error", "unmarked_forbidden", "provider_error"] as const)(
"preserves AI configuration recovery and reports only unexpected causes: %s", async (cause) => {
const { companyId, agentId, issueId, runId, wakeupRequestId } = await seedQueuedIssueRunFixture();
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" }).onConflictDoNothing();
await db.update(agents).set({ runtimeConfig: {
heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 },
aiConnection: { provider: "openai", method: "api_key", mode: "responsible_user" },
@@ -3378,9 +3382,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
expect(input.workspace.cwd).toBe(
path.join(
await fs.realpath(resolvePaperclipInstanceRoot()),
"chat-workspaces",
"isolated-workspaces",
companyId,
agentId,
issueId,
),
);
@@ -5803,7 +5806,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
mockAdapterExecute.mockClear();
});
it("blocks a git-sensitive local adapter before launch when a project-workspace-linked issue is missing its project id", async () => {
it("blocks a projectless task before launch when its configured source directory is unavailable", async () => {
mockAdapterExecute.mockClear();
const { companyId, agentId, runId, issueId } =
await seedQueuedIssueRunFixture();
@@ -5853,7 +5856,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
errorCode: "workspace_validation_failed",
});
expect(failedRun?.error).toContain(
"linked to a project workspace but has no project id",
"Configured project workspace is unavailable",
);
// The adapter process never started, so no agent could post an issue
// comment. The comment policy is not_applicable and no missing-comment
@@ -5872,8 +5875,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
expect(missingCommentWakeups).toHaveLength(0);
expect(failedRun?.resultJson).toMatchObject({
workspaceValidation: {
reason: "missing_project_id",
adapterType: "codex_local",
reason: "configured_workspace_unavailable",
issueId,
issueProjectId: null,
issueProjectWorkspaceId: projectWorkspaceId,
@@ -6358,7 +6360,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
legacyDispositionEpisode: { id: runId, attempt: 1, maxAttempts: 2 },
dispositionRepairInstruction: LEGACY_DISPOSITION_REPAIR_INSTRUCTION,
});
expect(repair?.status).toBe("succeeded");
expect({ status: repair?.status, errorCode: repair?.errorCode, error: repair?.error })
.toEqual({ status: "succeeded", errorCode: null, error: null });
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0].status).toBe("done");
expect(mockAdapterExecute).toHaveBeenCalledTimes(2);
expect(await db.select().from(issueComments).where(eq(issueComments.issueId, issueId))).not.toEqual(expect.arrayContaining([expect.objectContaining({body: SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY})]));
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
@@ -97,7 +98,7 @@ describeEmbeddedPostgres("heartbeat responsible-user invariant", () => {
}
return baseExecute();
});
}, 20_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(async () => {
mockAdapterExecute.mockClear();
@@ -299,6 +300,7 @@ describeEmbeddedPostgres("heartbeat responsible-user invariant", () => {
const { companyId, agentId } = await seedCompany();
const issueResponsibleUserId = `issue-owner-${randomUUID()}`;
const commenterUserId = `commenter-${randomUUID()}`;
await db.insert(companyMemberships).values([issueResponsibleUserId, commenterUserId].map(principalId => ({ companyId, principalType: "user" as const, principalId, status: "active", membershipRole: "member" })));
const issueId = randomUUID();
await db.insert(issues).values({
id: issueId,
@@ -355,6 +357,7 @@ describeEmbeddedPostgres("heartbeat responsible-user invariant", () => {
const { companyId, agentId } = await seedCompany();
const issueResponsibleUserId = `issue-owner-${randomUUID()}`;
const commenterUserId = `commenter-${randomUUID()}`;
await db.insert(companyMemberships).values([issueResponsibleUserId, commenterUserId].map(principalId => ({ companyId, principalType: "user" as const, principalId, status: "active", membershipRole: "member" })));
const issueId = randomUUID();
const commentId = randomUUID();
await db.insert(issues).values({
@@ -105,7 +105,8 @@ describePostgres("saved Slack invitation command in heartbeat prompts", () => {
const f = await seed();
for (let index = 0; index < 2; index++) {
const input = await turn(f);
expect(input.runtime.sessionId).toBe(index === 0 ? null : "invitation-session");
const completed = await heartbeat.getRun(input.runId);
expect(input.runtime.sessionId, JSON.stringify(completed?.resultJson?.configFreshness)).toBe(index === 0 ? null : "invitation-session");
const context = input.context;
for (const key of ["paperclipTaskMarkdown", "paperclipTaskMarkdownCompact", "paperclipTaskMarkdownAssignment", "paperclipTaskMarkdownAssignmentCompact"]) {
const prompt = String(context[key]);
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
@@ -14,6 +15,7 @@ import {
agentWakeupRequests,
agents,
companies,
companyMemberships,
companySkills,
createDb,
closeRegisteredClients,
@@ -342,6 +344,7 @@ async function seedBranchContainmentRun(
createdAt: now,
updatedAt: now,
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(projects).values({
id: projectId,
companyId,
@@ -880,7 +883,7 @@ describeEmbeddedPostgres("heartbeat workspace branch containment", () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-branch-containment-");
db = createDb(tempDb.connectionString);
}
}, 20_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(async () => {
// Await every in-flight background heartbeat run to quiescence before the
@@ -918,15 +921,16 @@ describeEmbeddedPostgres("heartbeat workspace branch containment", () => {
await deleteHeartbeatRunsForCleanup(db);
await db.delete(issueComments);
await db.delete(issues);
await db.delete(executionWorkspaces);
await db.delete(projectWorkspaces);
await db.delete(projects);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
await db.delete(agents);
await db.delete(workspaceOperations);
await db.delete(executionWorkspaces);
await db.delete(environments);
await db.delete(companySkills);
await db.delete(companyMemberships);
await db.delete(companies);
vi.unstubAllEnvs();
});
@@ -972,6 +976,7 @@ describeEmbeddedPostgres("heartbeat workspace branch containment", () => {
};
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
await db.insert(companies).values({ id: companyId, name: "Retained source", issuePrefix, status: "active", defaultResponsibleUserId: "responsible-user" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(projects).values({ id: projectId, companyId, name: "Repository replacement", status: "active",
executionWorkspacePolicy: { enabled: true, defaultMode: "isolated_workspace", workspaceStrategy: { type: "git_worktree", baseRef: "HEAD" } } });
await db.insert(projectWorkspaces).values([
@@ -1039,19 +1044,23 @@ describeEmbeddedPostgres("heartbeat workspace branch containment", () => {
await heartbeat.resumeQueuedRuns();
const run = await waitForRunToFinish(heartbeat, seeded.runId);
await heartbeat.waitForRunExecutionDrain(seeded.runId);
expect(run).toMatchObject({ status: "failed", errorCode: "workspace_validation_failed" });
expect(run?.resultJson).toMatchObject({ workspaceValidation: { reason: "persisted_workspace_source_conflict",
reasonCode: kind === "explicit_conflict" ? "explicit_project_workspace_conflict"
: kind === "unproven" ? "source_registration_unproven" : "source_scope_mismatch" } });
if (kind === "foreign_company") {
// Company access is checked before inspecting any retained source.
expect(run).toMatchObject({ status: "failed", errorCode: "setup_failed", error: "Task workspace access is no longer available" });
} else {
expect(run).toMatchObject({ status: "failed", errorCode: "workspace_validation_failed" });
expect(run?.resultJson).toMatchObject({ workspaceValidation: { reason: "persisted_workspace_source_conflict",
reasonCode: kind === "explicit_conflict" ? "explicit_project_workspace_conflict" : "source_registration_unproven" } });
}
expect(adapterExecute).not.toHaveBeenCalled();
const [workspace] = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, seeded.executionWorkspaceId));
expect(workspace).toMatchObject({ projectWorkspaceId: null, cwd: seeded.worktree, providerRef: seeded.worktree, branchName: seeded.branch, metadata: seeded.metadata });
const [issue] = await db.select().from(issues).where(eq(issues.id, seeded.issueId));
expect(issue).toMatchObject({ executionWorkspaceId: seeded.executionWorkspaceId,
projectWorkspaceId: kind === "explicit_conflict" ? seeded.newWorkspaceId : null });
// Intentionally malformed cross-company/project FKs also prevent the
// existing issue-update service from changing status. The source gate
// still must fail before dispatch and may never silently repair that FK.
// A different organizational project alone is legal; an unproven source
// registration or a cross-company binding must still fail before dispatch
// without silently replacing the retained files or binding.
if (kind === "explicit_conflict" || kind === "unproven") expect(issue.status).toBe("blocked");
expect(await readFile(path.join(seeded.worktree, "retained.txt"), "utf8")).toBe("unexported original task edits\n");
expect(await readFile(path.join(seeded.replacement, "replacement.txt"), "utf8")).toBe("replacement source must stay untouched\n");
@@ -1073,6 +1082,7 @@ describeEmbeddedPostgres("heartbeat workspace branch containment", () => {
status: "active",
defaultResponsibleUserId: "responsible-user",
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(agents).values({
id: agentId,
companyId,
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -14,6 +15,7 @@ import {
agentWakeupRequests,
agents,
companies,
companyMemberships,
companySkills,
createDb,
documentRevisions,
@@ -166,6 +168,7 @@ async function seedRunTarget(db: Db, repoRoot: string) {
createdAt: new Date(),
updatedAt: new Date(),
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db.insert(projects).values({
id: projectId,
companyId,
@@ -265,7 +268,7 @@ describeEmbeddedPostgres("heartbeat workspace finalization branch guard", () =>
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-finalize-branch-");
db = createDb(tempDb.connectionString);
}, 20_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(async () => {
// Await every in-flight background heartbeat run to quiescence before the
@@ -299,14 +302,15 @@ describeEmbeddedPostgres("heartbeat workspace finalization branch guard", () =>
await deleteHeartbeatRowsAfterActivityLogDrains(db);
await db.delete(issueComments);
await db.delete(issues);
await db.delete(executionWorkspaces);
await db.delete(projectWorkspaces);
await db.delete(projects);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
await db.delete(agents);
await db.delete(executionWorkspaces);
await db.delete(environments);
await db.delete(companySkills);
await db.delete(companyMemberships);
await db.delete(companies);
});
@@ -2468,6 +2468,7 @@ describe("effective run session config freshness", () => {
workspaceConfig: {
requestedMode: "shared_workspace",
effectiveMode: "shared_workspace",
issueSettings: null,
reusableExecutionWorkspaceConfig: null,
existingExecutionWorkspace: null,
},
@@ -2476,6 +2477,7 @@ describe("effective run session config freshness", () => {
workspaceConfig: {
requestedMode: "shared_workspace",
effectiveMode: "shared_workspace",
issueSettings: { mode: "shared_workspace" },
reusableExecutionWorkspaceConfig: {
strategyType: "project_primary",
workspaceGeneration: 1,
@@ -1,3 +1,4 @@
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { once } from "node:events";
@@ -9,6 +10,7 @@ import {
agentWakeupRequests,
agents,
companies,
companyMemberships,
completionContracts,
createDb,
executionWorkspaces,
@@ -260,7 +262,7 @@ describe("P6-25 pre-result native session recovery", () => {
leaseExpiresAt: new Date(0),
})),
]);
}, 30_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterAll(async () => temporary?.cleanup());
@@ -795,6 +797,7 @@ describe.each(["unchanged", "newer_active", "stale_idle"] as const)(
status: "active",
defaultResponsibleUserId: "responsible-user",
});
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member" });
await db
.insert(projects)
.values({
@@ -952,7 +955,7 @@ describe.each(["unchanged", "newer_active", "stale_idle"] as const)(
authorUserId: "responsible-user",
body: "Verify the newest user instruction before completing this task.",
});
}, 30_000);
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterAll(async () => {
if (
+4 -2
View File
@@ -37,9 +37,11 @@ export function gradeTaskWorkspaces(value: TaskWorkspaceObservation) {
check("projectless-task-owned-root", complete && value.projectCount === 0 && value.checkpoints.every(p =>
p.issue.projectId === null && p.workspace.projectId === null && p.confinedTaskRoot && p.issue.executionWorkspaceId === p.workspace.id),
"Task and execution workspace remain projectless, with a confined company/task directory rather than agent home.");
check("immutable-root-and-binding", complete && value.initial !== null && value.initial.repositories === 0 &&
// The provider may enqueue its receipt before the first browser observation.
// Deferred acquisition is checked at the settled first-turn checkpoint below.
check("immutable-root-and-binding", complete && value.initial !== null &&
value.checkpoints.every(p => p.workspace.id === value.initial!.workspaceId && p.workspace.cwd === value.initial!.cwd),
"The independently observed pre-preparation root and binding survive all three admissions.");
"The independently observed initial root and binding survive all three admissions.");
check("task-file-continuity", complete && value.checkpoints.every(p => p.note === `task-files-turn-${p.turn}-${value.nonce}\n`),
"Public execution-workspace file reads match the exact expected bytes after every turn.");
const a = first?.repositories[0], b = second?.repositories[0], c = third?.repositories[0];
+7
View File
@@ -49,6 +49,13 @@ describe("task workspace product E2E", () => {
legacy.checkpoints.forEach(point => { point.run.runtimeMode = "legacy"; delete point.prepareCalls; });
expect(gradeTaskWorkspaces(legacy).filter(check => !check.passed)).toEqual([]);
});
it("accepts a pending receipt queued before the first browser observation", () => {
const observation = valid();
observation.initial!.repositories = 1;
expect(gradeTaskWorkspaces(observation).filter(check => !check.passed)).toEqual([]);
observation.checkpoints[0]!.repositories[0]!.state = "ready";
expect(gradeTaskWorkspaces(observation).find(check => check.id === "deferred-idempotent-acquisition")?.passed).toBe(false);
});
it.each([
["missing evidence", (v: TaskWorkspaceObservation) => { v.checkpoints = []; }],
["agent home", (v: TaskWorkspaceObservation) => { v.checkpoints[0]!.confinedTaskRoot = false; }],