mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 01:57:31 +01:00
Merge branch 'master' into codex/task-workspace-ownership
This commit is contained in:
57 files changed
+10936
-8719
No files matched your search
@@ -249,6 +249,17 @@ function makeAgent(adapterType: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function skillSnapshot(adapterType = "claude_local", mode = "ephemeral") {
|
||||
return {
|
||||
adapterType: adapterType,
|
||||
supported: true,
|
||||
mode: mode,
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("agent skill routes", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
@@ -305,22 +316,8 @@ describe("agent skill routes", () => {
|
||||
unresolved: [],
|
||||
}),
|
||||
);
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.syncSkills.mockResolvedValue({
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot());
|
||||
mockAdapter.syncSkills.mockResolvedValue(skillSnapshot());
|
||||
mockAgentService.update.mockImplementation(async (_id: string, patch: Record<string, unknown>) => {
|
||||
const previousAgent = persistedAgent ?? makeAgent("claude_local");
|
||||
persistedAgent = {
|
||||
@@ -463,61 +460,11 @@ describe("agent skill routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("threads a non-undefined actor secret context into resolveAdapterConfigForRuntime on both skills routes (audit fidelity, skipUserSecrets preserved)", async () => {
|
||||
const expectedContext = {
|
||||
consumerType: "agent",
|
||||
consumerId: "11111111-1111-4111-8111-111111111111",
|
||||
actorType: "user",
|
||||
actorId: "local-board",
|
||||
actorSource: "local_implicit",
|
||||
responsibleUserId: "local-board",
|
||||
};
|
||||
|
||||
// GET /agents/:id/skills
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("claude_local"));
|
||||
const listRes = await requestApp(
|
||||
await createApp(),
|
||||
(baseUrl) => request(baseUrl)
|
||||
.get("/api/agents/11111111-1111-4111-8111-111111111111/skills?companyId=company-1"),
|
||||
);
|
||||
expect(listRes.status, JSON.stringify(listRes.body)).toBe(200);
|
||||
const listCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1);
|
||||
expect(listCall?.[2]).toBeDefined();
|
||||
expect(listCall?.[2]).toEqual(expectedContext);
|
||||
expect(listCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true });
|
||||
|
||||
// POST /agents/:id/skills/sync
|
||||
mockAdapter.syncSkills.mockResolvedValue({
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
const syncRes = await requestApp(
|
||||
await createApp(),
|
||||
(baseUrl) => request(baseUrl)
|
||||
.post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1")
|
||||
.send({ desiredSkills: ["paperclip"], mode: "replace" }),
|
||||
);
|
||||
expect(syncRes.status, JSON.stringify(syncRes.body)).toBe(200);
|
||||
const syncCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1);
|
||||
expect(syncCall?.[2]).toBeDefined();
|
||||
expect(syncCall?.[2]).toEqual(expectedContext);
|
||||
expect(syncCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true });
|
||||
});
|
||||
|
||||
it("skips runtime materialization when listing Codex skills", async () => {
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("codex_local"));
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "codex_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("codex_local"));
|
||||
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
@@ -540,14 +487,7 @@ describe("agent skill routes", () => {
|
||||
mockSecretService.resolveAdapterConfigForRuntime.mockResolvedValueOnce({
|
||||
config: { agent: "claude" },
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "acpx_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("acpx_local"));
|
||||
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
@@ -588,14 +528,7 @@ describe("agent skill routes", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
mockAdapter.syncSkills.mockResolvedValue({
|
||||
adapterType: "acpx_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.syncSkills.mockResolvedValue(skillSnapshot("acpx_local"));
|
||||
|
||||
const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl)
|
||||
.post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1")
|
||||
@@ -816,14 +749,7 @@ describe("agent skill routes", () => {
|
||||
|
||||
it("skips runtime materialization when listing persistent skill adapters", async () => {
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("cursor"));
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "cursor",
|
||||
supported: true,
|
||||
mode: "persistent",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("cursor", "persistent"));
|
||||
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
|
||||
@@ -507,6 +507,17 @@ describe("company portability", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
function exportCompanyAndAgents(portability: ReturnType<typeof companyPortabilityService>) {
|
||||
return portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it("renders high-volume YAML blocks without overflowing the call stack", () => {
|
||||
const tasks = Array.from({ length: 130_000 }, (_, index) => `issue-${index}`);
|
||||
|
||||
@@ -690,14 +701,7 @@ describe("company portability", () => {
|
||||
};
|
||||
const portability = companyPortabilityService(db as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const extension = asTextFile(exported.files[".paperclip.yaml"]);
|
||||
expect(extension).toContain("permissionGrants:");
|
||||
@@ -779,14 +783,7 @@ describe("company portability", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const serialized = JSON.stringify(exported);
|
||||
expect(serialized).not.toContain("sk-inline-secret-value");
|
||||
@@ -1763,14 +1760,7 @@ describe("company portability", () => {
|
||||
it("reads env inputs back from .paperclip.yaml during preview import", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const preview = await portability.previewImport({
|
||||
source: {
|
||||
@@ -3306,14 +3296,7 @@ describe("company portability", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const extension = asTextFile(exported.files[".paperclip.yaml"]);
|
||||
expect(extension).toContain("APIKEY:");
|
||||
@@ -3350,14 +3333,7 @@ describe("company portability", () => {
|
||||
reason: "Existing skill matched; renamed to paperclip-2.",
|
||||
}]);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3430,14 +3406,7 @@ describe("company portability", () => {
|
||||
});
|
||||
|
||||
const portability = companyPortabilityService({} as any, storage as any);
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
exported.files["images/company-logo.png"] = {
|
||||
encoding: "base64",
|
||||
@@ -3503,14 +3472,7 @@ describe("company portability", () => {
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3570,14 +3532,7 @@ describe("company portability", () => {
|
||||
})));
|
||||
agentSvc.list.mockClear();
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3622,14 +3577,7 @@ describe("company portability", () => {
|
||||
it("imports only selected files and leaves unchecked company metadata alone", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
projectSvc.list.mockResolvedValue([]);
|
||||
@@ -3720,14 +3668,7 @@ describe("company portability", () => {
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3802,14 +3743,7 @@ describe("company portability", () => {
|
||||
adapterConfig: input.adapterConfig,
|
||||
}));
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -5500,14 +5434,7 @@ describe("company portability", () => {
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
const originalAgentsMarkdown = exported.files["agents/claudecoder/AGENTS.md"];
|
||||
expect(typeof originalAgentsMarkdown).toBe("string");
|
||||
|
||||
@@ -5559,14 +5486,7 @@ describe("company portability", () => {
|
||||
|
||||
it("rejects dangerous adapter types on agent-safe imports", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -5743,14 +5663,7 @@ describe("company portability", () => {
|
||||
|
||||
it("imports new agents as active while preserving future hire approval settings", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
secretSvc.normalizeAdapterConfigForPersistence.mockResolvedValueOnce({
|
||||
@@ -5819,14 +5732,7 @@ describe("company portability", () => {
|
||||
},
|
||||
})));
|
||||
agentSvc.list.mockClear();
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
secretSvc.normalizeAdapterConfigForPersistence.mockResolvedValueOnce({
|
||||
normalized: "updated",
|
||||
|
||||
@@ -201,6 +201,75 @@ function registerModuleMocks() {
|
||||
}));
|
||||
}
|
||||
|
||||
function boardActor(userId = "local-board", source = "local_implicit") {
|
||||
return {
|
||||
type: "board",
|
||||
userId: userId,
|
||||
companyIds: ["company-1"],
|
||||
source: source,
|
||||
isInstanceAdmin: false,
|
||||
};
|
||||
}
|
||||
|
||||
function assignedAgentActor() {
|
||||
return {
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
};
|
||||
}
|
||||
|
||||
function createCatalogSkill() {
|
||||
return {
|
||||
id: "paperclipai:bundled:software-development:review",
|
||||
key: "paperclipai/bundled/software-development/review",
|
||||
kind: "bundled",
|
||||
category: "software-development",
|
||||
slug: "review",
|
||||
name: "review",
|
||||
description: "Review code",
|
||||
path: "catalog/bundled/software-development/review",
|
||||
entrypoint: "SKILL.md",
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
defaultInstall: false,
|
||||
recommendedForRoles: ["engineer"],
|
||||
requires: [],
|
||||
tags: ["review"],
|
||||
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
|
||||
contentHash: "sha256:abc",
|
||||
};
|
||||
}
|
||||
|
||||
function createSkillTestRunDetails(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "queued",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("company skill mutation permissions", () => {
|
||||
const routeModules = hoistModuleGraph(registerModuleMocks, async () => {
|
||||
const [{ companySkillRoutes }, { errorHandler }] = await Promise.all([
|
||||
@@ -399,25 +468,7 @@ describe("company skill mutation permissions", () => {
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
},
|
||||
catalogSkill: {
|
||||
id: "paperclipai:bundled:software-development:review",
|
||||
key: "paperclipai/bundled/software-development/review",
|
||||
kind: "bundled",
|
||||
category: "software-development",
|
||||
slug: "review",
|
||||
name: "review",
|
||||
description: "Review code",
|
||||
path: "catalog/bundled/software-development/review",
|
||||
entrypoint: "SKILL.md",
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
defaultInstall: false,
|
||||
recommendedForRoles: ["engineer"],
|
||||
requires: [],
|
||||
tags: ["review"],
|
||||
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
|
||||
contentHash: "sha256:abc",
|
||||
},
|
||||
catalogSkill: createCatalogSkill(),
|
||||
warnings: [],
|
||||
});
|
||||
mockCompanySkillService.createLocalSkill.mockResolvedValue({
|
||||
@@ -590,56 +641,14 @@ describe("company skill mutation permissions", () => {
|
||||
mockCompanySkillService.listTestRuns.mockResolvedValue([]);
|
||||
mockCompanySkillService.getTestRunDetail.mockResolvedValue(null);
|
||||
mockCompanySkillService.createTestRun.mockResolvedValue({
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "queued",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails(),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
taskExpired: false,
|
||||
});
|
||||
mockCompanySkillService.cancelTestRun.mockResolvedValue({
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "cancelled",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: "Cancelled by operator",
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails({ status: "cancelled", error: "Cancelled by operator" }),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -661,25 +670,7 @@ describe("company skill mutation permissions", () => {
|
||||
mockHeartbeatService.wakeup.mockResolvedValue({});
|
||||
mockHeartbeatService.cancelRun.mockResolvedValue({});
|
||||
mockCatalogService.listCatalogSkillsOrEmpty.mockReturnValue([]);
|
||||
mockCatalogService.getCatalogSkillOrThrow.mockReturnValue({
|
||||
id: "paperclipai:bundled:software-development:review",
|
||||
key: "paperclipai/bundled/software-development/review",
|
||||
kind: "bundled",
|
||||
category: "software-development",
|
||||
slug: "review",
|
||||
name: "review",
|
||||
description: "Review code",
|
||||
path: "catalog/bundled/software-development/review",
|
||||
entrypoint: "SKILL.md",
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
defaultInstall: false,
|
||||
recommendedForRoles: ["engineer"],
|
||||
requires: [],
|
||||
tags: ["review"],
|
||||
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
|
||||
contentHash: "sha256:abc",
|
||||
});
|
||||
mockCatalogService.getCatalogSkillOrThrow.mockReturnValue(createCatalogSkill());
|
||||
mockCatalogService.readCatalogSkillFile.mockResolvedValue({
|
||||
catalogSkillId: "paperclipai:bundled:software-development:review",
|
||||
path: "SKILL.md",
|
||||
@@ -709,13 +700,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("allows local board operators to mutate company skills", async () => {
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -750,13 +735,7 @@ describe("company skill mutation permissions", () => {
|
||||
}],
|
||||
warnings: [],
|
||||
});
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const preview = await request(app)
|
||||
.post("/api/companies/company-1/skills/scan-projects")
|
||||
@@ -798,13 +777,7 @@ describe("company skill mutation permissions", () => {
|
||||
"Actor is restricted from changing skill configuration.",
|
||||
));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/scan-projects")
|
||||
.send({ mode: "preview", workspaceIds: [workspaceId] });
|
||||
|
||||
@@ -814,13 +787,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("allows board users with skills:create to create, import, install, update, delete, audit, and reset company skills", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor("board-user", "session"));
|
||||
|
||||
await request(app)
|
||||
.post("/api/companies/company-1/skills")
|
||||
@@ -867,13 +834,7 @@ describe("company skill mutation permissions", () => {
|
||||
it("allows board users without skills:create when no explicit skill policy exists", async () => {
|
||||
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision());
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -897,12 +858,7 @@ describe("company skill mutation permissions", () => {
|
||||
remediation: "Contact a company administrator to change the skill policy.",
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://packages.example.com/skill.tgz" });
|
||||
|
||||
@@ -921,13 +877,7 @@ describe("company skill mutation permissions", () => {
|
||||
it("rejects secret-bearing remote import URLs without echoing the secret", async () => {
|
||||
const source = "https://github.com/acme/private-skill?token=secret#token=secret";
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source });
|
||||
|
||||
@@ -941,13 +891,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("rejects malformed remote import URLs before policy evaluation", async () => {
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://" });
|
||||
|
||||
@@ -965,12 +909,7 @@ describe("company skill mutation permissions", () => {
|
||||
"Low-trust agents cannot use company-wide skill APIs.",
|
||||
));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -998,13 +937,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1042,13 +975,7 @@ describe("company skill mutation permissions", () => {
|
||||
}
|
||||
));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.patch("/api/companies/company-1/skills/skill-1")
|
||||
.send({ name: "Updated review" });
|
||||
|
||||
@@ -1073,13 +1000,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
mockCompanySkillPolicyService.evaluate.mockResolvedValue(denySkillPolicy("skills.create"));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/skill-1/versions")
|
||||
.send({ label: "v1" });
|
||||
|
||||
@@ -1110,13 +1031,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "npx skills add Vercel-Labs/Agent-Browser --skill agent-browser -g" });
|
||||
|
||||
@@ -1160,13 +1075,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source });
|
||||
|
||||
@@ -1185,34 +1094,10 @@ describe("company skill mutation permissions", () => {
|
||||
|
||||
it("serves catalog listing without mutating company skills", async () => {
|
||||
mockCatalogService.listCatalogSkillsOrEmpty.mockReturnValue([
|
||||
{
|
||||
id: "paperclipai:bundled:software-development:review",
|
||||
key: "paperclipai/bundled/software-development/review",
|
||||
kind: "bundled",
|
||||
category: "software-development",
|
||||
slug: "review",
|
||||
name: "review",
|
||||
description: "Review code",
|
||||
path: "catalog/bundled/software-development/review",
|
||||
entrypoint: "SKILL.md",
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
defaultInstall: false,
|
||||
recommendedForRoles: ["engineer"],
|
||||
requires: [],
|
||||
tags: ["review"],
|
||||
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
|
||||
contentHash: "sha256:abc",
|
||||
},
|
||||
createCatalogSkill(),
|
||||
]);
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.get("/api/skills/catalog?kind=bundled&q=review");
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
@@ -1267,13 +1152,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("serves catalog detail and files by catalog reference", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const detail = await request(app)
|
||||
.get("/api/skills/catalog/review");
|
||||
@@ -1288,13 +1167,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("installs catalog skills with mutation permissions and logs provenance", async () => {
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/install-catalog")
|
||||
.send({
|
||||
catalogSkillId: "paperclipai:bundled:software-development:review",
|
||||
@@ -1348,13 +1221,7 @@ describe("company skill mutation permissions", () => {
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1486,13 +1353,7 @@ describe("company skill mutation permissions", () => {
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://ghe.example.com/acme/private-skill" });
|
||||
|
||||
@@ -1528,13 +1389,7 @@ describe("company skill mutation permissions", () => {
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/acme/private-skill" });
|
||||
|
||||
@@ -1553,12 +1408,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: { canCreateSkills: false },
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1578,12 +1428,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: { canCreateSkills: true },
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-2/skills/install-catalog")
|
||||
.send({ catalogSkillId: "paperclipai:bundled:software-development:review" });
|
||||
|
||||
@@ -1861,12 +1706,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: { canCreateSkills: false },
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1906,26 +1746,7 @@ describe("company skill mutation permissions", () => {
|
||||
expect(mockCompanySkillService.createLocalSkill).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not invoke the legacy consent gate under the open default", async () => {
|
||||
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision(
|
||||
"deny_missing_consent",
|
||||
"Permission skills:suggest-changes requires accepted change consent before applying this mutation.",
|
||||
));
|
||||
mockReflectionCoachMutationGate.assertConsented.mockRejectedValue(new Error("database unavailable"));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "reflection-coach",
|
||||
companyId: "company-1",
|
||||
runId: "run-apply",
|
||||
}))
|
||||
.post("/api/companies/company-1/skills")
|
||||
.send({ name: "Reflection Draft", slug: "reflection-draft", markdown: "# Draft" });
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(mockReflectionCoachMutationGate.assertConsented).not.toHaveBeenCalled();
|
||||
expect(mockCompanySkillService.createLocalSkill).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows suggest-tier skill mutations without a second consent decision", async () => {
|
||||
mockAccessService.decide
|
||||
@@ -1940,12 +1761,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1966,12 +1782,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -2016,13 +1827,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("routes skill test input CRUD through skills mutation permissions", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const created = await request(app)
|
||||
.post("/api/companies/company-1/skills/skill-1/test-inputs")
|
||||
@@ -2057,13 +1862,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("routes skill test run template CRUD through skills mutation permissions", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const listed = await request(app).get("/api/companies/company-1/skill-test-run-templates");
|
||||
expect(listed.status, JSON.stringify(listed.body)).toBe(200);
|
||||
@@ -2124,28 +1923,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
await deps.wakeHarnessIssue("44444444-4444-4444-8444-444444444444", "55555555-5555-4555-8555-555555555555");
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "queued",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails(),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -2160,28 +1938,7 @@ describe("company skill mutation permissions", () => {
|
||||
) => {
|
||||
await deps.cancelHarnessIssue("44444444-4444-4444-8444-444444444444");
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "cancelled",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: "Cancelled by operator",
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails({ status: "cancelled", error: "Cancelled by operator" }),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -2189,13 +1946,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const created = await request(app)
|
||||
.post("/api/companies/company-1/skills/skill-1/test-runs")
|
||||
@@ -2282,13 +2033,7 @@ describe("company skill mutation permissions", () => {
|
||||
harnessContent: { available: false, unavailableReason: "expired", documents: [], attachments: [], workProducts: [] },
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const listed = await request(app)
|
||||
.get("/api/companies/company-1/skills/skill-1/test-runs");
|
||||
@@ -2322,28 +2067,7 @@ describe("company skill mutation permissions", () => {
|
||||
) => {
|
||||
await deps.hideHarnessIssue("44444444-4444-4444-8444-444444444444");
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: null,
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "succeeded",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: new Date("2026-05-26T00:02:00.000Z"),
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails({ inputId: null, status: "succeeded", deletedAt: new Date("2026-05-26T00:02:00.000Z") }),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:02:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -2351,13 +2075,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const deleted = await request(app)
|
||||
.delete("/api/companies/company-1/skills/skill-1/test-runs/22222222-2222-4222-8222-222222222222");
|
||||
@@ -2371,13 +2089,7 @@ describe("company skill mutation permissions", () => {
|
||||
|
||||
it("returns 404 when deleting a missing test run", async () => {
|
||||
mockCompanySkillService.deleteTestRun.mockResolvedValueOnce(null);
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
const res = await request(app)
|
||||
.delete("/api/companies/company-1/skills/skill-1/test-runs/22222222-2222-4222-8222-222222222222");
|
||||
expect(res.status).toBe(404);
|
||||
@@ -2391,13 +2103,7 @@ describe("company skill mutation permissions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.delete("/api/companies/company-1/skills/skill-1");
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(422);
|
||||
|
||||
@@ -223,11 +223,7 @@ function createDeleteBlastRadius(overrides: Partial<{
|
||||
}
|
||||
|
||||
let server: Server | null = null;
|
||||
let currentActor: Record<string, unknown> = {
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
};
|
||||
let currentActor: Record<string, unknown> = localBoardActor();
|
||||
const routeOptions: Record<string, unknown> = {};
|
||||
const originalSecretsProviderEnv = process.env.PAPERCLIP_SECRETS_PROVIDER;
|
||||
|
||||
@@ -260,6 +256,67 @@ function createApp(actor: Record<string, unknown>, options: Record<string, unkno
|
||||
return server;
|
||||
}
|
||||
|
||||
function localBoardActor(userId = "user-1") {
|
||||
return {
|
||||
type: "board",
|
||||
userId: userId,
|
||||
source: "local_implicit",
|
||||
};
|
||||
}
|
||||
|
||||
function createSecurePluginDriver() {
|
||||
return {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSecretBackedSshEnvironment() {
|
||||
return {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSshConfig() {
|
||||
return {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("environment routes", () => {
|
||||
afterAll(async () => {
|
||||
if (originalSecretsProviderEnv === undefined) {
|
||||
@@ -369,20 +426,7 @@ describe("environment routes", () => {
|
||||
? {
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
}
|
||||
: null
|
||||
));
|
||||
@@ -401,11 +445,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("lists instance-scoped environments through the company route alias", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments?driver=local");
|
||||
|
||||
@@ -718,15 +758,7 @@ describe("environment routes", () => {
|
||||
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still rejects deletion of the managed sandbox row", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
|
||||
const app = createApp(ownerAdminActor);
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-managed-1");
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
|
||||
});
|
||||
|
||||
it("hides the local environment from every read surface under managed-sandbox-only", async () => {
|
||||
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
@@ -1173,11 +1205,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("returns provider capabilities for the company", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1213,11 +1241,7 @@ describe("environment routes", () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1271,11 +1295,7 @@ describe("environment routes", () => {
|
||||
configSchema: { type: "object", properties: {} },
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1295,11 +1315,7 @@ describe("environment routes", () => {
|
||||
configSchema: { type: "object", properties: {} },
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1322,11 +1338,7 @@ describe("environment routes", () => {
|
||||
configSchema: { type: "object", properties: {} },
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1384,11 +1396,7 @@ describe("environment routes", () => {
|
||||
it("creates an environment and logs activity", async () => {
|
||||
const environment = createEnvironment();
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -1425,11 +1433,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("returns conflict when creating a second local environment", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -1529,11 +1533,7 @@ describe("environment routes", () => {
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
|
||||
isManagedLocal: true,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1549,26 +1549,13 @@ describe("environment routes", () => {
|
||||
...createEnvironment(),
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
|
||||
isInstanceDefault: true,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1585,26 +1572,13 @@ describe("environment routes", () => {
|
||||
...createEnvironment(),
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
|
||||
pendingCleanupLeaseCount: 1,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1632,11 +1606,7 @@ describe("environment routes", () => {
|
||||
activeLeaseCount: 1,
|
||||
reusableSandboxLeaseCount: 1,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1662,11 +1632,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
|
||||
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
|
||||
|
||||
@@ -1696,11 +1662,7 @@ describe("environment routes", () => {
|
||||
isInstanceDefault: true,
|
||||
reusableSandboxLeaseCount: 1,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
|
||||
|
||||
@@ -1722,11 +1684,7 @@ describe("environment routes", () => {
|
||||
failed: 1,
|
||||
skippedLiveRun: 0,
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
|
||||
|
||||
@@ -1746,24 +1704,11 @@ describe("environment routes", () => {
|
||||
id: "env-ssh",
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-ssh")
|
||||
@@ -1790,25 +1735,12 @@ describe("environment routes", () => {
|
||||
id: "env-ssh",
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
|
||||
mockEnvironmentService.update.mockResolvedValue({ ...environment, description: "Updated" });
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-ssh")
|
||||
@@ -1844,11 +1776,7 @@ describe("environment routes", () => {
|
||||
companyName: "Other Team",
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).get("/api/environments/env-sandbox/secret-refs");
|
||||
|
||||
@@ -1890,35 +1818,12 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("clears environment selections and secret bindings across all companies when deleting an environment", async () => {
|
||||
const environment = {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
const environment = createSecretBackedSshEnvironment();
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius());
|
||||
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
|
||||
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-ssh");
|
||||
|
||||
@@ -1956,11 +1861,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("rejects invalid SSH config on create", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -1979,33 +1880,10 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("normalizes SSH private keys into secret refs before persistence", async () => {
|
||||
const environment = {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
const environment = createSecretBackedSshEnvironment();
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2045,32 +1923,9 @@ describe("environment routes", () => {
|
||||
|
||||
it("uses the configured provider for SSH private key secret materialization", async () => {
|
||||
process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager";
|
||||
const environment = {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
const environment = createSecretBackedSshEnvironment();
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2097,11 +1952,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("rejects persisted fake sandbox environments", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2134,11 +1985,7 @@ describe("environment routes", () => {
|
||||
};
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2223,11 +2070,7 @@ describe("environment routes", () => {
|
||||
},
|
||||
);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2277,27 +2120,10 @@ describe("environment routes", () => {
|
||||
},
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
});
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2369,27 +2195,10 @@ describe("environment routes", () => {
|
||||
normalizedConfig: { ...config },
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
}));
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2452,27 +2261,10 @@ describe("environment routes", () => {
|
||||
},
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
});
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2516,11 +2308,7 @@ describe("environment routes", () => {
|
||||
mockValidatePluginEnvironmentDriverConfig.mockResolvedValue(environment.config);
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2604,11 +2392,7 @@ describe("environment routes", () => {
|
||||
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get(`/api/environments/${environment.id}/leases?status=active`);
|
||||
|
||||
@@ -2640,11 +2424,7 @@ describe("environment routes", () => {
|
||||
createdAt: new Date("2026-04-16T05:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/environment-leases/lease-1");
|
||||
|
||||
@@ -2677,11 +2457,7 @@ describe("environment routes", () => {
|
||||
...environment,
|
||||
status: "archived",
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/environments/${environment.id}?companyId=company-1`)
|
||||
@@ -2736,11 +2512,7 @@ describe("environment routes", () => {
|
||||
driver: "local" as const,
|
||||
config: {},
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/environments/${environment.id}?companyId=company-1`)
|
||||
@@ -2783,30 +2555,13 @@ describe("environment routes", () => {
|
||||
normalizedConfig: { ...config },
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
}));
|
||||
// The environment's only binding still lives in another company. Before
|
||||
// bindings moved with the referenced secret, this state made every save
|
||||
// from the caller's company fail, with no route-level way out.
|
||||
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue(["company-old"]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager: {} });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager: {} });
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-sandbox?companyId=company-new")
|
||||
@@ -2856,11 +2611,7 @@ describe("environment routes", () => {
|
||||
mockSecretService.replaceSecretRefsForInstanceTarget.mockRejectedValue(
|
||||
unprocessable("Secret referenced at apiKey was not found", { code: "secret_missing" }),
|
||||
);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager: {} });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager: {} });
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-sandbox?companyId=company-1")
|
||||
@@ -2876,11 +2627,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("requires explicit SSH config when switching from local to SSH", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-1?companyId=company-1")
|
||||
@@ -2895,11 +2642,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("rejects switching an environment to the built-in fake sandbox provider", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-1?companyId=company-1")
|
||||
@@ -2918,11 +2661,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("returns 404 when patching a missing environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(null);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/missing-env")
|
||||
@@ -3200,20 +2939,7 @@ describe("environment routes", () => {
|
||||
},
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
});
|
||||
mockProbeEnvironment.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -294,6 +294,104 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
function insertBlockedSourceTask(issueId: string, companyId: string, projectId: string) {
|
||||
return db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
}
|
||||
|
||||
function insertBoardWorkspaceRecovery(companyId: string, issueId: string, fingerprint: string) {
|
||||
return db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
}
|
||||
|
||||
function insertMissingRecordedBranchWorkspace(executionWorkspaceId: string, companyId: string, projectId: string, issueId: string, worktreePath: string) {
|
||||
return db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
}
|
||||
|
||||
function insertRecordedBranchWorkspace(executionWorkspaceId: string, companyId: string, projectId: string, projectWorkspaceId: string, issueId: string, worktreePath: string) {
|
||||
return db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/recorded",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
}
|
||||
|
||||
function insertRunningWorkspaceService(runtimeServiceId: string, companyId: string, projectId: string, executionWorkspaceId: string, issueId: string, worktreePath: string) {
|
||||
return db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
executionWorkspaceId,
|
||||
issueId,
|
||||
scopeType: "execution_workspace",
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
command: "pnpm dev",
|
||||
cwd: worktreePath,
|
||||
provider: "local_process",
|
||||
healthStatus: "healthy",
|
||||
});
|
||||
}
|
||||
|
||||
function insertActiveCodexCoder(agentId: string, companyId: string) {
|
||||
return db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Codex Coder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function seedTerminalWorkspace(options: {
|
||||
mergedPr?: boolean;
|
||||
activeRun?: boolean;
|
||||
@@ -2204,21 +2302,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2311,21 +2395,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2392,21 +2462,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2465,21 +2521,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2529,17 +2571,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Codex Coder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await insertActiveCodexCoder(agentId, companyId);
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
@@ -2565,37 +2597,8 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
priority: "medium",
|
||||
assigneeAgentId: agentId,
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/recorded",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
await insertRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, projectWorkspaceId, issueId, worktreePath);
|
||||
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "quarantine_restore",
|
||||
@@ -2719,21 +2722,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
executionWorkspaceId,
|
||||
issueId,
|
||||
scopeType: "execution_workspace",
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
command: "pnpm dev",
|
||||
cwd: worktreePath,
|
||||
provider: "local_process",
|
||||
healthStatus: "healthy",
|
||||
});
|
||||
await insertRunningWorkspaceService(runtimeServiceId, companyId, projectId, executionWorkspaceId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "quarantine_restore",
|
||||
@@ -2877,37 +2866,8 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/recorded",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
await insertRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, projectWorkspaceId, issueId, worktreePath);
|
||||
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "quarantine_restore",
|
||||
@@ -2988,17 +2948,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Codex Coder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await insertActiveCodexCoder(agentId, companyId);
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
@@ -3161,14 +3111,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3246,14 +3189,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3331,14 +3267,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3435,14 +3364,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3458,21 +3380,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
executionWorkspaceId,
|
||||
issueId,
|
||||
scopeType: "execution_workspace",
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
command: "pnpm dev",
|
||||
cwd: worktreePath,
|
||||
provider: "local_process",
|
||||
healthStatus: "healthy",
|
||||
});
|
||||
await insertRunningWorkspaceService(runtimeServiceId, companyId, projectId, executionWorkspaceId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "override",
|
||||
@@ -3542,14 +3450,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3671,14 +3572,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3872,14 +3766,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
|
||||
@@ -68,6 +68,15 @@ const mockDb = {
|
||||
select: vi.fn(() => ({ from: () => Promise.resolve(mockCompanyRows) })),
|
||||
};
|
||||
|
||||
function localAdminActor() {
|
||||
return {
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("instance settings routes", () => {
|
||||
const routeModules = hoistModuleGraph(registerModuleMocks, async () => {
|
||||
const [{ errorHandler }, { instanceSettingsRoutes }] = await Promise.all([
|
||||
@@ -234,12 +243,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to read and update experimental settings", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const getRes = await request(app).get("/api/instance/settings/experimental");
|
||||
expect(getRes.status).toBe(200);
|
||||
@@ -273,12 +277,7 @@ describe("instance settings routes", () => {
|
||||
}, 10_000);
|
||||
|
||||
it("does not expose the retired liveness auto-recovery endpoints", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.post("/api/instance/settings/experimental/issue-graph-liveness-auto-recovery/preview")
|
||||
@@ -303,12 +302,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("accepts the instance-wide Streamlined UI preference", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -321,12 +315,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("strips server-managed worktree run execution fields before updating experimental settings", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -343,12 +332,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to read and update the instance default environment", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const getRes = await request(app).get("/api/instance/settings");
|
||||
expect(getRes.status).toBe(200);
|
||||
@@ -379,12 +363,7 @@ describe("instance settings routes", () => {
|
||||
envVars: {},
|
||||
metadata: { managedByPaperclip: true, managedDefaultStamped: true },
|
||||
});
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const patchRes = await request(app)
|
||||
.patch("/api/instance/settings")
|
||||
@@ -419,12 +398,7 @@ describe("instance settings routes", () => {
|
||||
metadata: { managedByPaperclip: true, managedDefaultStamped: true },
|
||||
});
|
||||
mockEnvironmentService.update.mockRejectedValue(new Error("metadata write failed"));
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const patchRes = await request(app)
|
||||
.patch("/api/instance/settings")
|
||||
@@ -436,12 +410,7 @@ describe("instance settings routes", () => {
|
||||
|
||||
it("rejects unknown defaultEnvironmentId values with 422", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(null);
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/instance/settings")
|
||||
@@ -453,12 +422,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update guarded dev-server auto-restart", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -473,12 +437,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update external object detection", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -491,12 +450,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update built-in agents", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -509,12 +463,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update the goals sidebar link", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -527,12 +476,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update the server info debug view", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -545,12 +489,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update environment controls", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -582,12 +521,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to read and update general settings", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const getRes = await request(app).get("/api/instance/settings/general");
|
||||
expect(getRes.status).toBe(200);
|
||||
|
||||
@@ -321,6 +321,56 @@ async function waitForWakeup(assertion: () => void) {
|
||||
await vi.waitFor(assertion);
|
||||
}
|
||||
|
||||
function createAgentReviewPolicy(reviewerAgentId: string) {
|
||||
return normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function createReviewIssue(policy: NonNullable<Awaited<ReturnType<typeof normalizePolicy>>>, reviewerAgentId: string) {
|
||||
return {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createScheduledRetry() {
|
||||
return {
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("issue comment reopen routes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -956,7 +1006,7 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
// Same guard on cancelled status — explicit resume must use `resume: true`,
|
||||
// a log-class self-comment with `reopen: true` is not a reopen signal.
|
||||
it("does not reopen via POST comment+reopen when the assignee agent is the actor on a cancelled issue", async () => {
|
||||
it("rejects bare agent reopen requests on cancelled issues before writing a comment", async () => {
|
||||
const assigneeAgentId = "22222222-2222-4222-8222-222222222222";
|
||||
mockIssueService.getById.mockResolvedValue(makeIssue("cancelled"));
|
||||
mockIssueService.update.mockImplementation(
|
||||
@@ -985,14 +1035,13 @@ describe("issue comment reopen routes", () => {
|
||||
}),
|
||||
)
|
||||
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
|
||||
// Cancelled issues reject explicit resume entirely, so only reopen=true
|
||||
// is observable here — the guard is what keeps it from flipping back.
|
||||
.send({ body: "log line", reopen: true });
|
||||
|
||||
// Cancelled issues are rejected at assertExplicitResumeIntentAllowed for
|
||||
// agent actors with reopen=true (409). The guard runs after that, but
|
||||
// either way no reopen wakeup must fire and no status update to todo.
|
||||
expect([200, 201, 409]).toContain(res.status);
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.error).toBe(
|
||||
"Cancelled issues must be restored through the dedicated restore flow",
|
||||
);
|
||||
expect(mockIssueService.addComment).not.toHaveBeenCalled();
|
||||
expect(mockIssueService.update).not.toHaveBeenCalledWith(
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
expect.objectContaining({ status: "todo" }),
|
||||
@@ -1142,18 +1191,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockIssueService.update.mockImplementation(
|
||||
async (_id: string, patch: Record<string, unknown>) => ({
|
||||
...issue,
|
||||
@@ -1214,18 +1252,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockHeartbeatService.cancelRun.mockRejectedValue(
|
||||
new Error("cancel failed"),
|
||||
);
|
||||
@@ -1890,18 +1917,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockIssueService.update.mockImplementation(
|
||||
async (_id: string, patch: Record<string, unknown>) => ({
|
||||
...issue,
|
||||
@@ -1950,18 +1966,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockHeartbeatService.cancelRun.mockRejectedValue(
|
||||
new Error("cancel failed"),
|
||||
);
|
||||
@@ -2635,8 +2640,8 @@ describe("issue comment reopen routes", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["invalid", "wrong agent", "wrong company"])(
|
||||
"rejects comment and PATCH writes with a %s run",
|
||||
it(
|
||||
"rejects comment and PATCH writes when influence validation rejects the run",
|
||||
async () => {
|
||||
mockIssueService.getById.mockResolvedValue(makeIssue("todo"));
|
||||
mockObserveCrossIssueInfluence.mockRejectedValue(
|
||||
@@ -2940,35 +2945,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("auto-approves a reviewer comment with the APPROVED review marker", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3032,35 +3010,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("auto-approves a reviewer comment with structured review metadata", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "kind: review\ndecision: approved\nsummary: ship it";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3125,15 +3076,7 @@ describe("issue comment reopen routes", () => {
|
||||
it("auto-approves a reviewer comment and wakes dependents when the final blocker resolves", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const dependentAgentId = "44444444-4444-4444-8444-444444444444";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
@@ -3220,15 +3163,7 @@ describe("issue comment reopen routes", () => {
|
||||
it("does not wake the returnAssignee with issue_commented when auto-approval reassigns the issue", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const returnAssigneeAgentId = "22222222-2222-4222-8222-222222222222";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
@@ -3298,35 +3233,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("does not auto-approve APPROVED comments from a non-review participant", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3422,35 +3330,8 @@ describe("issue comment reopen routes", () => {
|
||||
// Even though both `kind: review` and `decision: approved` appear, a blank line between
|
||||
// them means they are not on truly consecutive lines, so the strict regex must reject it.
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "kind: review\n\ndecision: approved";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3483,35 +3364,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("does not auto-approve reviewer comments without an approval marker", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "Looks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3545,15 +3399,7 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("does not auto-approve approval comments outside the in_review status", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_progress",
|
||||
@@ -3633,35 +3479,8 @@ describe("issue comment reopen routes", () => {
|
||||
])("does not auto-approve negated approval phrasings ($name)", ({ body }) => {
|
||||
it("rejects the auto-approval transition and keeps the comment as a regular comment", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
id: "comment-review-negated",
|
||||
@@ -3705,35 +3524,8 @@ describe("issue comment reopen routes", () => {
|
||||
])("auto-approves positive approval phrasings ($name)", ({ body }) => {
|
||||
it("triggers the auto-approval transition", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
id: "comment-review-positive",
|
||||
@@ -3794,35 +3586,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("rolls back the comment when the auto-approval status transition fails", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3869,35 +3634,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("rolls back the auto-approval comment when the issue is concurrently deleted", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
|
||||
@@ -81,17 +81,55 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
function interactionCompanyRow(companyId: string): typeof companies.$inferInsert {
|
||||
return {
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
};
|
||||
}
|
||||
|
||||
function parentIssueRow(issueId: string, companyId: string, goalId: string): typeof issues.$inferInsert {
|
||||
return {
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
};
|
||||
}
|
||||
|
||||
function acceptIssueScope(issueId: string, companyId: string, goalId: string) {
|
||||
return {
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function activeCodexAgentRow(agentId: string, companyId: string, name: string, role: string): typeof agents.$inferInsert {
|
||||
return {
|
||||
id: agentId,
|
||||
companyId,
|
||||
name,
|
||||
role,
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
};
|
||||
}
|
||||
|
||||
async function seedConfirmationIssue(title = "Comment supersede") {
|
||||
const companyId = randomUUID();
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -100,14 +138,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
|
||||
return { companyId, goalId, issueId };
|
||||
}
|
||||
@@ -174,17 +205,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const { companyId, issueId } = await seedConfirmationIssue("Source question race");
|
||||
const agentId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Questioner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Questioner", "engineer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
@@ -596,17 +617,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
it("rejects an explicitly mismatched source-run issue or agent", async () => {
|
||||
const fixture = await seedSourceQuestionFixture({ paperclipWake: { comments: [] } });
|
||||
const otherAgentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: otherAgentId,
|
||||
companyId: fixture.companyId,
|
||||
name: "Other questioner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(otherAgentId, fixture.companyId, "Other questioner", "engineer"));
|
||||
|
||||
// Simulate historical corruption to retain the service-level defense test.
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -764,17 +775,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const agentId = randomUUID();
|
||||
const firstRunId = randomUUID();
|
||||
const secondRunId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Researcher",
|
||||
role: "researcher",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Researcher", "researcher"));
|
||||
await db.update(issues).set({ assigneeAgentId: agentId, status: "in_progress" }).where(eq(issues.id, issueId));
|
||||
await db.insert(heartbeatRuns).values([
|
||||
{
|
||||
@@ -1168,12 +1169,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const assigneeAgentId = randomUUID();
|
||||
const responsibleUserId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1183,17 +1179,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: assigneeAgentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(assigneeAgentId, companyId, "CodexCoder", "engineer"));
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
@@ -1233,12 +1219,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
expect(created.status).toBe("pending");
|
||||
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -1295,12 +1276,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
expect(listed).toHaveLength(1);
|
||||
expect(listed[0]?.status).toBe("accepted");
|
||||
|
||||
await expect(interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
await expect(interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toThrow("Interaction has already been resolved");
|
||||
|
||||
@@ -1313,12 +1289,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1366,12 +1337,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedClientKeys: ["root"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -1395,12 +1361,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1410,14 +1371,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
|
||||
const created = await interactionsSvc.create({
|
||||
id: issueId,
|
||||
@@ -1444,12 +1398,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedClientKeys: ["child"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -1462,12 +1411,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1564,12 +1508,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -1951,12 +1890,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const agentId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1966,25 +1900,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "CodexCoder", "engineer"));
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
@@ -2039,28 +1956,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const firstAgentId = randomUUID();
|
||||
const secondAgentId = randomUUID();
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: firstAgentId,
|
||||
companyId,
|
||||
name: "First agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: secondAgentId,
|
||||
companyId,
|
||||
name: "Second agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
activeCodexAgentRow(firstAgentId, companyId, "First agent", "engineer"),
|
||||
activeCodexAgentRow(secondAgentId, companyId, "Second agent", "engineer"),
|
||||
]);
|
||||
|
||||
const older = await interactionsSvc.create({ id: issueId, companyId }, {
|
||||
@@ -2117,28 +2014,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const probingAgentId = randomUUID();
|
||||
const otherAgentId = randomUUID();
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: probingAgentId,
|
||||
companyId,
|
||||
name: "Probing agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: otherAgentId,
|
||||
companyId,
|
||||
name: "Other agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
activeCodexAgentRow(probingAgentId, companyId, "Probing agent", "engineer"),
|
||||
activeCodexAgentRow(otherAgentId, companyId, "Other agent", "engineer"),
|
||||
]);
|
||||
|
||||
const question = (prompt: string) => ({
|
||||
@@ -2198,23 +2075,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Chief of staff",
|
||||
role: "chief_of_staff",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Chief of staff", "chief_of_staff"));
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
companyId,
|
||||
@@ -2291,28 +2153,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const firstAgentId = randomUUID();
|
||||
const secondAgentId = randomUUID();
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: firstAgentId,
|
||||
companyId,
|
||||
name: "First agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: secondAgentId,
|
||||
companyId,
|
||||
name: "Second agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
activeCodexAgentRow(firstAgentId, companyId, "First agent", "engineer"),
|
||||
activeCodexAgentRow(secondAgentId, companyId, "Second agent", "engineer"),
|
||||
]);
|
||||
|
||||
const firstAgentIds = [randomUUID(), randomUUID(), randomUUID()];
|
||||
@@ -2411,12 +2253,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -2425,14 +2262,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
|
||||
const created = await interactionsSvc.create({
|
||||
id: issueId,
|
||||
@@ -2454,12 +2284,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
expect(created.kind).toBe("request_confirmation");
|
||||
expect(created.status).toBe("pending");
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -2551,17 +2376,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const resolverAgentId = randomUUID();
|
||||
const resolverRunId = randomUUID();
|
||||
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
|
||||
await db.insert(agents).values({
|
||||
id: resolverAgentId,
|
||||
companyId,
|
||||
name: "Review agent",
|
||||
role: "reviewer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: resolverRunId,
|
||||
companyId,
|
||||
@@ -2579,12 +2394,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
await recordReviewTransition({ companyId, issueId, interactionId: created.id });
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
agentId: resolverAgentId,
|
||||
runId: resolverRunId,
|
||||
resolverPolicyRestriction: "anyone",
|
||||
@@ -2604,17 +2414,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const { companyId, goalId, issueId } = await seedConfirmationIssue(`Locked ${action} policy`);
|
||||
const resolverAgentId = randomUUID();
|
||||
const resolverRunId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: resolverAgentId,
|
||||
companyId,
|
||||
name: "Review agent",
|
||||
role: "reviewer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: resolverRunId,
|
||||
companyId,
|
||||
@@ -2705,17 +2505,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const resolverAgentId = randomUUID();
|
||||
const resolverRunId = randomUUID();
|
||||
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
|
||||
await db.insert(agents).values({
|
||||
id: resolverAgentId,
|
||||
companyId,
|
||||
name: "Review agent",
|
||||
role: "reviewer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: resolverRunId,
|
||||
companyId,
|
||||
@@ -2806,12 +2596,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: ["file-c", "file-a"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -2854,34 +2639,19 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: [],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toThrow("Select at least 1 checkbox confirmation option(s)");
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: ["missing"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toThrow("Unknown checkbox confirmation optionId: missing");
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: ["one", "two", "three"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -3217,12 +2987,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const issueId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -3231,17 +2996,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Senior Product Engineer",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Senior Product Engineer", "engineer"));
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
@@ -3268,12 +3023,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
agentId,
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3315,12 +3065,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
agentId,
|
||||
});
|
||||
|
||||
const resumed = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, agentOwnedConfirmation.id, {}, {
|
||||
const resumed = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), agentOwnedConfirmation.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3358,12 +3103,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
agentId,
|
||||
});
|
||||
|
||||
const acceptedWhileActive = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, activeConfirmation.id, {}, {
|
||||
const acceptedWhileActive = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), activeConfirmation.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3380,17 +3120,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
it("atomically returns an accepted Plan-mode issue to its agent in Auto mode", async () => {
|
||||
const { companyId, goalId, issueId } = await seedConfirmationIssue("Accept a plan into Auto mode");
|
||||
const agentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Plan owner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Plan owner", "engineer"));
|
||||
await db.update(issues).set({
|
||||
status: "in_review",
|
||||
workMode: "planning",
|
||||
@@ -3403,12 +3133,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: { version: 1, prompt: "Accept this plan?", target },
|
||||
}, { agentId });
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, { userId: "local-board" });
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, { userId: "local-board" });
|
||||
|
||||
expect(accepted.interaction).toMatchObject({
|
||||
id: created.id,
|
||||
@@ -3433,17 +3158,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
it("keeps Plan mode for non-plan and checkbox confirmations", async () => {
|
||||
const { companyId, goalId, issueId } = await seedConfirmationIssue("Do not auto-transition other confirmations");
|
||||
const agentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Plan owner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Plan owner", "engineer"));
|
||||
await db.update(issues).set({
|
||||
status: "in_review",
|
||||
workMode: "planning",
|
||||
@@ -3454,7 +3169,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
kind: "request_confirmation",
|
||||
payload: { version: 1, prompt: "Accept this unrelated decision?" },
|
||||
}, { agentId });
|
||||
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, nonPlan.id, {}, {
|
||||
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), nonPlan.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
await expect(db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]?.workMode))
|
||||
@@ -3471,7 +3186,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
target,
|
||||
},
|
||||
}, { agentId });
|
||||
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, checkbox.id, {
|
||||
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), checkbox.id, {
|
||||
selectedOptionIds: ["phase-1"],
|
||||
}, { userId: "local-board" });
|
||||
await expect(db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]?.workMode))
|
||||
@@ -3487,7 +3202,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: { version: 1, prompt: "Accept this plan?", target },
|
||||
}, { userId: "local-board" });
|
||||
|
||||
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, created.id, {}, {
|
||||
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3929,12 +3644,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const revisionId = randomUUID();
|
||||
const nextRevisionId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -3943,14 +3653,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.insert(documents).values({
|
||||
id: documentId,
|
||||
companyId,
|
||||
@@ -4013,12 +3716,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
latestRevisionNumber: 2,
|
||||
});
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toMatchObject({
|
||||
status: 409,
|
||||
@@ -4057,12 +3755,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const revisionId = randomUUID();
|
||||
const nextRevisionId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -4071,14 +3764,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.update(issues).set({ workMode: "planning" }).where(eq(issues.id, issueId));
|
||||
// Document is already at revision 2 — revision 1 is stale.
|
||||
await db.insert(documents).values({
|
||||
@@ -4172,12 +3858,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
userId: "local-board",
|
||||
});
|
||||
expect(created).toMatchObject({ status: "pending", kind: "request_confirmation" });
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
})).resolves.toMatchObject({
|
||||
interaction: { status: "accepted" },
|
||||
@@ -4196,12 +3877,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const revisionId = randomUUID();
|
||||
const nextRevisionId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -4210,14 +3886,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.insert(documents).values({
|
||||
id: documentId,
|
||||
companyId,
|
||||
@@ -4357,12 +4026,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const foreignRunId = randomUUID();
|
||||
const kind = options?.kind ?? "request_confirmation";
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
@@ -4379,17 +4043,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
visibility: "default",
|
||||
isPrimary: true,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "CodexCoder", "engineer"));
|
||||
const sourceRunStatus = options?.sourceRunStatus ?? "succeeded";
|
||||
const sourceRunTerminal = sourceRunStatus !== "running";
|
||||
await db.insert(heartbeatRuns).values([
|
||||
@@ -4596,7 +4250,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4624,7 +4278,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4654,7 +4308,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4692,7 +4346,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4722,7 +4376,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4752,7 +4406,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4780,7 +4434,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4815,7 +4469,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-b"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4846,7 +4500,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-a"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4869,7 +4523,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-a"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4896,7 +4550,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-a"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4946,7 +4600,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
created.id,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
|
||||
@@ -184,6 +184,46 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
rmSync(secretsTmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function awsVaultInput(): Parameters<ReturnType<typeof secretService>["createProviderConfig"]>[1] {
|
||||
return {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
};
|
||||
}
|
||||
|
||||
function preparedAwsVersion(externalRef: string, versionId: string, valueSha256: string, fingerprintSha256: string): Awaited<ReturnType<typeof awsSecretsManagerProvider.createSecret>> {
|
||||
return {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId,
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256,
|
||||
fingerprintSha256,
|
||||
externalRef,
|
||||
providerVersionRef: versionId,
|
||||
};
|
||||
}
|
||||
|
||||
function managedAwsVersionRow(secretId: string, externalRef: string): typeof companySecretVersions.$inferInsert {
|
||||
return {
|
||||
secretId,
|
||||
version: 1,
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
providerVersionRef: "aws-version-1",
|
||||
status: "current",
|
||||
};
|
||||
}
|
||||
|
||||
async function seedCompany(name = "Acme") {
|
||||
const companyId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
@@ -925,52 +965,62 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
// provider is `local_encrypted`.
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const createSecretSpy = vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
|
||||
const originalProviderWrite = createSecretSpy.getMockImplementation()!;
|
||||
const uncontendedEntryDurationMs = await measureUncontendedEntryDurationMs(
|
||||
createSecretSpy,
|
||||
originalProviderWrite,
|
||||
() => svc.create(companyId, {
|
||||
name: `aws-baseline-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
providerConfigId: awsVault.id,
|
||||
value: "baseline-runtime-secret",
|
||||
}),
|
||||
);
|
||||
|
||||
const events: string[] = [];
|
||||
const cleanupEntered = deferred<void>();
|
||||
const providerEntered = deferred<void>();
|
||||
createSecretSpy.mockImplementationOnce(async (input) => {
|
||||
events.push("aws-provider-enter");
|
||||
providerEntered.resolve();
|
||||
return originalProviderWrite(input);
|
||||
});
|
||||
let releaseCleanup!: () => void;
|
||||
const cleanupGate = new Promise<void>((resolve) => {
|
||||
releaseCleanup = resolve;
|
||||
});
|
||||
const cleanupCall = withAccountHomeSecretMutationLock(undefined, companyId, async () => {
|
||||
events.push("cleanup-enter");
|
||||
cleanupEntered.resolve();
|
||||
await cleanupGate;
|
||||
events.push("cleanup-exit");
|
||||
});
|
||||
// Give the cleanup a chance to acquire the lock before the AWS create
|
||||
// starts racing for the same lock.
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
const createCall = svc.create(companyId, {
|
||||
name: `aws-secret-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
providerConfigId: awsVault.id,
|
||||
value: "runtime-secret",
|
||||
});
|
||||
// The AWS create must stay queued behind the held lock, the same way a
|
||||
// `local_encrypted` create does.
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(events).toEqual(["cleanup-enter"]);
|
||||
|
||||
releaseCleanup();
|
||||
await cleanupCall;
|
||||
const created = await createCall;
|
||||
expect(events).toEqual(["cleanup-enter", "cleanup-exit"]);
|
||||
let createCall: ReturnType<typeof svc.create> | undefined;
|
||||
let outcomes: PromiseSettledResult<unknown>[] = [];
|
||||
try {
|
||||
await awaitEntryOrOperationFailure(cleanupEntered.promise, cleanupCall, "cleanupCall");
|
||||
createCall = svc.create(companyId, {
|
||||
name: `aws-secret-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
providerConfigId: awsVault.id,
|
||||
value: "runtime-secret",
|
||||
});
|
||||
void createCall.catch(() => {});
|
||||
await waitEntryDetectionWindow(uncontendedEntryDurationMs, providerEntered.promise);
|
||||
expect(events).toEqual(["cleanup-enter"]);
|
||||
expect(createSecretSpy).toHaveBeenCalledTimes(ENTRY_DETECTION_BASELINE_SAMPLE_COUNT);
|
||||
} finally {
|
||||
releaseCleanup();
|
||||
outcomes = await Promise.allSettled([cleanupCall, createCall]);
|
||||
}
|
||||
for (const outcome of outcomes) {
|
||||
if (outcome.status === "rejected") throw outcome.reason;
|
||||
}
|
||||
const created = await createCall!;
|
||||
expect(events).toEqual(["cleanup-enter", "cleanup-exit", "aws-provider-enter"]);
|
||||
expect(created.status).toBe("active");
|
||||
});
|
||||
|
||||
@@ -980,35 +1030,9 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
// `local_encrypted` rotate.
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
|
||||
versionId: "aws-version-2",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-2",
|
||||
fingerprintSha256: "fingerprint-sha-2",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
|
||||
providerVersionRef: "aws-version-2",
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
const createVersionSpy = vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate", "aws-version-2", "value-sha-2", "fingerprint-sha-2"));
|
||||
const existing = await svc.create(companyId, {
|
||||
name: `aws-secret-rotate-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
@@ -1016,25 +1040,55 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
value: "runtime-secret",
|
||||
});
|
||||
|
||||
const baselineSecret = await svc.create(companyId, {
|
||||
name: `aws-baseline-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
providerConfigId: awsVault.id,
|
||||
value: "baseline-runtime-secret",
|
||||
});
|
||||
const originalProviderWrite = createVersionSpy.getMockImplementation()!;
|
||||
const uncontendedEntryDurationMs = await measureUncontendedEntryDurationMs(
|
||||
createVersionSpy,
|
||||
originalProviderWrite,
|
||||
() => svc.rotate(baselineSecret.id, { value: "baseline-rotated-runtime-secret" }),
|
||||
);
|
||||
|
||||
const events: string[] = [];
|
||||
const cleanupEntered = deferred<void>();
|
||||
const providerEntered = deferred<void>();
|
||||
createVersionSpy.mockImplementationOnce(async (input) => {
|
||||
events.push("aws-provider-enter");
|
||||
providerEntered.resolve();
|
||||
return originalProviderWrite(input);
|
||||
});
|
||||
let releaseCleanup!: () => void;
|
||||
const cleanupGate = new Promise<void>((resolve) => {
|
||||
releaseCleanup = resolve;
|
||||
});
|
||||
const cleanupCall = withAccountHomeSecretMutationLock(undefined, companyId, async () => {
|
||||
events.push("cleanup-enter");
|
||||
cleanupEntered.resolve();
|
||||
await cleanupGate;
|
||||
events.push("cleanup-exit");
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
const rotateCall = svc.rotate(existing.id, { value: "rotated-runtime-secret" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(events).toEqual(["cleanup-enter"]);
|
||||
|
||||
releaseCleanup();
|
||||
await cleanupCall;
|
||||
const rotated = await rotateCall;
|
||||
expect(events).toEqual(["cleanup-enter", "cleanup-exit"]);
|
||||
let rotateCall: ReturnType<typeof svc.rotate> | undefined;
|
||||
let outcomes: PromiseSettledResult<unknown>[] = [];
|
||||
try {
|
||||
await awaitEntryOrOperationFailure(cleanupEntered.promise, cleanupCall, "cleanupCall");
|
||||
rotateCall = svc.rotate(existing.id, { value: "rotated-runtime-secret" });
|
||||
void rotateCall.catch(() => {});
|
||||
await waitEntryDetectionWindow(uncontendedEntryDurationMs, providerEntered.promise);
|
||||
expect(events).toEqual(["cleanup-enter"]);
|
||||
expect(createVersionSpy).toHaveBeenCalledTimes(ENTRY_DETECTION_BASELINE_SAMPLE_COUNT);
|
||||
} finally {
|
||||
releaseCleanup();
|
||||
outcomes = await Promise.allSettled([cleanupCall, rotateCall]);
|
||||
}
|
||||
for (const outcome of outcomes) {
|
||||
if (outcome.status === "rejected") throw outcome.reason;
|
||||
}
|
||||
const rotated = await rotateCall!;
|
||||
expect(events).toEqual(["cleanup-enter", "cleanup-exit", "aws-provider-enter"]);
|
||||
expect(rotated.latestVersion).toBe(2);
|
||||
});
|
||||
|
||||
@@ -2008,11 +2062,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
const companyId = await seedCompany();
|
||||
await seedCompanyMember(companyId, "user-1", "owner");
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const definition = await svc.createUserSecretDefinition(companyId, {
|
||||
key: "github_token",
|
||||
name: "GitHub token",
|
||||
@@ -2070,11 +2120,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
const companyId = await seedCompany();
|
||||
await seedCompanyMember(companyId, "user-1", "owner");
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const definition = await svc.createUserSecretDefinition(companyId, {
|
||||
key: "github_token",
|
||||
name: "GitHub token",
|
||||
@@ -2083,18 +2129,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
});
|
||||
const externalRef =
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user/github-token";
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef,
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion(externalRef, "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db activate failed"));
|
||||
vi.spyOn(db, "delete").mockImplementationOnce(() => {
|
||||
@@ -2164,11 +2199,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
await seedCompanyMember(companyId, "user-1", "owner");
|
||||
await seedCompanyMember(companyId, "user-2", "member");
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const definition = await svc.createUserSecretDefinition(companyId, {
|
||||
key: "github_token",
|
||||
name: "GitHub token",
|
||||
@@ -2180,18 +2211,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
nextVersion += 1;
|
||||
const externalRef =
|
||||
`arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/${input.context.secretKey}`;
|
||||
return {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: `aws-version-${nextVersion}`,
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: `value-sha-${nextVersion}`,
|
||||
fingerprintSha256: `fingerprint-sha-${nextVersion}`,
|
||||
externalRef,
|
||||
providerVersionRef: `aws-version-${nextVersion}`,
|
||||
};
|
||||
return preparedAwsVersion(externalRef, `aws-version-${nextVersion}`, `value-sha-${nextVersion}`, `fingerprint-sha-${nextVersion}`);
|
||||
});
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
const userOneSecret = await svc.createCurrentUserSecretValue(companyId, "user-1", {
|
||||
@@ -2245,29 +2265,14 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
const companyId = await seedCompany();
|
||||
await seedCompanyMember(companyId, "user-1", "owner");
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const definition = await svc.createUserSecretDefinition(companyId, {
|
||||
key: "github_token",
|
||||
name: "GitHub token",
|
||||
provider: "aws_secrets_manager",
|
||||
providerConfigId: awsVault.id,
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user-secret",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user-secret",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user-secret", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
const userSecret = await svc.createCurrentUserSecretValue(companyId, "user-1", {
|
||||
definitionId: definition.id,
|
||||
@@ -2622,11 +2627,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("stores external references without requiring or persisting secret values", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `external-${randomUUID()}`,
|
||||
@@ -2727,11 +2728,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("removes provider vault config locally without deleting remote AWS secrets", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const vault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const vault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `external-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
@@ -2782,11 +2779,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("rejects bindings and env refs to soft-deleted external reference secrets", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const deleted = await svc.create(companyId, {
|
||||
name: "Deleted external",
|
||||
key: "deleted-external",
|
||||
@@ -2816,11 +2809,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("rejects updates to already soft-deleted external reference secrets", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const deleted = await svc.create(companyId, {
|
||||
name: "Deleted patch target",
|
||||
key: "deleted-patch-target",
|
||||
@@ -2839,11 +2828,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("allows re-importing a remote secret after the prior external reference is soft-deleted", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const externalRef =
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/reimportable";
|
||||
const deleted = await svc.create(companyId, {
|
||||
@@ -2892,11 +2877,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("ignores soft-deleted name and key conflicts during remote import", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const deleted = await svc.create(companyId, {
|
||||
name: "Deleted external",
|
||||
key: "deleted-external",
|
||||
@@ -2992,30 +2973,8 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const createSpy = vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
const createVersionSpy = vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
|
||||
versionId: "aws-version-2",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-2",
|
||||
fingerprintSha256: "fingerprint-sha-2",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
|
||||
providerVersionRef: "aws-version-2",
|
||||
});
|
||||
const createSpy = vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
const createVersionSpy = vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key", "aws-version-2", "value-sha-2", "fingerprint-sha-2"));
|
||||
const resolveSpy = vi.spyOn(awsSecretsManagerProvider, "resolveVersion").mockResolvedValue("resolved-secret");
|
||||
|
||||
const secret = await svc.create(companyId, {
|
||||
@@ -3048,25 +3007,8 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("cleans up managed provider secrets when create persistence fails", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const prepared = {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-rollback",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-rollback",
|
||||
providerVersionRef: "aws-version-1",
|
||||
};
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-rollback", "aws-version-1", "value-sha-1", "fingerprint-sha-1");
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(prepared);
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db insert failed"));
|
||||
@@ -3103,25 +3045,8 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("keeps a local cleanup handle when create rollback cleanup fails", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const prepared = {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-cleanup-handle",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-cleanup-handle",
|
||||
providerVersionRef: "aws-version-1",
|
||||
};
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-cleanup-handle", "aws-version-1", "value-sha-1", "fingerprint-sha-1");
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(prepared);
|
||||
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValue(
|
||||
new Error("cleanup failed"),
|
||||
@@ -3171,25 +3096,8 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("reports managed secret persistence rollback failures when local cleanup cannot remove the reserved row", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const prepared = {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-local-cleanup",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-local-cleanup",
|
||||
providerVersionRef: "aws-version-1",
|
||||
};
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-local-cleanup", "aws-version-1", "value-sha-1", "fingerprint-sha-1");
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(prepared);
|
||||
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db activate failed"));
|
||||
@@ -3227,25 +3135,8 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("archives managed provider versions when rotate persistence fails", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
const secret = await svc.create(companyId, {
|
||||
name: "Rotate Rollback",
|
||||
key: "rotate-rollback",
|
||||
@@ -3253,20 +3144,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
providerConfigId: awsVault.id,
|
||||
value: "runtime-secret",
|
||||
});
|
||||
const prepared = {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
|
||||
versionId: "aws-version-2",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-2",
|
||||
fingerprintSha256: "fingerprint-sha-2",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
|
||||
providerVersionRef: "aws-version-2",
|
||||
};
|
||||
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback", "aws-version-2", "value-sha-2", "fingerprint-sha-2");
|
||||
vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(prepared);
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db rotate failed"));
|
||||
@@ -3292,25 +3170,8 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("keeps a disabled version cleanup handle when rotate rollback cleanup fails", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
const secret = await svc.create(companyId, {
|
||||
name: "Rotate Cleanup Handle",
|
||||
key: "rotate-cleanup-handle",
|
||||
@@ -3318,20 +3179,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
providerConfigId: awsVault.id,
|
||||
value: "runtime-secret",
|
||||
});
|
||||
const prepared = {
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
|
||||
versionId: "aws-version-2",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-2",
|
||||
fingerprintSha256: "fingerprint-sha-2",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
|
||||
providerVersionRef: "aws-version-2",
|
||||
};
|
||||
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle", "aws-version-2", "value-sha-2", "fingerprint-sha-2");
|
||||
vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(prepared);
|
||||
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValue(
|
||||
new Error("cleanup failed"),
|
||||
@@ -3372,20 +3220,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
displayName: "AWS secondary",
|
||||
config: { region: "us-west-2", namespace: "prod-usw2" },
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/vault-reassign",
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
externalRef:
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/vault-reassign",
|
||||
providerVersionRef: "aws-version-1",
|
||||
});
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/vault-reassign", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
|
||||
const secret = await svc.create(companyId, {
|
||||
name: "Vault Reassign",
|
||||
key: "vault-reassign",
|
||||
@@ -3426,11 +3261,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("previews AWS remote import candidates with duplicate and collision enrichment", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const duplicate = await svc.create(companyId, {
|
||||
name: "Existing duplicate",
|
||||
provider: "aws_secrets_manager",
|
||||
@@ -3504,11 +3335,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("sanitizes AWS remote import preview provider errors before crossing the service boundary", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const rawProviderMessage =
|
||||
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:ListSecrets";
|
||||
|
||||
@@ -3542,11 +3369,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("sanitizes AWS managed secret create failures and removes the reserved row", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const rawProviderMessage =
|
||||
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:CreateSecret on arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1";
|
||||
|
||||
@@ -3600,11 +3423,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("reports rollback failures when AWS managed secret create cleanup cannot remove the reserved row", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
|
||||
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockRejectedValueOnce(
|
||||
new SecretProviderClientError({
|
||||
@@ -3777,11 +3596,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("writes external reference rotations through the provider when a value is given", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const externalRef = "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/neon-admin";
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `external-${randomUUID()}`,
|
||||
@@ -3832,11 +3647,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("restores the provider current version when external value rotation persistence fails", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const externalRef = "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/rollback";
|
||||
const secret = await svc.create(companyId, {
|
||||
name: "External rollback",
|
||||
@@ -3885,11 +3696,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("rejects external value rotations that also retarget or pin versions", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `external-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
@@ -3927,11 +3734,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("imports AWS remote references row-by-row without fetching plaintext", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const duplicate = await svc.create(companyId, {
|
||||
name: "Existing duplicate",
|
||||
provider: "aws_secrets_manager",
|
||||
@@ -4000,11 +3803,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("sanitizes AWS remote import row provider errors", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const rawProviderMessage =
|
||||
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:DescribeSecret on arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai";
|
||||
vi.spyOn(awsSecretsManagerProvider, "linkExternalSecret").mockRejectedValueOnce(
|
||||
@@ -4047,11 +3846,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("rejects Paperclip-managed AWS namespace refs during preview and import commit", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
|
||||
vi.spyOn(awsSecretsManagerProvider, "listRemoteSecrets").mockResolvedValue({
|
||||
secrets: [
|
||||
@@ -4112,11 +3907,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("skips duplicate AWS remote imports for the same provider vault and canonical ref", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
|
||||
const first = await svc.importRemoteSecrets(companyId, {
|
||||
providerConfigId: awsVault.id,
|
||||
@@ -4200,11 +3991,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
it("rejects generic update retargeting for external reference secrets", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `external-${randomUUID()}`,
|
||||
provider: "aws_secrets_manager",
|
||||
@@ -4263,20 +4050,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
|
||||
await db.insert(companySecretVersions).values({
|
||||
secretId: secret.id,
|
||||
version: 1,
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
providerVersionRef: "aws-version-1",
|
||||
status: "current",
|
||||
});
|
||||
await db.insert(companySecretVersions).values(managedAwsVersionRow(secret.id, externalRef));
|
||||
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
|
||||
@@ -4329,20 +4103,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
|
||||
await db.insert(companySecretVersions).values({
|
||||
secretId: secret.id,
|
||||
version: 1,
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
providerVersionRef: "aws-version-1",
|
||||
status: "current",
|
||||
});
|
||||
await db.insert(companySecretVersions).values(managedAwsVersionRow(secret.id, externalRef));
|
||||
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValueOnce(
|
||||
new Error("provider delete failed"),
|
||||
);
|
||||
@@ -4386,20 +4147,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
status: "deleted",
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
await db.insert(companySecretVersions).values({
|
||||
secretId,
|
||||
version: 1,
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
providerVersionRef: "aws-version-1",
|
||||
status: "current",
|
||||
});
|
||||
await db.insert(companySecretVersions).values(managedAwsVersionRow(secretId, externalRef));
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValueOnce(
|
||||
new SecretProviderClientError({
|
||||
code: "not_found",
|
||||
@@ -4449,20 +4197,7 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
|
||||
await db.insert(companySecretVersions).values({
|
||||
secretId: secret.id,
|
||||
version: 1,
|
||||
material: {
|
||||
scheme: "aws_secrets_manager_v1",
|
||||
secretId: externalRef,
|
||||
versionId: "aws-version-1",
|
||||
source: "managed",
|
||||
},
|
||||
valueSha256: "value-sha-1",
|
||||
fingerprintSha256: "fingerprint-sha-1",
|
||||
providerVersionRef: "aws-version-1",
|
||||
status: "current",
|
||||
});
|
||||
await db.insert(companySecretVersions).values(managedAwsVersionRow(secret.id, externalRef));
|
||||
await svc.disableProviderConfig(vault.id);
|
||||
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
|
||||
|
||||
|
||||
@@ -149,6 +149,56 @@ async function createApprovedToolAction(input: {
|
||||
return { decisionInput, invocation: recorded.invocation, actionRequest: updated };
|
||||
}
|
||||
|
||||
type IdRef = { id: string };
|
||||
|
||||
function sendEmailGrantRow(company: IdRef, agent: IdRef): typeof principalPermissionGrants.$inferInsert {
|
||||
return {
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: agent.id,
|
||||
permissionKey: "tools:use",
|
||||
scope: { toolName: "send_email" },
|
||||
};
|
||||
}
|
||||
|
||||
function agentProfileBindingRow(company: IdRef, profile: IdRef, agent: IdRef): typeof toolProfileBindings.$inferInsert {
|
||||
return {
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
targetType: "agent",
|
||||
targetId: agent.id,
|
||||
};
|
||||
}
|
||||
|
||||
function sendEmailDecisionInput(company: IdRef, agent: IdRef, connection: IdRef, catalogEntry: IdRef): Parameters<ReturnType<typeof toolAccessPolicyService>["decide"]>[0] {
|
||||
return {
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
};
|
||||
}
|
||||
|
||||
function reviewSendPolicyRow(company: IdRef, name: string): typeof toolPolicies.$inferInsert {
|
||||
return {
|
||||
companyId: company.id,
|
||||
name: name,
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
};
|
||||
}
|
||||
|
||||
function executedActionInput(db: ReturnType<typeof createDb>, company: IdRef, agent: IdRef, connection: IdRef, catalogEntry: IdRef, args: Record<string, unknown>): Parameters<typeof createApprovedToolAction>[0] {
|
||||
return {
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
};
|
||||
}
|
||||
|
||||
describeEmbeddedPostgres("tool access policy service", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||
@@ -190,11 +240,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
|
||||
const result = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
allowed: false,
|
||||
@@ -213,12 +259,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
name: "Write tools",
|
||||
defaultAction: "deny",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await db.insert(toolProfileBindings).values({
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
targetType: "agent",
|
||||
targetId: agent.id,
|
||||
});
|
||||
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
|
||||
await db.insert(toolProfileEntries).values({
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
@@ -227,11 +268,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
toolName: "send_email",
|
||||
});
|
||||
|
||||
const result = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
allowed: true,
|
||||
@@ -468,11 +505,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
]);
|
||||
|
||||
for (const agent of [draftAgent, archivedAgent]) {
|
||||
const result = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
expect(result).toMatchObject({
|
||||
allowed: false,
|
||||
decision: "deny",
|
||||
@@ -486,13 +519,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { application, connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(principalPermissionGrants).values({
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: agent.id,
|
||||
permissionKey: "tools:use",
|
||||
scope: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
|
||||
const input = {
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent" as const, actorId: agent.id, agentId: agent.id },
|
||||
@@ -615,12 +642,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
name: "Write tools",
|
||||
defaultAction: "allow",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await db.insert(toolProfileBindings).values({
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
targetType: "agent",
|
||||
targetId: agent.id,
|
||||
});
|
||||
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
|
||||
const wildcardPolicy = await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review wildcard-looking sends",
|
||||
@@ -825,13 +847,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection } = await createTool(db, company.id);
|
||||
await db.insert(principalPermissionGrants).values({
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: agent.id,
|
||||
permissionKey: "tools:use",
|
||||
scope: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
|
||||
const input = {
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent" as const, actorId: agent.id, agentId: agent.id },
|
||||
@@ -857,13 +873,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection } = await createTool(db, company.id);
|
||||
await db.insert(principalPermissionGrants).values({
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: agent.id,
|
||||
permissionKey: "tools:use",
|
||||
scope: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
|
||||
const input = {
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent" as const, actorId: agent.id, agentId: agent.id },
|
||||
@@ -889,13 +899,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection } = await createTool(db, company.id);
|
||||
await db.insert(principalPermissionGrants).values({
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: agent.id,
|
||||
permissionKey: "tools:use",
|
||||
scope: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "One send per minute",
|
||||
@@ -925,13 +929,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection } = await createTool(db, company.id);
|
||||
await db.insert(principalPermissionGrants).values({
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: agent.id,
|
||||
permissionKey: "tools:use",
|
||||
scope: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "One concurrent send per minute",
|
||||
@@ -1105,12 +1103,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
name: "Fallback allow",
|
||||
defaultAction: "allow",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await db.insert(toolProfileBindings).values({
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
targetType: "agent",
|
||||
targetId: agent.id,
|
||||
});
|
||||
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
|
||||
const [redactPolicy] = await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Legacy redact",
|
||||
@@ -1128,17 +1121,9 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
priority: 2,
|
||||
}).returning();
|
||||
|
||||
const redactDecision = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const redactDecision = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
await db.update(toolPolicies).set({ enabled: false }).where(eq(toolPolicies.id, redactPolicy!.id));
|
||||
const validateDecision = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const validateDecision = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
|
||||
expect(redactDecision).toMatchObject({
|
||||
allowed: false,
|
||||
@@ -1164,12 +1149,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
name: "Fallback allow",
|
||||
defaultAction: "allow",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await db.insert(toolProfileBindings).values({
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
targetType: "agent",
|
||||
targetId: agent.id,
|
||||
});
|
||||
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
|
||||
const [policy] = await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Legacy conditional allow",
|
||||
@@ -1179,11 +1159,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
priority: 1,
|
||||
}).returning();
|
||||
|
||||
const result = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
allowed: false,
|
||||
@@ -1203,12 +1179,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
name: "Fallback allow",
|
||||
defaultAction: "allow",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await db.insert(toolProfileBindings).values({
|
||||
companyId: company.id,
|
||||
profileId: profile.id,
|
||||
targetType: "agent",
|
||||
targetId: agent.id,
|
||||
});
|
||||
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
|
||||
const [policy] = await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Broken rate limit",
|
||||
@@ -1218,11 +1189,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
priority: 1,
|
||||
}).returning();
|
||||
|
||||
const result = await toolAccessPolicyService(db).decide({
|
||||
companyId: company.id,
|
||||
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
|
||||
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
|
||||
});
|
||||
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
allowed: false,
|
||||
@@ -1311,12 +1278,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const agentA = await createAgent(db, company.id);
|
||||
const agentB = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review scoped sends",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review scoped sends"));
|
||||
const args = { to: "ops@example.com", body: "same reviewed payload" };
|
||||
await createApprovedToolAction({
|
||||
db,
|
||||
@@ -1348,35 +1310,14 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review versioned sends",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review versioned sends"));
|
||||
const args = { to: "ops@example.com", body: "same payload after tool change" };
|
||||
await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
await db
|
||||
.update(toolCatalogEntries)
|
||||
.set({ versionHash: randomUUID(), schemaHash: randomUUID(), updatedAt: new Date() })
|
||||
.where(eq(toolCatalogEntries.id, catalogEntry.id));
|
||||
const currentVersionAction = await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
const currentVersionAction = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
|
||||
await expect(toolAccessPolicyService(db).createTrustRuleFromActionRequest({
|
||||
companyId: company.id,
|
||||
@@ -1389,31 +1330,10 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review scoped sends",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review scoped sends"));
|
||||
const args = { to: "ops@example.com", body: "same reviewed payload" };
|
||||
const first = await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
|
||||
await expect(toolAccessPolicyService(db).createTrustRuleFromActionRequest({
|
||||
companyId: company.id,
|
||||
@@ -1435,31 +1355,10 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review exact payload sends",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review exact payload sends"));
|
||||
const args = { to: "ops@example.com", body: "same reviewed payload" };
|
||||
const first = await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
|
||||
await expect(toolAccessPolicyService(db).createTrustRuleFromActionRequest({
|
||||
companyId: company.id,
|
||||
@@ -1475,22 +1374,9 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review changed sends",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review changed sends"));
|
||||
const args = { to: "ops@example.com", body: "repeatable" };
|
||||
const first = await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
@@ -1523,22 +1409,9 @@ describeEmbeddedPostgres("tool access policy service", () => {
|
||||
const company = await createCompany(db);
|
||||
const agent = await createAgent(db, company.id);
|
||||
const { connection, catalogEntry } = await createTool(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review revocable sends",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "send_email" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review revocable sends"));
|
||||
const args = { to: "ops@example.com", body: "revocable" };
|
||||
const first = await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
connectionId: connection.id,
|
||||
catalogEntryId: catalogEntry.id,
|
||||
argumentsValue: args,
|
||||
status: "executed",
|
||||
});
|
||||
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
|
||||
await createApprovedToolAction({
|
||||
db,
|
||||
companyId: company.id,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -169,6 +169,34 @@ function fakePluginDispatcher(): PluginToolDispatcher {
|
||||
};
|
||||
}
|
||||
|
||||
type IdRef = { id: string };
|
||||
|
||||
function reviewNotePolicyRow(company: IdRef): typeof toolPolicies.$inferInsert {
|
||||
return {
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
};
|
||||
}
|
||||
|
||||
function readPolicyRow(company: IdRef, name: string): typeof toolPolicies.$inferInsert {
|
||||
return {
|
||||
companyId: company.id,
|
||||
name: name,
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
};
|
||||
}
|
||||
|
||||
function runSessionInput(company: IdRef, agent: IdRef, run: IdRef): Parameters<ReturnType<typeof createToolGatewayService>["createSession"]>[0] {
|
||||
return {
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
};
|
||||
}
|
||||
|
||||
describeEmbeddedPostgres("tool gateway service", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||
@@ -208,18 +236,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("gates write tools with an action request and executes only stored reviewed arguments once", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
const notified = vi.fn();
|
||||
const unsubscribe = subscribeDeliveryWork(db, DELIVERY_QUEUES.toolAction, notified);
|
||||
@@ -313,18 +332,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
companyId: company.id, runId: run.id, issueId: issue.id,
|
||||
responsibleUserId: null, cause: "company_default",
|
||||
});
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
@@ -379,7 +389,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
});
|
||||
const deliveries = toolActionDeliveryService(db, { wakeup });
|
||||
const gateway = createTestToolGatewayService(db, { onToolActionSettled: deliveries.deliver });
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "n1", body: "receipt" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
const [request] = await db.select().from(toolActionRequests);
|
||||
await gateway.approveActionRequest({ companyId: company.id, actionRequestId: request.id, actor: { userId: "reviewer" } });
|
||||
@@ -419,7 +429,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const wakeup = vi.fn(async (agentId: string, input: any) => (await db.insert(agentWakeupRequests).values({ companyId: company.id, agentId, source: input.source, idempotencyKey: input.idempotencyKey, payload: input.payload }).returning())[0] as any);
|
||||
const delivery = toolActionDeliveryService(db, { wakeup });
|
||||
const gateway = createTestToolGatewayService(db, { onToolActionSettled: delivery.deliver });
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
for (const noteId of ["one", "two"]) await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId, body: "reviewed body" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
const requests = await db.select().from(toolActionRequests);
|
||||
expect(requests).toHaveLength(2);
|
||||
@@ -470,7 +480,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
for (let n = 0; n < 12; n++) {
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: `note-${n}`, body: "review me" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
}
|
||||
@@ -500,7 +510,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const { company, agent, issue, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "reassigned" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
const [request] = await db.select().from(toolActionRequests);
|
||||
await gateway.declineActionRequest({ companyId: company.id, actionRequestId: request.id, actor: { userId: "reviewer" } });
|
||||
@@ -518,7 +528,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "expire" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
const [request] = await db.select().from(toolActionRequests);
|
||||
await db.update(toolActionRequests).set({ expiresAt: new Date(Date.now() - 1000) }).where(eq(toolActionRequests.id, request.id));
|
||||
@@ -537,7 +547,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const { company, agent, issue, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "expire" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
const [expiring] = await db.select().from(toolActionRequests);
|
||||
await db.update(toolActionRequests).set({ expiresAt: new Date(Date.now() - 1000) }).where(eq(toolActionRequests.id, expiring.id));
|
||||
@@ -572,7 +582,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
if (body.method === "tools/call") calls++;
|
||||
return new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result: { content: [{ type: "text", text: "Roadmap and meeting notes" }] } }), { headers: { "content-type": "application/json" } });
|
||||
} });
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token)).find(t => t.connectionId === connection.id)!;
|
||||
const call = (limit: number) => gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: { limit } });
|
||||
await expect(call(10)).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
@@ -611,7 +621,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "n1" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
const [request] = await db.select().from(toolActionRequests);
|
||||
// This internal fixture has no connection and cannot grant action-wide access.
|
||||
@@ -624,14 +634,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("refuses to approve an action request through a different interaction", async () => {
|
||||
const { company, agent, issue, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
@@ -654,14 +659,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("prevents another run from consuming an approved action request by id", async () => {
|
||||
const { company, agent, issue, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const originatingSession = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const originatingSession = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: originatingSession.token,
|
||||
@@ -702,14 +702,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("executes an approved identical-args race once and returns the winner result", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const parameters = { noteId: "n1", body: "race body" };
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
@@ -737,14 +732,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("refuses to execute an approved action after its issue closes", async () => {
|
||||
const { company, agent, issue, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const parameters = { noteId: "n1", body: "post-close body" };
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
@@ -774,14 +764,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("keeps pre-execute-on-approve approved requests inert", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const parameters = { noteId: "n1", body: "legacy" };
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters }))
|
||||
.rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
@@ -833,12 +818,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("does not let a stale legacy consumer overwrite the winning approved execution", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
|
||||
let observeLegacyClaim!: () => void;
|
||||
const legacyClaimObserved = new Promise<void>((resolve) => {
|
||||
@@ -854,11 +834,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
await legacyClaimBlocked;
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const parameters = { noteId: "n1", body: "legacy race" };
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
@@ -941,18 +917,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
it("does not leave unsigned action requests pending when signing is unavailable", async () => {
|
||||
vi.stubEnv("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET", "");
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db, { toolActionSigningSecret: " " });
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
@@ -993,14 +960,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
status: "running",
|
||||
contextSnapshot: {},
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
@@ -1016,18 +978,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("cancels a stale pending action request when direct approval sees an invalid signature", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
tool: "mcp-remote-fixture:update_note",
|
||||
@@ -1053,18 +1006,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("declines a pending action request and rejects the invocation (PAP-10859)", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
await expect(gateway.executeTool({
|
||||
sessionToken: session.token,
|
||||
@@ -1107,14 +1051,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
|
||||
it("expires a stale identical request and creates a fresh approval", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Review note writes",
|
||||
policyType: "require_approval",
|
||||
selectors: { toolName: "mcp-remote-fixture:update_note" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const parameters = { noteId: "n1", body: "expires" };
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters }))
|
||||
.rejects.toMatchObject({ reasonCode: "approval_required" });
|
||||
@@ -1138,11 +1077,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
selectors: { toolName: "fixture:delete_everything" },
|
||||
});
|
||||
const gateway = createTestToolGatewayService(db, { pluginToolDispatcher: fakePluginDispatcher() });
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
|
||||
let approvalRequired: ToolGatewayHttpError | null = null;
|
||||
try {
|
||||
@@ -1204,12 +1139,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
it("maps remote MCP elicitation to a durable issue interaction", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
await createRemoteMcpToolFixture(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow read tools",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (_url, init) => new Response(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
@@ -1235,11 +1165,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
try {
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
expect(tool).toBeTruthy();
|
||||
@@ -1296,12 +1222,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
...(persistPreference ? { mcpSessionRequired: true } : {}),
|
||||
},
|
||||
}).where(eq(toolConnections.id, connection.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow read tools",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
|
||||
const requests: Array<{ method: string; sessionId: string | null; protocolVersion: string | null }> = [];
|
||||
const gateway = createTestToolGatewayService(db, {
|
||||
remoteHttpRequest: async (_url, init) => {
|
||||
@@ -1334,11 +1255,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
@@ -1359,12 +1276,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
it("keeps direct tools/call for generic connections without the curated session preference", async () => {
|
||||
const { company, agent, run } = await createRunFixture(db);
|
||||
const { connection } = await createRemoteMcpToolFixture(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow read tools",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
|
||||
const requests: string[] = [];
|
||||
const gateway = createTestToolGatewayService(db, {
|
||||
remoteHttpRequest: async (_url, init) => {
|
||||
@@ -1377,7 +1289,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
});
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
@@ -1406,9 +1318,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
} },
|
||||
} },
|
||||
}).where(eq(toolCatalogEntries.id, catalogEntry.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id, name: "Allow Chat reads", policyType: "allow", selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow Chat reads"));
|
||||
const provider = vi.fn(async (_url, init) => {
|
||||
const payload = JSON.parse(String(init.body));
|
||||
if (payload.method === "notifications/initialized") return new Response(null, { status: 202 });
|
||||
@@ -1417,7 +1327,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
: { content: [{ type: "text", text: "Found matching messages." }] } });
|
||||
});
|
||||
const gateway = createTestToolGatewayService(db, { remoteHttpRequest: provider });
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.upstreamToolName === "search_messages")!;
|
||||
expect(tool).toBeTruthy();
|
||||
@@ -1457,12 +1367,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
sourceTemplateKey: "google-drive",
|
||||
},
|
||||
}).where(eq(toolConnections.id, connection.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow Drive reads",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow Drive reads"));
|
||||
const gateway = createTestToolGatewayService(db, {
|
||||
remoteHttpRequest: async (_url, init) => {
|
||||
const requestBody = JSON.parse(String(init.body)) as { id: string };
|
||||
@@ -1476,11 +1381,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
@@ -1517,12 +1418,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
externalCredential: { provider: "vercel_connect", subjectType: "app" },
|
||||
credentialSecretRefs: [],
|
||||
}).where(eq(connectionGrants.connectionId, connection.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow Vercel-backed reads",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow Vercel-backed reads"));
|
||||
|
||||
const getToken = vi.fn<VercelConnectClient["getToken"]>(async (_request, options) => ({
|
||||
token: options?.forceRefresh ? "fresh-provider-bearer" : "stale-provider-bearer",
|
||||
@@ -1552,7 +1448,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
@@ -1596,8 +1492,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}))).returning();
|
||||
await initializeRunIdentity(db, { companyId: company.id, runId: run.id, issueId: issue.id,
|
||||
responsibleUserId: "A", cause: "instruction" });
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Allow reads",
|
||||
policyType: "allow", selectors: { riskLevel: "read" } });
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow reads"));
|
||||
const resolvedGrants: string[] = [];
|
||||
const gateway = createTestToolGatewayService(db, {
|
||||
oauthGrantRefresher: async ({ grantId }) => {
|
||||
@@ -1608,7 +1503,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
id: JSON.parse(String(init.body)).id, result: { content: [{ type: "text", text: "ok" }] },
|
||||
}), { status: 200, headers: { "content-type": "application/json" } }),
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token)).find(t => t.providerType === "mcp_remote_http")!;
|
||||
for (const [index, user] of ["A", "B", "A"].entries()) {
|
||||
if (index > 0) {
|
||||
@@ -1655,8 +1550,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}
|
||||
await initializeRunIdentity(db, { companyId: company.id, runId: run.id, issueId: issue.id,
|
||||
responsibleUserId: "A", cause: "instruction" });
|
||||
await db.insert(toolPolicies).values({ companyId: company.id, name: "Allow reads",
|
||||
policyType: "allow", selectors: { riskLevel: "read" } });
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow reads"));
|
||||
const refreshed: string[] = [];
|
||||
const dispatched = vi.fn(async (_url: string, init: RequestInit) => {
|
||||
expect(new Headers(init.headers).get("authorization")).toBe(`Bearer token-${upstreamFailure ? 1 : 0}`);
|
||||
@@ -1674,7 +1568,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
return grants.find(grant => grant.id === grantId)!;
|
||||
}, remoteHttpRequest: dispatched,
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token)).find(t => t.providerType === "mcp_remote_http")!;
|
||||
const execution = gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: {} });
|
||||
if (upstreamFailure) await expect(execution).rejects.toMatchObject({ status: 502 });
|
||||
@@ -1732,12 +1626,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
},
|
||||
},
|
||||
}).where(eq(connectionGrants.id, grant.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow customer OAuth reads",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow customer OAuth reads"));
|
||||
|
||||
const oauthGrantRefresher: NonNullable<ToolGatewayServiceOptions["oauthGrantRefresher"]> = vi.fn(async (input) => {
|
||||
if (input.forceRefresh) {
|
||||
@@ -1784,7 +1673,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
@@ -1865,12 +1754,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
},
|
||||
},
|
||||
}).where(eq(connectionGrants.id, grant.id));
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow managed OAuth reads",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow managed OAuth reads"));
|
||||
|
||||
const oauthGrantRefresher: NonNullable<ToolGatewayServiceOptions["oauthGrantRefresher"]> = vi.fn(async (input) => {
|
||||
if (input.forceRefresh) {
|
||||
@@ -1887,7 +1771,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
return new Response(null, { status: 401 });
|
||||
},
|
||||
});
|
||||
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
|
||||
@@ -1926,12 +1810,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
contextSnapshot: {},
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
await createRemoteMcpToolFixture(db, company.id);
|
||||
await db.insert(toolPolicies).values({
|
||||
companyId: company.id,
|
||||
name: "Allow read tools",
|
||||
policyType: "allow",
|
||||
selectors: { riskLevel: "read" },
|
||||
});
|
||||
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (_url, init) => new Response(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
@@ -1940,11 +1819,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
try {
|
||||
const gateway = createTestToolGatewayService(db);
|
||||
const session = await gateway.createSession({
|
||||
companyId: company.id,
|
||||
agentId: agent.id,
|
||||
runId: run.id,
|
||||
});
|
||||
const session = await gateway.createSession(runSessionInput(company, agent, run));
|
||||
const tool = (await gateway.listToolsForSession(session.token))
|
||||
.find((candidate) => candidate.providerType === "mcp_remote_http");
|
||||
expect(tool).toBeTruthy();
|
||||
@@ -2073,7 +1948,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
|
||||
actor: { type: "agent", companyId: company.id, agentId: agent.id, runId: run.id },
|
||||
tool: "fixture:read_status",
|
||||
parameters: { query: "ok", apiKey: "sk-secret-value" },
|
||||
runContext: { companyId: company.id, agentId: agent.id, runId: run.id },
|
||||
runContext: runSessionInput(company, agent, run),
|
||||
});
|
||||
|
||||
expect(executedParameters).toEqual({ query: "ok", apiKey: "sk-secret-value" });
|
||||
|
||||
Reference in new issue
Block a user