Add application edit actions

This commit is contained in:
Dotta committed 2026-06-10 15:36:42 +00:00
1 parent 401d82f6c4
commit fb4d7cf221
5 files changed
+400 -16

No files matched your search

@@ -53,11 +53,11 @@ function mockToolsList(tools: unknown[]) {
} as Response);
}
function createRouteApp(db: ReturnType<typeof createDb>) {
function createRouteApp(db: ReturnType<typeof createDb>, actor?: Express.Request["actor"]) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.actor = {
req.actor = actor ?? {
type: "board",
userId: "board-user",
userName: "Board User",
@@ -470,6 +470,90 @@ describeEmbeddedPostgres("tool access service", () => {
});
});
it("updates tool applications through the board route and records activity", async () => {
const company = await createCompany(db);
const service = toolAccessService(db);
const app = createRouteApp(db);
const application = await service.createApplication(company.id, {
name: "Editable app",
description: "Before",
type: "mcp_http",
});
const res = await request(app)
.patch(`/api/tool-applications/${application.id}`)
.send({ name: "Edited app", description: "After" });
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
id: application.id,
companyId: company.id,
name: "Edited app",
description: "After",
type: "mcp_http",
});
const activities = await db.select().from(activityLog).where(eq(activityLog.entityId, application.id));
expect(activities).toEqual([
expect.objectContaining({
action: "tool_application.updated",
companyId: company.id,
details: expect.objectContaining({ name: "Edited app" }),
}),
]);
});
it("returns 409 instead of 500 when an application update collides with a duplicate name", async () => {
const company = await createCompany(db);
const service = toolAccessService(db);
const app = createRouteApp(db);
await service.createApplication(company.id, { name: "Existing app", type: "mcp_http" });
const application = await service.createApplication(company.id, { name: "Editable app", type: "mcp_http" });
const res = await request(app)
.patch(`/api/tool-applications/${application.id}`)
.send({ name: "Existing app" });
expect(res.status).toBe(409);
expect(res.body).toMatchObject({
error: "A tool access record with that name already exists",
});
});
it("returns 403 for cross-company application updates and 404 for missing applications", async () => {
const allowedCompany = await createCompany(db);
const otherCompany = await createCompany(db);
const application = await toolAccessService(db).createApplication(otherCompany.id, {
name: "Other company app",
type: "mcp_http",
});
const app = createRouteApp(db, {
type: "board",
userId: "member-user",
userName: "Member User",
userEmail: null,
companyIds: [allowedCompany.id],
memberships: [
{
companyId: allowedCompany.id,
membershipRole: "owner",
status: "active",
},
],
isInstanceAdmin: false,
source: "session",
});
const forbiddenRes = await request(app)
.patch(`/api/tool-applications/${application.id}`)
.send({ name: "Forbidden edit" });
const missingRes = await request(createRouteApp(db))
.patch(`/api/tool-applications/${randomUUID()}`)
.send({ name: "Missing edit" });
expect(forbiddenRes.status).toBe(403);
expect(missingRes.status).toBe(404);
});
it("links run tool decisions to invocations, audit events, and pending action requests", async () => {
const company = await createCompany(db);
const [agent] = await db.insert(agents).values({
+15 -11
View File
@@ -127,17 +127,21 @@ export function toolAccessRoutes(
assertBoard(req);
const existing = await svc.getApplication(req.params.applicationId as string);
assertCompanyAccess(req, existing.companyId);
const application = await svc.updateApplication(existing.id, req.body);
await logActivity(db, {
companyId: application.companyId,
actorType: "user",
actorId: req.actor.userId ?? "board",
action: "tool_application.updated",
entityType: "tool_application",
entityId: application.id,
details: { status: application.status, name: application.name },
});
res.json(application);
try {
const application = await svc.updateApplication(existing.id, req.body);
await logActivity(db, {
companyId: application.companyId,
actorType: "user",
actorId: req.actor.userId ?? "board",
action: "tool_application.updated",
entityType: "tool_application",
entityId: application.id,
details: { status: application.status, name: application.name },
});
res.json(application);
} catch (error) {
svc.ensureNoDuplicateNameError(error);
}
});
router.get("/companies/:companyId/tools/connections", async (req, res) => {
+30 -1
View File
@@ -2431,7 +2431,36 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
},
ensureNoDuplicateNameError: (error: unknown) => {
if (error instanceof Error && /duplicate key value/.test(error.message)) {
const maybeRecord = typeof error === "object" && error !== null ? error as Record<string, unknown> : null;
const cause = maybeRecord?.cause;
const maybeCause = typeof cause === "object" && cause !== null ? cause as Record<string, unknown> : null;
const message = [
error instanceof Error ? error.message : String(error),
maybeRecord && typeof maybeRecord.detail === "string" ? maybeRecord.detail : null,
maybeCause instanceof Error ? maybeCause.message : null,
maybeCause && typeof maybeCause.detail === "string" ? maybeCause.detail : null,
].filter(Boolean).join("\n");
const code =
maybeRecord && typeof maybeRecord.code === "string"
? maybeRecord.code
: maybeCause && typeof maybeCause.code === "string"
? maybeCause.code
: null;
const constraint =
maybeRecord && typeof maybeRecord.constraint === "string"
? maybeRecord.constraint
: maybeRecord && typeof maybeRecord.constraint_name === "string"
? maybeRecord.constraint_name
: maybeCause && typeof maybeCause.constraint === "string"
? maybeCause.constraint
: maybeCause && typeof maybeCause.constraint_name === "string"
? maybeCause.constraint_name
: null;
if (
code === "23505" ||
constraint?.includes("tool_applications") ||
/duplicate key value|unique constraint|tool_applications_company_id_name_unique/i.test(message)
) {
throw conflict("A tool access record with that name already exists");
}
throw error;