378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { createAgentToken, initializeAgentTokenSigningKey } from "../../src/main/agent-auth";
|
|
import { createApiServer } from "../../src/main/api-server";
|
|
import { AppDatabase } from "../../src/main/database";
|
|
import { createTestApiServerCallbacks } from "../support/api-server-callbacks";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
async function createTempDir(prefix: string) {
|
|
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
async function createHarness() {
|
|
const profileDir = await createTempDir("agentcompany-work-product-url-");
|
|
initializeAgentTokenSigningKey(profileDir);
|
|
const db = new AppDatabase(profileDir);
|
|
db.init();
|
|
db.core.connectors.updateConnectorHealth({
|
|
id: "openai-codex",
|
|
status: "ready",
|
|
authState: "authenticated",
|
|
version: "test",
|
|
lastError: null,
|
|
});
|
|
|
|
const companyId = db.core.company.saveCompany({
|
|
name: "URL Policy Co",
|
|
description: "Tests work product URL trust policy",
|
|
status: "active",
|
|
});
|
|
const workspaceId = db.core.workspaces.saveWorkspace({
|
|
companyId,
|
|
name: "Primary",
|
|
localPath: profileDir,
|
|
repoUrl: "",
|
|
repoRef: "",
|
|
isPrimary: true,
|
|
});
|
|
const agentId = db.core.agents.saveAgent({
|
|
companyId,
|
|
name: "Reporter",
|
|
role: "Researcher",
|
|
title: "Reporter",
|
|
status: "active",
|
|
reportsTo: null,
|
|
connectorId: "openai-codex",
|
|
workspaceId,
|
|
model: null,
|
|
capabilities: "Writes reports",
|
|
budgetMonthlyUsd: 100,
|
|
});
|
|
const taskId = db.core.tasks.saveTask({
|
|
companyId,
|
|
title: "Publish external report",
|
|
description: "Attach a report link.",
|
|
assigneeAgentId: agentId,
|
|
workspaceId,
|
|
priority: "medium",
|
|
status: "in_progress",
|
|
});
|
|
const run = db.core.runs.createRun({
|
|
companyId,
|
|
taskId,
|
|
agentId,
|
|
workspaceId,
|
|
connectorId: "openai-codex",
|
|
});
|
|
const token = createAgentToken(agentId, companyId, run.id);
|
|
const server = createApiServer(db, createTestApiServerCallbacks());
|
|
const port = await server.start();
|
|
|
|
return {
|
|
db,
|
|
server,
|
|
companyId,
|
|
taskId,
|
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"X-Agent-Company-Run-Id": run.id,
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("work product URL API policy", () => {
|
|
it("rejects JSON bodies that declare a non UTF-8 charset", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: {
|
|
...harness.headers,
|
|
"Content-Type": "application/json; charset=gbk",
|
|
},
|
|
body: JSON.stringify({ type: "link", provider: "external", title: "Report" }),
|
|
});
|
|
|
|
expect(response.status).toBe(415);
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects JSON bodies with duplicate charset declarations", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: {
|
|
...harness.headers,
|
|
"Content-Type": "application/json; charset=gbk; charset=utf-8",
|
|
},
|
|
body: JSON.stringify({ type: "link", provider: "external", title: "Report" }),
|
|
});
|
|
|
|
expect(response.status).toBe(415);
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects unknown work product taxonomy during creation", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const cases = [
|
|
{ type: "spreadsheet", provider: "external", title: "Bad type" },
|
|
{ type: "link", provider: "slack", title: "Bad provider" },
|
|
{ type: "link", provider: "external", title: "Bad status", status: "ship_it" },
|
|
{ type: "link", provider: "external", title: "Bad review", reviewState: "rubber_stamp" },
|
|
];
|
|
|
|
for (const body of cases) {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: harness.headers,
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
}
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects unknown work product taxonomy during updates", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const id = harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
type: "link",
|
|
provider: "external",
|
|
title: "External report",
|
|
status: "active",
|
|
reviewState: "none",
|
|
});
|
|
const cases = [
|
|
{ type: "spreadsheet" },
|
|
{ provider: "slack" },
|
|
{ status: "ship_it" },
|
|
{ reviewState: "rubber_stamp" },
|
|
];
|
|
|
|
for (const body of cases) {
|
|
const response = await fetch(`${harness.baseUrl}/api/work-products/${id}`, {
|
|
method: "PATCH",
|
|
headers: harness.headers,
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
}
|
|
const row = harness.db.queryAll<{
|
|
type: string;
|
|
provider: string;
|
|
status: string;
|
|
review_state: string;
|
|
}>("select type, provider, status, review_state from work_products where id = ? limit 1", id)[0];
|
|
expect(row).toEqual({
|
|
type: "link",
|
|
provider: "external",
|
|
status: "active",
|
|
review_state: "none",
|
|
});
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects unknown work product taxonomy at the database boundary", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
expect(() => harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
type: "link",
|
|
// @ts-expect-error verifies runtime validation for direct DB callers.
|
|
provider: "slack",
|
|
title: "Bad provider",
|
|
})).toThrow();
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
|
|
expect(() => harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
type: "link",
|
|
provider: "external",
|
|
title: "Empty status",
|
|
// @ts-expect-error verifies runtime validation for direct DB callers.
|
|
status: "",
|
|
})).toThrow();
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
|
|
expect(() => harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
type: "link",
|
|
provider: "external",
|
|
title: "Empty review state",
|
|
// @ts-expect-error verifies runtime validation for direct DB callers.
|
|
reviewState: "",
|
|
})).toThrow();
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
|
|
const id = harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
type: "link",
|
|
provider: "external",
|
|
title: "External report",
|
|
});
|
|
expect(() => harness.db.core.documents.updateWorkProduct(id, harness.companyId, {
|
|
// @ts-expect-error verifies runtime validation for direct DB callers.
|
|
status: "ship_it",
|
|
})).toThrow();
|
|
const row = harness.db.queryAll<{ status: string }>("select status from work_products where id = ? limit 1", id)[0];
|
|
expect(row?.status).toBe("active");
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects blank URLs during work product creation", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: harness.headers,
|
|
body: JSON.stringify({
|
|
type: "link",
|
|
provider: "external",
|
|
title: "Blank URL",
|
|
url: " ",
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects custom schemes during work product creation", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: harness.headers,
|
|
body: JSON.stringify({
|
|
type: "link",
|
|
provider: "external",
|
|
title: "Local scheme",
|
|
url: "vscode://file/etc/passwd",
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects credential-bearing URLs during work product creation", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: harness.headers,
|
|
body: JSON.stringify({
|
|
type: "link",
|
|
provider: "external",
|
|
title: "Credential URL",
|
|
url: "https://user:pass@example.com/reports/q1",
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(harness.db.queryAll("select id from work_products")).toHaveLength(0);
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("stores http links for later confirmation during open", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const response = await fetch(`${harness.baseUrl}/api/issues/${harness.taskId}/work-products`, {
|
|
method: "POST",
|
|
headers: harness.headers,
|
|
body: JSON.stringify({
|
|
type: "link",
|
|
provider: "external",
|
|
title: "External report",
|
|
url: " https://example.com/reports/q1 ",
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
const row = harness.db.queryAll<{ url: string }>("select url from work_products limit 1")[0];
|
|
expect(row?.url).toBe("https://example.com/reports/q1");
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("rejects custom schemes during work product updates", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const id = harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
type: "link",
|
|
provider: "external",
|
|
title: "External report",
|
|
url: "https://example.com/reports/q1",
|
|
});
|
|
|
|
const response = await fetch(`${harness.baseUrl}/api/work-products/${id}`, {
|
|
method: "PATCH",
|
|
headers: harness.headers,
|
|
body: JSON.stringify({ url: "file:///etc/passwd" }),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
const row = harness.db.queryAll<{ url: string }>("select url from work_products where id = ? limit 1", id)[0];
|
|
expect(row?.url).toBe("https://example.com/reports/q1");
|
|
} finally {
|
|
harness.server.stop();
|
|
harness.db.close();
|
|
}
|
|
});
|
|
});
|