mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
Fix workspace runtime state reconciliation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
5a9a2a9112
commit
f515f2aa12
9 changed files with 477 additions and 64 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
issues,
|
||||
projectWorkspaces,
|
||||
projects,
|
||||
workspaceRuntimeServices,
|
||||
} from "@paperclipai/db";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
|
|
@ -133,6 +134,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
|||
|
||||
afterEach(async () => {
|
||||
await db.delete(issues);
|
||||
await db.delete(workspaceRuntimeServices);
|
||||
await db.delete(executionWorkspaces);
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(projects);
|
||||
|
|
@ -322,4 +324,136 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
|||
"git_branch_delete",
|
||||
]));
|
||||
}, 20_000);
|
||||
|
||||
it("shows inherited shared project runtime services on shared execution workspaces without duplicating old history", async () => {
|
||||
const companyId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const olderServiceId = randomUUID();
|
||||
const currentServiceId = randomUUID();
|
||||
const reuseKey = `project_workspace:${projectWorkspaceId}:paperclip-dev`;
|
||||
const startedAt = new Date("2026-04-04T17:00:00.000Z");
|
||||
const stoppedAt = new Date("2026-04-04T17:05:00.000Z");
|
||||
const runningAt = new Date("2026-04-04T17:10:00.000Z");
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Workspaces",
|
||||
status: "in_progress",
|
||||
executionWorkspacePolicy: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Primary",
|
||||
sourceType: "local_path",
|
||||
isPrimary: true,
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
metadata: {
|
||||
runtimeConfig: {
|
||||
desiredState: "running",
|
||||
workspaceRuntime: {
|
||||
services: [{ name: "paperclip-dev", command: "pnpm dev" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "shared_workspace",
|
||||
strategyType: "project_primary",
|
||||
name: "Shared workspace",
|
||||
status: "active",
|
||||
providerType: "local_fs",
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
});
|
||||
await db.insert(workspaceRuntimeServices).values([
|
||||
{
|
||||
id: olderServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId: null,
|
||||
issueId: null,
|
||||
scopeType: "project_workspace",
|
||||
scopeId: projectWorkspaceId,
|
||||
serviceName: "paperclip-dev",
|
||||
status: "stopped",
|
||||
lifecycle: "shared",
|
||||
reuseKey,
|
||||
command: "pnpm dev",
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
port: 49195,
|
||||
url: "http://127.0.0.1:49195",
|
||||
provider: "local_process",
|
||||
providerRef: "11111",
|
||||
ownerAgentId: null,
|
||||
startedByRunId: null,
|
||||
lastUsedAt: stoppedAt,
|
||||
startedAt,
|
||||
stoppedAt,
|
||||
stopPolicy: { type: "manual" },
|
||||
healthStatus: "unknown",
|
||||
createdAt: startedAt,
|
||||
updatedAt: stoppedAt,
|
||||
},
|
||||
{
|
||||
id: currentServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId: null,
|
||||
issueId: null,
|
||||
scopeType: "project_workspace",
|
||||
scopeId: projectWorkspaceId,
|
||||
serviceName: "paperclip-dev",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
reuseKey,
|
||||
command: "pnpm dev",
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
port: 49222,
|
||||
url: "http://127.0.0.1:49222",
|
||||
provider: "local_process",
|
||||
providerRef: "22222",
|
||||
ownerAgentId: null,
|
||||
startedByRunId: null,
|
||||
lastUsedAt: runningAt,
|
||||
startedAt: runningAt,
|
||||
stoppedAt: null,
|
||||
stopPolicy: { type: "manual" },
|
||||
healthStatus: "healthy",
|
||||
createdAt: runningAt,
|
||||
updatedAt: runningAt,
|
||||
},
|
||||
]);
|
||||
|
||||
const workspace = await svc.getById(executionWorkspaceId);
|
||||
const listed = await svc.list(companyId, { projectId });
|
||||
|
||||
expect(workspace?.runtimeServices).toHaveLength(1);
|
||||
expect(workspace?.runtimeServices?.[0]).toMatchObject({
|
||||
id: currentServiceId,
|
||||
status: "running",
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId: null,
|
||||
url: "http://127.0.0.1:49222",
|
||||
});
|
||||
expect(listed[0]?.runtimeServices).toHaveLength(1);
|
||||
expect(listed[0]?.runtimeServices?.[0]?.id).toBe(currentServiceId);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
createDb,
|
||||
executionWorkspaces,
|
||||
heartbeatRuns,
|
||||
projectWorkspaces,
|
||||
projects,
|
||||
workspaceRuntimeServices,
|
||||
} from "@paperclipai/db";
|
||||
|
|
@ -30,6 +31,7 @@ import {
|
|||
stopRuntimeServicesForExecutionWorkspace,
|
||||
type RealizedExecutionWorkspace,
|
||||
} from "../services/workspace-runtime.ts";
|
||||
import { writeLocalServiceRegistryRecord } from "../services/local-service-supervisor.ts";
|
||||
import { resolvePaperclipConfigPath } from "../paths.ts";
|
||||
import type { WorkspaceOperation } from "@paperclipai/shared";
|
||||
import type { WorkspaceOperationRecorder } from "../services/workspace-operations.ts";
|
||||
|
|
@ -1416,6 +1418,7 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => {
|
|||
afterEach(async () => {
|
||||
await db.delete(workspaceRuntimeServices);
|
||||
await db.delete(executionWorkspaces);
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(projects);
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(agents);
|
||||
|
|
@ -1530,6 +1533,96 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => {
|
|||
await expect(fetch(service!.url!)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("marks persisted local services stopped when the registry pid is stale", async () => {
|
||||
const companyId = randomUUID();
|
||||
const runtimeServiceId = randomUUID();
|
||||
const startedAt = new Date("2026-04-04T17:00:00.000Z");
|
||||
const updatedAt = new Date("2026-04-04T17:10:00.000Z");
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Runtime reconcile test",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Primary",
|
||||
sourceType: "local_path",
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
isPrimary: true,
|
||||
});
|
||||
await db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId: null,
|
||||
issueId: null,
|
||||
scopeType: "project_workspace",
|
||||
scopeId: projectWorkspaceId,
|
||||
serviceName: "paperclip-dev",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
reuseKey: `project_workspace:${projectWorkspaceId}:paperclip-dev`,
|
||||
command: "pnpm dev",
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
port: 49195,
|
||||
url: "http://127.0.0.1:49195",
|
||||
provider: "local_process",
|
||||
providerRef: "999999",
|
||||
ownerAgentId: null,
|
||||
startedByRunId: null,
|
||||
lastUsedAt: updatedAt,
|
||||
startedAt,
|
||||
stoppedAt: null,
|
||||
stopPolicy: { type: "manual" },
|
||||
healthStatus: "healthy",
|
||||
createdAt: startedAt,
|
||||
updatedAt,
|
||||
});
|
||||
await writeLocalServiceRegistryRecord({
|
||||
version: 1,
|
||||
serviceKey: "workspace-runtime-paperclip-dev-stale",
|
||||
profileKind: "workspace-runtime",
|
||||
serviceName: "paperclip-dev",
|
||||
command: "pnpm dev",
|
||||
cwd: "/tmp/paperclip-primary",
|
||||
envFingerprint: "fingerprint",
|
||||
port: 49195,
|
||||
url: "http://127.0.0.1:49195",
|
||||
pid: 999999,
|
||||
processGroupId: 999999,
|
||||
provider: "local_process",
|
||||
runtimeServiceId,
|
||||
reuseKey: `project_workspace:${projectWorkspaceId}:paperclip-dev`,
|
||||
startedAt: startedAt.toISOString(),
|
||||
lastSeenAt: updatedAt.toISOString(),
|
||||
metadata: null,
|
||||
});
|
||||
|
||||
const result = await reconcilePersistedRuntimeServicesOnStartup(db);
|
||||
|
||||
expect(result).toMatchObject({ reconciled: 1, adopted: 0, stopped: 1 });
|
||||
const persisted = await db
|
||||
.select()
|
||||
.from(workspaceRuntimeServices)
|
||||
.where(eq(workspaceRuntimeServices.id, runtimeServiceId))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(persisted?.status).toBe("stopped");
|
||||
expect(persisted?.stoppedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it("persists controlled execution workspace stops as stopped", async () => {
|
||||
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-stop-persisted-"));
|
||||
const companyId = randomUUID();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue