mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
Stabilize serialized route tests
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -5,6 +5,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const serverRoot = path.join(repoRoot, "server");
|
||||
const serverTestsDir = path.join(repoRoot, "server", "src", "__tests__");
|
||||
const nonServerProjects = [
|
||||
"@paperclipai/shared",
|
||||
@@ -63,6 +64,10 @@ function toRepoPath(file) {
|
||||
return path.relative(repoRoot, file).split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function toServerPath(file) {
|
||||
return path.relative(serverRoot, file).split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function isRouteOrAuthzTest(file) {
|
||||
if (routeTestPattern.test(file)) {
|
||||
return true;
|
||||
@@ -99,10 +104,13 @@ function runVitest(args, label) {
|
||||
|
||||
const routeTests = walk(serverTestsDir)
|
||||
.filter((file) => isRouteOrAuthzTest(toRepoPath(file)))
|
||||
.map((file) => ({ repoPath: toRepoPath(file) }))
|
||||
.map((file) => ({
|
||||
repoPath: toRepoPath(file),
|
||||
serverPath: toServerPath(file),
|
||||
}))
|
||||
.sort((a, b) => a.repoPath.localeCompare(b.repoPath));
|
||||
|
||||
const excludeRouteArgs = routeTests.flatMap((file) => ["--exclude", file.repoPath]);
|
||||
const excludeRouteArgs = routeTests.flatMap((file) => ["--exclude", file.serverPath]);
|
||||
for (const project of nonServerProjects) {
|
||||
runVitest(["--project", project], `non-server project ${project}`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Server } from "node:http";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const agentId = "11111111-1111-4111-8111-111111111111";
|
||||
const companyId = "22222222-2222-4222-8222-222222222222";
|
||||
@@ -20,7 +19,6 @@ const baseAgent = {
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
defaultEnvironmentId: null,
|
||||
budgetMonthlyCents: 0,
|
||||
spentMonthlyCents: 0,
|
||||
pauseReason: null,
|
||||
@@ -61,10 +59,6 @@ const mockBudgetService = vi.hoisted(() => ({
|
||||
upsertPolicy: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironmentService = vi.hoisted(() => ({
|
||||
getById: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockHeartbeatService = vi.hoisted(() => ({
|
||||
listTaskSessions: vi.fn(),
|
||||
resetRuntimeSession: vi.fn(),
|
||||
@@ -97,25 +91,13 @@ const mockLogActivity = vi.hoisted(() => vi.fn());
|
||||
const mockTrackAgentCreated = vi.hoisted(() => vi.fn());
|
||||
const mockGetTelemetryClient = vi.hoisted(() => vi.fn());
|
||||
const mockSyncInstructionsBundleConfigFromFilePath = vi.hoisted(() => vi.fn());
|
||||
const mockEnsureOpenCodeModelConfiguredAndAvailable = vi.hoisted(() => vi.fn());
|
||||
const mockEnvironmentService = vi.hoisted(() => ({}));
|
||||
|
||||
const mockInstanceSettingsService = vi.hoisted(() => ({
|
||||
getGeneral: vi.fn(),
|
||||
}));
|
||||
|
||||
function registerModuleMocks() {
|
||||
vi.doMock("../routes/agents.js", async () => vi.importActual("../routes/agents.js"));
|
||||
vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js"));
|
||||
vi.doMock("../adapters/index.js", async () => vi.importActual("../adapters/index.js"));
|
||||
vi.doMock("../middleware/index.js", async () => vi.importActual("../middleware/index.js"));
|
||||
vi.doMock("@paperclipai/adapter-opencode-local/server", async () => {
|
||||
const actual = await vi.importActual<typeof import("@paperclipai/adapter-opencode-local/server")>("@paperclipai/adapter-opencode-local/server");
|
||||
return {
|
||||
...actual,
|
||||
ensureOpenCodeModelConfiguredAndAvailable: mockEnsureOpenCodeModelConfiguredAndAvailable,
|
||||
};
|
||||
});
|
||||
|
||||
vi.doMock("@paperclipai/shared/telemetry", () => ({
|
||||
trackAgentCreated: mockTrackAgentCreated,
|
||||
trackErrorHandlerCrash: vi.fn(),
|
||||
@@ -219,44 +201,53 @@ function createDbStub(options: { requireBoardApprovalForNewAgents?: boolean } =
|
||||
};
|
||||
}
|
||||
|
||||
let sharedServer: Server | null = null;
|
||||
|
||||
async function closeSharedServer() {
|
||||
if (!sharedServer) return;
|
||||
const server = sharedServer;
|
||||
sharedServer = null;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createApp(actor: Record<string, unknown>, dbOptions: { requireBoardApprovalForNewAgents?: boolean } = {}) {
|
||||
await closeSharedServer();
|
||||
const [{ errorHandler }, { agentRoutes }] = await Promise.all([
|
||||
import("../middleware/index.js"),
|
||||
import("../routes/agents.js"),
|
||||
import("../middleware/index.js") as Promise<typeof import("../middleware/index.js")>,
|
||||
import("../routes/agents.js") as Promise<typeof import("../routes/agents.js")>,
|
||||
]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
(req as any).actor = actor;
|
||||
(req as any).actor = {
|
||||
...actor,
|
||||
companyIds: Array.isArray(actor.companyIds) ? [...actor.companyIds] : actor.companyIds,
|
||||
};
|
||||
next();
|
||||
});
|
||||
app.use("/api", agentRoutes(createDbStub(dbOptions) as any));
|
||||
app.use(errorHandler);
|
||||
sharedServer = app.listen(0, "127.0.0.1");
|
||||
await new Promise<void>((resolve) => {
|
||||
sharedServer?.once("listening", resolve);
|
||||
});
|
||||
return sharedServer;
|
||||
return app;
|
||||
}
|
||||
|
||||
async function requestApp(
|
||||
app: express.Express,
|
||||
buildRequest: (baseUrl: string) => request.Test,
|
||||
) {
|
||||
const { createServer } = await vi.importActual<typeof import("node:http")>("node:http");
|
||||
const server = createServer(app);
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("Expected HTTP server to listen on a TCP port");
|
||||
}
|
||||
return await buildRequest(`http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
if (server.listening) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.sequential("agent permission routes", () => {
|
||||
afterEach(closeSharedServer);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("@paperclipai/shared/telemetry");
|
||||
@@ -316,7 +307,6 @@ describe.sequential("agent permission routes", () => {
|
||||
mockGetTelemetryClient.mockReset();
|
||||
mockSyncInstructionsBundleConfigFromFilePath.mockReset();
|
||||
mockInstanceSettingsService.getGeneral.mockReset();
|
||||
mockEnsureOpenCodeModelConfiguredAndAvailable.mockReset();
|
||||
mockSyncInstructionsBundleConfigFromFilePath.mockImplementation((_agent, config) => config);
|
||||
mockGetTelemetryClient.mockReturnValue({ track: vi.fn() });
|
||||
mockAgentService.getById.mockResolvedValue(baseAgent);
|
||||
@@ -348,7 +338,6 @@ describe.sequential("agent permission routes", () => {
|
||||
mockCompanySkillService.listRuntimeSkillEntries.mockResolvedValue([]);
|
||||
mockCompanySkillService.resolveRequestedSkillKeys.mockImplementation(async (_companyId, requested) => requested);
|
||||
mockBudgetService.upsertPolicy.mockResolvedValue(undefined);
|
||||
mockEnvironmentService.getById.mockResolvedValue(null);
|
||||
mockAgentInstructionsService.materializeManagedBundle.mockImplementation(
|
||||
async (agent: Record<string, unknown>, files: Record<string, string>) => ({
|
||||
bundle: null,
|
||||
@@ -371,9 +360,6 @@ describe.sequential("agent permission routes", () => {
|
||||
mockInstanceSettingsService.getGeneral.mockResolvedValue({
|
||||
censorUsernameInLogs: false,
|
||||
});
|
||||
mockEnsureOpenCodeModelConfiguredAndAvailable.mockResolvedValue([
|
||||
{ id: "opencode/gpt-5-nano", label: "opencode/gpt-5-nano" },
|
||||
]);
|
||||
mockLogActivity.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
@@ -388,7 +374,7 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app).get(`/api/agents/${agentId}`);
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl).get(`/api/agents/${agentId}`));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.adapterConfig).toEqual({});
|
||||
@@ -406,7 +392,7 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app).get(`/api/companies/${companyId}/agents`);
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl).get(`/api/companies/${companyId}/agents`));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([
|
||||
@@ -429,9 +415,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({ title: "Compromised" });
|
||||
.send({ title: "Compromised" }));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
@@ -447,9 +433,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/agents/${agentId}/keys`)
|
||||
.send({ name: "backdoor" });
|
||||
.send({ name: "backdoor" }));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
@@ -465,9 +451,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/agents/${agentId}/wakeup`)
|
||||
.send({});
|
||||
.send({}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
@@ -481,7 +467,7 @@ describe.sequential("agent permission routes", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
adapterConfig: {
|
||||
@@ -490,7 +476,7 @@ describe.sequential("agent permission routes", () => {
|
||||
provisionCommand: "touch /tmp/paperclip-rce",
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("host-executed workspace commands");
|
||||
@@ -506,14 +492,14 @@ describe.sequential("agent permission routes", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
adapterConfig: {
|
||||
instructionsRootPath: "/etc",
|
||||
instructionsEntryFile: "passwd",
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("instructions path or bundle configuration");
|
||||
@@ -529,9 +515,9 @@ describe.sequential("agent permission routes", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}/instructions-path`)
|
||||
.send({ path: "/etc/passwd" });
|
||||
.send({ path: "/etc/passwd" }));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("instructions path or bundle configuration");
|
||||
@@ -549,7 +535,7 @@ describe.sequential("agent permission routes", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agent-hires`)
|
||||
.send({
|
||||
name: "Injected",
|
||||
@@ -559,7 +545,7 @@ describe.sequential("agent permission routes", () => {
|
||||
instructionsRootPath: "/etc",
|
||||
instructionsEntryFile: "passwd",
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("instructions path or bundle configuration");
|
||||
@@ -578,14 +564,14 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Backdoor",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("agents:create");
|
||||
@@ -604,16 +590,16 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
});
|
||||
}));
|
||||
|
||||
expect([200, 201]).toContain(res.status);
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
@@ -642,14 +628,14 @@ describe.sequential("agent permission routes", () => {
|
||||
{ requireBoardApprovalForNewAgents: true },
|
||||
);
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.error).toContain("/agent-hires");
|
||||
@@ -667,14 +653,14 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
});
|
||||
}));
|
||||
|
||||
expect([200, 201]).toContain(res.status);
|
||||
expect(mockAccessService.ensureMembership).toHaveBeenCalledWith(
|
||||
@@ -703,9 +689,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.get(`/api/companies/${companyId}/agents`)
|
||||
.query({ urlKey: "builder" });
|
||||
.query({ urlKey: "builder" }));
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("urlKey");
|
||||
@@ -721,7 +707,7 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
@@ -733,7 +719,7 @@ describe.sequential("agent permission routes", () => {
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
expect([200, 201]).toContain(res.status);
|
||||
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||
@@ -759,7 +745,7 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/companies/${companyId}/agent-hires`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
@@ -771,7 +757,7 @@ describe.sequential("agent permission routes", () => {
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||
@@ -811,9 +797,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/agents/${agentId}/approve`)
|
||||
.send({});
|
||||
.send({}));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockAgentService.activatePendingApproval).toHaveBeenCalledWith(agentId);
|
||||
@@ -837,9 +823,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.post(`/api/agents/${agentId}/approve`)
|
||||
.send({});
|
||||
.send({}));
|
||||
|
||||
expect(res.status).toBe(409);
|
||||
expect(mockAgentService.activatePendingApproval).not.toHaveBeenCalled();
|
||||
@@ -848,524 +834,6 @@ describe.sequential("agent permission routes", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("rejects creating an agent with an environment from another company", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId: "other-company",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(res.body.error).toContain("Environment not found");
|
||||
});
|
||||
|
||||
it("rejects creating an agent with an unsupported non-local default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Builder",
|
||||
role: "engineer",
|
||||
adapterType: "process",
|
||||
adapterConfig: {},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(res.body.error).toContain('Environment driver "ssh" is not allowed here');
|
||||
});
|
||||
|
||||
it("allows creating a codex agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.create.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Codex Builder",
|
||||
role: "engineer",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(res.status);
|
||||
});
|
||||
|
||||
it("allows creating a claude agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.create.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "claude_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Claude Builder",
|
||||
role: "engineer",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
});
|
||||
|
||||
it("allows creating a gemini agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.create.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "gemini_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Gemini Builder",
|
||||
role: "engineer",
|
||||
adapterType: "gemini_local",
|
||||
adapterConfig: {},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
});
|
||||
|
||||
it("allows creating an opencode agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.create.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "opencode_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "OpenCode Builder",
|
||||
role: "engineer",
|
||||
adapterType: "opencode_local",
|
||||
adapterConfig: {
|
||||
model: "opencode/gpt-5-nano",
|
||||
},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
});
|
||||
|
||||
it("allows creating a cursor agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.create.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "cursor",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Cursor Builder",
|
||||
role: "engineer",
|
||||
adapterType: "cursor",
|
||||
adapterConfig: {},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
});
|
||||
|
||||
it("allows creating a pi agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.create.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "pi_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/companies/${companyId}/agents`)
|
||||
.send({
|
||||
name: "Pi Builder",
|
||||
role: "engineer",
|
||||
adapterType: "pi_local",
|
||||
adapterConfig: {
|
||||
model: "openai/gpt-5.4-mini",
|
||||
},
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(res.status);
|
||||
});
|
||||
|
||||
it("rejects updating an agent with an unsupported non-local default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(res.body.error).toContain('Environment driver "ssh" is not allowed here');
|
||||
});
|
||||
|
||||
it("allows updating a codex agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
defaultEnvironmentId: null,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows updating a claude agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "claude_local",
|
||||
defaultEnvironmentId: null,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "claude_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows updating a gemini agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "gemini_local",
|
||||
defaultEnvironmentId: null,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "gemini_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows updating an opencode agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "opencode_local",
|
||||
defaultEnvironmentId: null,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "opencode_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows updating a cursor agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "cursor",
|
||||
defaultEnvironmentId: null,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "cursor",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows updating a pi agent with an SSH default environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "pi_local",
|
||||
defaultEnvironmentId: null,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "pi_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("rejects switching a codex agent away from SSH-capable runtime without clearing its SSH default", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue({
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
companyId,
|
||||
driver: "ssh",
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
defaultEnvironmentId: "33333333-3333-4333-8333-333333333333",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
adapterType: "process",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(res.body.error).toContain('Environment driver "ssh" is not allowed here');
|
||||
});
|
||||
|
||||
it("exposes explicit task assignment access on agent detail", async () => {
|
||||
mockAccessService.listPrincipalGrants.mockResolvedValue([
|
||||
{
|
||||
@@ -1389,7 +857,7 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app).get(`/api/agents/${agentId}`);
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl).get(`/api/agents/${agentId}`));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.access.canAssignTasks).toBe(true);
|
||||
@@ -1410,9 +878,9 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}/permissions`)
|
||||
.send({ canCreateAgents: true, canAssignTasks: false });
|
||||
.send({ canCreateAgents: true, canAssignTasks: false }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockAccessService.setPrincipalPermission).toHaveBeenCalledWith(
|
||||
@@ -1445,9 +913,9 @@ describe.sequential("agent permission routes", () => {
|
||||
source: "agent_key",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.get("/api/agents/me/inbox/mine")
|
||||
.query({ userId: "board-user" });
|
||||
.query({ userId: "board-user" }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([
|
||||
@@ -1482,7 +950,7 @@ describe.sequential("agent permission routes", () => {
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await request(app).post("/api/heartbeat-runs/run-1/cancel").send({});
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl).post("/api/heartbeat-runs/run-1/cancel").send({}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(mockHeartbeatService.cancelRun).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { errorHandler } from "../middleware/index.js";
|
||||
import { issueRoutes } from "../routes/issues.js";
|
||||
|
||||
const mockIssueService = vi.hoisted(() => ({
|
||||
getById: vi.fn(),
|
||||
@@ -32,72 +34,86 @@ const mockExecutionWorkspaceService = vi.hoisted(() => ({
|
||||
getById: vi.fn(),
|
||||
}));
|
||||
|
||||
function registerModuleMocks() {
|
||||
vi.doMock("../services/index.js", () => ({
|
||||
accessService: () => ({
|
||||
canUser: vi.fn(),
|
||||
hasPermission: vi.fn(),
|
||||
}),
|
||||
agentService: () => ({
|
||||
getById: vi.fn(),
|
||||
}),
|
||||
documentService: () => mockDocumentsService,
|
||||
environmentService: () => ({}),
|
||||
executionWorkspaceService: () => mockExecutionWorkspaceService,
|
||||
feedbackService: () => ({
|
||||
listIssueVotesForUser: vi.fn(async () => []),
|
||||
saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })),
|
||||
}),
|
||||
goalService: () => mockGoalService,
|
||||
heartbeatService: () => ({
|
||||
wakeup: vi.fn(async () => undefined),
|
||||
reportRunActivity: vi.fn(async () => undefined),
|
||||
}),
|
||||
instanceSettingsService: () => ({
|
||||
get: vi.fn(async () => ({
|
||||
id: "instance-settings-1",
|
||||
general: {
|
||||
censorUsernameInLogs: false,
|
||||
feedbackDataSharingPreference: "prompt",
|
||||
},
|
||||
})),
|
||||
listCompanyIds: vi.fn(async () => ["company-1"]),
|
||||
}),
|
||||
issueApprovalService: () => ({}),
|
||||
issueReferenceService: () => ({
|
||||
deleteDocumentSource: async () => undefined,
|
||||
diffIssueReferenceSummary: () => ({
|
||||
addedReferencedIssues: [],
|
||||
removedReferencedIssues: [],
|
||||
currentReferencedIssues: [],
|
||||
}),
|
||||
emptySummary: () => ({ outbound: [], inbound: [] }),
|
||||
listIssueReferenceSummary: async () => ({ outbound: [], inbound: [] }),
|
||||
syncComment: async () => undefined,
|
||||
syncDocument: async () => undefined,
|
||||
syncIssue: async () => undefined,
|
||||
}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: vi.fn(async () => undefined),
|
||||
projectService: () => mockProjectService,
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
}),
|
||||
workProductService: () => ({
|
||||
listForIssue: vi.fn(async () => []),
|
||||
}),
|
||||
}));
|
||||
const mockAccessService = vi.hoisted(() => ({
|
||||
canUser: vi.fn(),
|
||||
hasPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("../services/execution-workspaces.js", () => ({
|
||||
executionWorkspaceService: () => mockExecutionWorkspaceService,
|
||||
}));
|
||||
}
|
||||
const mockAgentService = vi.hoisted(() => ({
|
||||
getById: vi.fn(),
|
||||
}));
|
||||
|
||||
async function createApp() {
|
||||
const [{ issueRoutes }, { errorHandler }] = await Promise.all([
|
||||
vi.importActual<typeof import("../routes/issues.js")>("../routes/issues.js"),
|
||||
vi.importActual<typeof import("../middleware/index.js")>("../middleware/index.js"),
|
||||
]);
|
||||
const mockFeedbackService = vi.hoisted(() => ({
|
||||
listIssueVotesForUser: vi.fn(async () => []),
|
||||
saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })),
|
||||
}));
|
||||
|
||||
const mockHeartbeatService = vi.hoisted(() => ({
|
||||
wakeup: vi.fn(async () => undefined),
|
||||
reportRunActivity: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
const mockInstanceSettingsService = vi.hoisted(() => ({
|
||||
get: vi.fn(async () => ({
|
||||
id: "instance-settings-1",
|
||||
general: {
|
||||
censorUsernameInLogs: false,
|
||||
feedbackDataSharingPreference: "prompt",
|
||||
},
|
||||
})),
|
||||
listCompanyIds: vi.fn(async () => ["company-1"]),
|
||||
}));
|
||||
|
||||
const mockIssueReferenceService = vi.hoisted(() => ({
|
||||
deleteDocumentSource: vi.fn(async () => undefined),
|
||||
diffIssueReferenceSummary: vi.fn(() => ({
|
||||
addedReferencedIssues: [],
|
||||
removedReferencedIssues: [],
|
||||
currentReferencedIssues: [],
|
||||
})),
|
||||
emptySummary: vi.fn(() => ({ outbound: [], inbound: [] })),
|
||||
listIssueReferenceSummary: vi.fn(async () => ({ outbound: [], inbound: [] })),
|
||||
syncComment: vi.fn(async () => undefined),
|
||||
syncDocument: vi.fn(async () => undefined),
|
||||
syncIssue: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
|
||||
const mockRoutineService = vi.hoisted(() => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
const mockWorkProductService = vi.hoisted(() => ({
|
||||
listForIssue: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
const mockEnvironmentService = vi.hoisted(() => ({}));
|
||||
|
||||
vi.mock("../services/index.js", () => ({
|
||||
accessService: () => mockAccessService,
|
||||
agentService: () => mockAgentService,
|
||||
documentService: () => mockDocumentsService,
|
||||
environmentService: () => mockEnvironmentService,
|
||||
executionWorkspaceService: () => mockExecutionWorkspaceService,
|
||||
feedbackService: () => mockFeedbackService,
|
||||
goalService: () => mockGoalService,
|
||||
heartbeatService: () => mockHeartbeatService,
|
||||
instanceSettingsService: () => mockInstanceSettingsService,
|
||||
issueApprovalService: () => ({}),
|
||||
issueReferenceService: () => mockIssueReferenceService,
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
projectService: () => mockProjectService,
|
||||
routineService: () => mockRoutineService,
|
||||
workProductService: () => mockWorkProductService,
|
||||
}));
|
||||
|
||||
vi.mock("../services/execution-workspaces.js", () => ({
|
||||
executionWorkspaceService: () => mockExecutionWorkspaceService,
|
||||
}));
|
||||
|
||||
function createApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
@@ -147,16 +163,9 @@ const projectGoal = {
|
||||
updatedAt: new Date("2026-03-20T00:00:00Z"),
|
||||
};
|
||||
|
||||
describe("issue goal context routes", () => {
|
||||
describe.sequential("issue goal context routes", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("../services/index.js");
|
||||
vi.doUnmock("../services/execution-workspaces.js");
|
||||
vi.doUnmock("../routes/issues.js");
|
||||
vi.doUnmock("../routes/authz.js");
|
||||
vi.doUnmock("../middleware/index.js");
|
||||
registerModuleMocks();
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mockIssueService.getById.mockResolvedValue(legacyProjectLinkedIssue);
|
||||
mockIssueService.getAncestors.mockResolvedValue([]);
|
||||
mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [], blocks: [] });
|
||||
@@ -213,7 +222,7 @@ describe("issue goal context routes", () => {
|
||||
});
|
||||
|
||||
it("surfaces the project goal from GET /issues/:id when the issue has no direct goal", async () => {
|
||||
const res = await request(await createApp()).get("/api/issues/11111111-1111-4111-8111-111111111111");
|
||||
const res = await request(createApp()).get("/api/issues/11111111-1111-4111-8111-111111111111");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.goalId).toBe(projectGoal.id);
|
||||
@@ -231,7 +240,7 @@ describe("issue goal context routes", () => {
|
||||
});
|
||||
|
||||
it("surfaces the project goal from GET /issues/:id/heartbeat-context", async () => {
|
||||
const res = await request(await createApp()).get(
|
||||
const res = await request(createApp()).get(
|
||||
"/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context",
|
||||
);
|
||||
|
||||
@@ -257,7 +266,7 @@ describe("issue goal context routes", () => {
|
||||
updatedAt: new Date("2026-04-19T12:00:00.000Z"),
|
||||
});
|
||||
|
||||
const res = await request(await createApp()).get(
|
||||
const res = await request(createApp()).get(
|
||||
"/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context",
|
||||
);
|
||||
|
||||
@@ -288,7 +297,7 @@ describe("issue goal context routes", () => {
|
||||
blocks: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp()).get(
|
||||
const res = await request(createApp()).get(
|
||||
"/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context",
|
||||
);
|
||||
|
||||
@@ -323,7 +332,7 @@ describe("issue goal context routes", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const res = await request(await createApp()).get(
|
||||
const res = await request(createApp()).get(
|
||||
"/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context",
|
||||
);
|
||||
|
||||
|
||||
@@ -56,9 +56,37 @@ vi.mock("../routes/workspace-runtime-service-authz.js", () => ({
|
||||
assertCanManageExecutionWorkspaceRuntimeServices: mockAssertCanManageExecutionWorkspaceRuntimeServices,
|
||||
}));
|
||||
|
||||
function registerWorkspaceRouteMocks() {
|
||||
vi.doMock("../telemetry.js", () => ({
|
||||
getTelemetryClient: mockGetTelemetryClient,
|
||||
}));
|
||||
|
||||
vi.doMock("../services/index.js", () => ({
|
||||
environmentService: () => mockEnvironmentService,
|
||||
executionWorkspaceService: () => mockExecutionWorkspaceService,
|
||||
logActivity: mockLogActivity,
|
||||
projectService: () => mockProjectService,
|
||||
secretService: () => mockSecretService,
|
||||
workspaceOperationService: () => mockWorkspaceOperationService,
|
||||
}));
|
||||
|
||||
vi.doMock("../services/workspace-runtime.js", () => ({
|
||||
cleanupExecutionWorkspaceArtifacts: vi.fn(),
|
||||
startRuntimeServicesForWorkspaceControl: vi.fn(),
|
||||
stopRuntimeServicesForExecutionWorkspace: vi.fn(),
|
||||
stopRuntimeServicesForProjectWorkspace: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("../routes/workspace-runtime-service-authz.js", () => ({
|
||||
assertCanManageProjectWorkspaceRuntimeServices: mockAssertCanManageProjectWorkspaceRuntimeServices,
|
||||
assertCanManageExecutionWorkspaceRuntimeServices: mockAssertCanManageExecutionWorkspaceRuntimeServices,
|
||||
}));
|
||||
}
|
||||
|
||||
let appImportCounter = 0;
|
||||
|
||||
async function createProjectApp(actor: Record<string, unknown>) {
|
||||
registerWorkspaceRouteMocks();
|
||||
appImportCounter += 1;
|
||||
const routeModulePath = `../routes/projects.js?workspace-runtime-routes-authz-${appImportCounter}`;
|
||||
const middlewareModulePath = `../middleware/index.js?workspace-runtime-routes-authz-${appImportCounter}`;
|
||||
@@ -78,6 +106,7 @@ async function createProjectApp(actor: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
async function createExecutionWorkspaceApp(actor: Record<string, unknown>) {
|
||||
registerWorkspaceRouteMocks();
|
||||
appImportCounter += 1;
|
||||
const routeModulePath = `../routes/execution-workspaces.js?workspace-runtime-routes-authz-${appImportCounter}`;
|
||||
const middlewareModulePath = `../middleware/index.js?workspace-runtime-routes-authz-${appImportCounter}`;
|
||||
|
||||
@@ -5,6 +5,8 @@ export default defineConfig({
|
||||
environment: "node",
|
||||
isolate: true,
|
||||
maxConcurrency: 1,
|
||||
maxWorkers: 1,
|
||||
minWorkers: 1,
|
||||
pool: "forks",
|
||||
poolOptions: {
|
||||
forks: {
|
||||
|
||||
Reference in New Issue
Block a user