mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
test(server): align instance-scoped environment coverage
This commit is contained in:
1 parent
1c6efc41a3
commit
2ffe76fd8d
3 files changed
+79
-115
No files matched your search
@@ -1285,7 +1285,7 @@ describe.sequential("agent permission routes", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("rejects creating an agent with an environment from another company", async () => {
|
||||
it("allows creating an agent with an instance-scoped environment referenced from another company", async () => {
|
||||
const environmentId = "33333333-3333-4333-8333-333333333333";
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: environmentId,
|
||||
@@ -1312,9 +1312,13 @@ describe.sequential("agent permission routes", () => {
|
||||
defaultEnvironmentId: environmentId,
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(res.body.error).toContain("Environment not found");
|
||||
expect(mockAgentService.create).not.toHaveBeenCalled();
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
defaultEnvironmentId: environmentId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects creating an agent with an unsupported default environment driver", async () => {
|
||||
|
||||
@@ -229,7 +229,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lists company-scoped environments", async () => {
|
||||
it("lists instance-scoped environments through the company route alias", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
@@ -241,7 +241,7 @@ describe("environment routes", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(1);
|
||||
expect(mockEnvironmentService.list).toHaveBeenCalledWith("company-1", {
|
||||
expect(mockEnvironmentService.list).toHaveBeenCalledWith({
|
||||
status: undefined,
|
||||
driver: "local",
|
||||
});
|
||||
@@ -308,7 +308,7 @@ describe("environment routes", () => {
|
||||
.toBe("supported");
|
||||
});
|
||||
|
||||
it("redacts config and metadata for unprivileged agent list reads", async () => {
|
||||
it("rejects agent list reads for instance-scoped environments", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
@@ -327,19 +327,12 @@ describe("environment routes", () => {
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "env-1",
|
||||
config: {},
|
||||
metadata: null,
|
||||
configRedacted: true,
|
||||
metadataRedacted: true,
|
||||
}),
|
||||
]);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("board operators");
|
||||
expect(mockEnvironmentService.list).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns full config for privileged environment readers", async () => {
|
||||
it("rejects agent detail reads for instance-scoped environments", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
@@ -358,59 +351,17 @@ describe("environment routes", () => {
|
||||
|
||||
const res = await request(app).get("/api/environments/env-1");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.config).toEqual({ shell: "zsh" });
|
||||
expect(res.body.metadata).toEqual({ source: "manual" });
|
||||
expect(res.body.configRedacted).toBeUndefined();
|
||||
});
|
||||
|
||||
it("redacts config and metadata for unprivileged agent detail reads", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
role: "engineer",
|
||||
permissions: { canCreateAgents: false },
|
||||
});
|
||||
mockAccessService.hasPermission.mockResolvedValue(false);
|
||||
const app = createApp({
|
||||
type: "agent",
|
||||
agentId: "agent-1",
|
||||
companyId: "company-1",
|
||||
source: "agent_key",
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await request(app).get("/api/environments/env-1");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "env-1",
|
||||
config: {},
|
||||
metadata: null,
|
||||
configRedacted: true,
|
||||
metadataRedacted: true,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("board operators");
|
||||
});
|
||||
|
||||
it("creates an environment and logs activity", async () => {
|
||||
const environment = createEnvironment();
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
role: "cto",
|
||||
permissions: { canCreateAgents: true },
|
||||
});
|
||||
mockAccessService.hasPermission.mockResolvedValue(false);
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const app = createApp({
|
||||
type: "agent",
|
||||
agentId: "agent-1",
|
||||
companyId: "company-1",
|
||||
source: "agent_key",
|
||||
runId: "run-1",
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
@@ -423,21 +374,22 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", {
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
|
||||
name: "Local",
|
||||
driver: "local",
|
||||
description: "Current development machine",
|
||||
status: "active",
|
||||
config: { shell: "zsh" },
|
||||
envVars: {},
|
||||
});
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
companyId: "company-1",
|
||||
actorType: "agent",
|
||||
actorId: "agent-1",
|
||||
agentId: "agent-1",
|
||||
runId: "run-1",
|
||||
actorType: "user",
|
||||
actorId: "user-1",
|
||||
agentId: null,
|
||||
runId: null,
|
||||
action: "environment.created",
|
||||
entityType: "environment",
|
||||
entityId: environment.id,
|
||||
@@ -462,11 +414,11 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.error).toBe("A local environment already exists for this company.");
|
||||
expect(res.body.error).toBe("A local environment already exists for this instance.");
|
||||
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows non-admin board users with environments:manage to create environments", async () => {
|
||||
it("rejects non-admin board users even when they have company environment permissions", async () => {
|
||||
const environment = createEnvironment();
|
||||
mockAccessService.canUser.mockResolvedValue(true);
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
@@ -486,15 +438,12 @@ describe("environment routes", () => {
|
||||
config: {},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockAccessService.canUser).toHaveBeenCalledWith(
|
||||
"company-1",
|
||||
"user-1",
|
||||
"environments:manage",
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("Instance admin access required");
|
||||
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects non-admin board users without environments:manage", async () => {
|
||||
it("rejects non-admin board users without instance admin access", async () => {
|
||||
mockAccessService.canUser.mockResolvedValue(false);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
@@ -513,11 +462,11 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("environments:manage");
|
||||
expect(res.body.error).toContain("Instance admin access required");
|
||||
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows agents with explicit environments:manage grants to create environments", async () => {
|
||||
it("rejects agent environment creation even with explicit company grants", async () => {
|
||||
const environment = createEnvironment();
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
@@ -543,13 +492,9 @@ describe("environment routes", () => {
|
||||
config: {},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockAccessService.hasPermission).toHaveBeenCalledWith(
|
||||
"company-1",
|
||||
"agent",
|
||||
"agent-1",
|
||||
"environments:manage",
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("board operators");
|
||||
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects invalid SSH config on create", async () => {
|
||||
@@ -618,7 +563,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
@@ -627,8 +572,9 @@ describe("environment routes", () => {
|
||||
version: "latest",
|
||||
},
|
||||
}),
|
||||
envVars: {},
|
||||
}));
|
||||
expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][1])).not.toContain("super-secret-key");
|
||||
expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("super-secret-key");
|
||||
expect(mockSecretService.create).toHaveBeenCalledWith(
|
||||
"company-1",
|
||||
expect.objectContaining({
|
||||
@@ -760,7 +706,7 @@ describe("environment routes", () => {
|
||||
reuseLease: true,
|
||||
},
|
||||
});
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", {
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
|
||||
name: "Fake plugin Sandbox",
|
||||
driver: "sandbox",
|
||||
status: "active",
|
||||
@@ -770,6 +716,7 @@ describe("environment routes", () => {
|
||||
timeoutMs: 450000,
|
||||
reuseLease: true,
|
||||
},
|
||||
envVars: {},
|
||||
});
|
||||
expect(mockSecretService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -846,7 +793,7 @@ describe("environment routes", () => {
|
||||
reuseLease: true,
|
||||
},
|
||||
});
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", {
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
|
||||
name: "Secure Sandbox",
|
||||
driver: "sandbox",
|
||||
status: "active",
|
||||
@@ -857,8 +804,9 @@ describe("environment routes", () => {
|
||||
timeoutMs: 450000,
|
||||
reuseLease: true,
|
||||
},
|
||||
envVars: {},
|
||||
});
|
||||
expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][1])).not.toContain("test-provider-key");
|
||||
expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("test-provider-key");
|
||||
expect(mockSecretService.create).toHaveBeenCalledWith(
|
||||
"company-1",
|
||||
expect.objectContaining({
|
||||
@@ -990,12 +938,13 @@ describe("environment routes", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
config: environment.config,
|
||||
envVars: {},
|
||||
}));
|
||||
});
|
||||
|
||||
it("rejects unprivileged agent mutations for shared environments", async () => {
|
||||
it("rejects agent mutations for instance-scoped environments", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
@@ -1019,7 +968,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("environments:manage");
|
||||
expect(res.body.error).toContain("board operators");
|
||||
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1094,7 +1043,7 @@ describe("environment routes", () => {
|
||||
expect(mockEnvironmentService.getLeaseById).toHaveBeenCalledWith("lease-1");
|
||||
});
|
||||
|
||||
it("rejects cross-company agent access", async () => {
|
||||
it("rejects agent access regardless of company when environment management is instance-scoped", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([]);
|
||||
const app = createApp({
|
||||
type: "agent",
|
||||
@@ -1107,7 +1056,7 @@ describe("environment routes", () => {
|
||||
const res = await request(app).get("/api/companies/company-1/environments");
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("another company");
|
||||
expect(res.body.error).toContain("board operators");
|
||||
expect(mockEnvironmentService.list).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1125,7 +1074,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/environments/${environment.id}`)
|
||||
.patch(`/api/environments/${environment.id}?companyId=company-1`)
|
||||
.send({
|
||||
status: "archived",
|
||||
config: {
|
||||
@@ -1184,7 +1133,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/environments/${environment.id}`)
|
||||
.patch(`/api/environments/${environment.id}?companyId=company-1`)
|
||||
.send({
|
||||
driver: "local",
|
||||
});
|
||||
@@ -1207,7 +1156,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-1")
|
||||
.patch("/api/environments/env-1?companyId=company-1")
|
||||
.send({
|
||||
driver: "ssh",
|
||||
});
|
||||
@@ -1226,7 +1175,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-1")
|
||||
.patch("/api/environments/env-1?companyId=company-1")
|
||||
.send({
|
||||
driver: "sandbox",
|
||||
config: {
|
||||
@@ -1295,6 +1244,7 @@ describe("environment routes", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, {
|
||||
companyId: null,
|
||||
pluginWorkerManager: undefined,
|
||||
});
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||
@@ -1349,6 +1299,7 @@ describe("environment routes", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.driver).toBe("sandbox");
|
||||
expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, {
|
||||
companyId: null,
|
||||
pluginWorkerManager: undefined,
|
||||
});
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||
@@ -1509,7 +1460,7 @@ describe("environment routes", () => {
|
||||
expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("resolved-provider-key");
|
||||
});
|
||||
|
||||
it("rejects sandbox draft probes when the actor cannot read company secrets", async () => {
|
||||
it("rejects sandbox draft probes for non-admin board users", async () => {
|
||||
mockValidatePluginSandboxProviderConfig.mockResolvedValue({
|
||||
normalizedConfig: {
|
||||
template: "base",
|
||||
@@ -1557,7 +1508,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("secrets:read");
|
||||
expect(res.body.error).toContain("Instance admin access required");
|
||||
expect(mockSecretService.resolveSecretValueForEphemeralAccess).not.toHaveBeenCalled();
|
||||
expect(mockProbeEnvironment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -87,7 +87,6 @@ describeEmbeddedPostgres("environment runtime driver contract", () => {
|
||||
}) {
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const environmentId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
const now = new Date();
|
||||
let config = input.config;
|
||||
@@ -135,16 +134,26 @@ describeEmbeddedPostgres("environment runtime driver contract", () => {
|
||||
},
|
||||
};
|
||||
}
|
||||
await db.insert(environments).values({
|
||||
id: environmentId,
|
||||
companyId,
|
||||
name: `${input.driver} contract`,
|
||||
driver: input.driver,
|
||||
status: "active",
|
||||
config,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const existingLocal =
|
||||
input.driver === "local"
|
||||
? await db.query.environments.findFirst({
|
||||
where: (environment, { eq }) => eq(environment.driver, "local"),
|
||||
})
|
||||
: null;
|
||||
const environmentId = existingLocal?.id ?? randomUUID();
|
||||
if (!existingLocal) {
|
||||
await db.insert(environments).values({
|
||||
id: environmentId,
|
||||
name: `${input.driver} contract`,
|
||||
driver: input.driver,
|
||||
status: "active",
|
||||
config,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
config = (existingLocal.config as Record<string, unknown> | null) ?? {};
|
||||
}
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
|
||||
Reference in new issue
Block a user