Files
PaperClipAI/server/src/__tests__/cases-routes.test.ts
T
DottaandClaude Opus 5.5 eb5ad4c920 fix(cases): say a Cases-disabled denial is final and name the Experimental setting (#20892)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cases is an experimental feature; an instance admin turns it on in
Experimental settings
> - When Cases is off, every case and workflow route answers 403 "Cases
are disabled" with no other information
> - A chat agent asked to "open a customer case" could not tell whether
the denial was final or what to offer
> - In one eval run it created a project and a task in place of the
case; in another it spent most of its turn searching routes before it
replied
> - This pull request gives the denial a code and a final, actionable
remediation, like the board-only denial
> - The benefit is a fast, truthful reply that names the setting and
lets the person choose

## Linked Issues or Issue Description

**What happened**
In the chat eval cases.cases-disabled ("Open a customer case for this
bug." with Cases off), the agent got 403 "Cases are disabled". In one
run it then created a project and a bug task without asking. In a later
run it searched many routes and instance settings for about 100 seconds
before it replied.

**Expected behavior**
The denial says that Cases is off, that the denial is final, where an
instance admin turns it on, and that the agent must not create a
substitute record before the person chooses. The agent replies at once
and offers the setting or an ordinary task.

**Steps to reproduce**
1. Turn off Cases in Experimental settings.
2. As an agent, call GET /api/companies/{companyId}/cases.
3. See a bare 403 "Cases are disabled" with no code or remediation.

## What Changed

- Rebased on #20894, which already gave the case routes a
`cases_disabled` code, a final remediation and the hidden-settings
check. This PR moves that remediation into one helper used by every case
and workflow path, and adds: no substitute task or project before the
person chooses, and the offer of an ordinary task.
- New `casesDisabledError()` in `server/src/services/cases-disabled.ts`:
same 403 message, plus `code: "cases_disabled"` and a remediation. The
remediation says the denial is final, forbids a substitute task or
project before the person chooses, links `[Experimental
settings](/<prefix>/company/settings/instance/experimental)`, and offers
an ordinary task.
- Native tool results carry only the error message, so the native case
tools, workflow tools and runner tool authority inline the remediation
in the message (`casesDisabledError({ inlineRemediation: true })`).
- Case routes, workflow service, case handoffs, native case tools,
native workflow tools and the runner tool authority use the helper
instead of a bare `forbidden("Cases are disabled")`.
- Tests: `cases-routes.test.ts` checks the code and remediation;
`native-case-tools.integration.test.ts` checks the native message
carries the remediation (red on master, green here).

## Verification

- `npx vitest run src/__tests__/cases-routes.test.ts
src/services/case-handoffs.test.ts
src/services/native-runtime/native-case-tools.integration.test.ts
src/__tests__/heartbeat-case-handoff.test.ts` (166 passed).
- The cases.cases-disabled chat eval is rerun on a temporary branch;
results are recorded in the PR conversation.

## Risks

- Low risk. The status and message do not change. The error body adds
`code` and `remediation`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:36:08 -06:00

1470 lines
78 KiB
TypeScript

import { createHash, randomUUID } from "node:crypto";
import express from "express";
import request from "supertest";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
activityLog,
agents,
assets,
caseAttachments,
caseAccessSources,
caseHandoffs,
caseDocuments,
caseEvents,
caseIssueLinks,
caseLabels,
cases,
companies,
companyMemberships,
createDb,
documentAnnotationComments,
documentAnnotationThreads,
documents,
documentRevisions,
heartbeatRuns,
instanceSettings,
issueRelations,
issues,
labels,
projects,
} from "@paperclipai/db";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { errorHandler } from "../middleware/error-handler.js";
import { actorMiddleware } from "../middleware/auth.js";
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
import { buildCasePatchUpdateValues, caseRoutes } from "../routes/cases.js";
import { caseHandoffRoutes } from "../routes/case-handoffs.js";
import { canActorReadAssetContent } from "../services/asset-content-access.js";
import { caseActivityReadSqlCondition } from "../services/case-activity-access.js";
import { activityService } from "../services/activity.js";
import { agentActionAuditService } from "../services/agent-action-audit.js";
import { assertCaseTrustedForExecution, canActorReadCase, canActorReceiveCase, retainCaseAccessSource } from "../services/case-access.js";
import { instanceSettingsService } from "../services/instance-settings.js";
import { subscribeCompanyLiveEvents } from "../services/live-events.js";
import type { StorageService } from "../storage/types.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres cases route tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
describeEmbeddedPostgres("cases routes", () => {
it("omits completedAt from non-status case patches", () => {
const now = new Date("2026-07-10T00:00:00.000Z");
const completedAt = new Date("2026-07-09T00:00:00.000Z");
expect(buildCasePatchUpdateValues({ title: "Rename" }, { status: "todo", completedAt: null }, now)).not.toHaveProperty("completedAt");
expect(buildCasePatchUpdateValues({ title: "Rename" }, { status: "done", completedAt }, now)).not.toHaveProperty("completedAt");
const statusPatch = buildCasePatchUpdateValues({ status: "done" }, { status: "todo", completedAt: null }, now);
expect(statusPatch).toHaveProperty("completedAt");
expect(statusPatch.completedAt).toBeInstanceOf(Date);
});
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
const previousAgentJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET;
const liveEventUnsubscribers: Array<() => void> = [];
const storage: StorageService = {
provider: "local_disk",
async putFile(input) {
if (!Buffer.isBuffer(input.body)) throw new Error("Expected bounded bytes");
return {
provider: "local_disk",
objectKey: `${input.namespace}/${randomUUID()}`,
contentType: input.contentType,
byteSize: input.body.length,
sha256: createHash("sha256").update(input.body).digest("hex"),
originalFilename: input.originalFilename,
};
},
async getObject() {
throw new Error("not used");
},
async headObject() {
return { exists: false };
},
async deleteObject() {},
};
beforeAll(async () => {
process.env.PAPERCLIP_AGENT_JWT_SECRET = "cases-routes-test-secret";
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-cases-routes-");
db = createDb(tempDb.connectionString);
}, 20_000);
afterEach(async () => {
for (const unsubscribe of liveEventUnsubscribers.splice(0)) unsubscribe();
await db.delete(activityLog);
await db.delete(documentAnnotationComments);
await db.delete(documentAnnotationThreads);
await db.delete(caseAttachments);
await db.delete(caseLabels);
await db.delete(caseDocuments);
await db.delete(caseIssueLinks);
await db.delete(caseEvents);
await db.delete(cases);
await db.delete(documentRevisions);
await db.delete(documents);
await db.delete(assets);
await db.delete(labels);
await db.delete(issueRelations);
await db.delete(issues);
await db.delete(heartbeatRuns);
await db.delete(projects);
await db.delete(agents);
await db.delete(companyMemberships);
await db.delete(companies);
await db.delete(instanceSettings);
});
afterAll(async () => {
await tempDb?.cleanup();
if (previousAgentJwtSecret === undefined) {
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
} else {
process.env.PAPERCLIP_AGENT_JWT_SECRET = previousAgentJwtSecret;
}
});
function app(actor: Express.Request["actor"]) {
const instance = express();
instance.use(express.json());
instance.use((req, _res, next) => {
req.actor = actor;
next();
});
instance.use("/api", caseRoutes(db, storage));
instance.use("/api", caseHandoffRoutes(db));
instance.use(errorHandler);
return instance;
}
function authenticatedApp() {
const instance = express();
instance.use(express.json());
instance.use(actorMiddleware(db, { deploymentMode: "authenticated" }));
instance.use("/api", caseRoutes(db, storage));
instance.use("/api", caseHandoffRoutes(db));
instance.use(errorHandler);
return instance;
}
async function enableCases() {
await instanceSettingsService(db).updateExperimental({ enableCases: true });
}
async function seedCompany(prefix = "CASE") {
const [company] = await db.insert(companies).values({
name: `${prefix} Co`,
issuePrefix: `${prefix}${randomUUID().replace(/-/g, "").slice(0, 4)}`,
}).returning();
return company!;
}
async function seedAgent(companyId: string) {
const [agent] = await db.insert(agents).values({
companyId,
name: "Cases Agent",
role: "engineer",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
}).returning();
return agent!;
}
const boardActor: Express.Request["actor"] = {
type: "board",
userId: "board-user",
source: "local_implicit",
isInstanceAdmin: true,
};
it("gates every case route when enableCases is off", async () => {
const company = await seedCompany("OFF");
const [caseRow] = await db.insert(cases).values({
companyId: company.id,
caseNumber: 1,
identifier: `${company.issuePrefix}-C1`,
caseType: "bug",
title: "Hidden case",
}).returning();
const http = request(app(boardActor));
const denied = await http.get(`/api/companies/${company.id}/cases`).expect(403);
// The denial is final and names the page where an instance admin turns Cases on.
expect(denied.body).toMatchObject({ error: "Cases are disabled", code: "cases_disabled" });
expect(denied.body.remediation).toContain("[Experimental settings](/<prefix>/company/settings/instance/experimental)");
expect(denied.body.remediation).toContain("Agent credentials cannot change that setting");
// Hiding only the Cases switch keeps the page, so the denial must not send the person there.
const previousHidden = process.env.PAPERCLIP_HIDDEN_SETTINGS;
process.env.PAPERCLIP_HIDDEN_SETTINGS = "instance.experimental.enableCases";
try {
const hiddenDenial = await http.get(`/api/companies/${company.id}/cases`).expect(403);
expect(hiddenDenial.body.remediation).toContain("An instance operator controls it.");
expect(hiddenDenial.body.remediation).not.toContain("/instance/experimental");
} finally {
if (previousHidden === undefined) delete process.env.PAPERCLIP_HIDDEN_SETTINGS;
else process.env.PAPERCLIP_HIDDEN_SETTINGS = previousHidden;
}
await http.get(`/api/companies/${company.id}/cases/types`).expect(403);
await http.post(`/api/companies/${company.id}/cases`).send({ caseType: "bug", title: "Bug" }).expect(403);
await http.get(`/api/cases/${caseRow!.id}`).expect(403);
await http.patch(`/api/cases/${caseRow!.id}`).send({ status: "in_progress" }).expect(403);
await http.put(`/api/cases/${caseRow!.id}/documents/body`).send({ body: "Body" }).expect(403);
await http.get(`/api/cases/${caseRow!.id}/documents/body/annotations`).expect(403);
await http.post(`/api/cases/${caseRow!.id}/links`).send({ issueId: randomUUID(), role: "work" }).expect(403);
await http.post(`/api/cases/${caseRow!.id}/attachments`).attach("file", Buffer.from("x"), "x.txt").expect(403);
await http.get(`/api/cases/${caseRow!.id}/events`).expect(403);
});
it("tells a caller that Cases is off for good and names the setting instead of a substitute", async () => {
const company = await seedCompany("OFFX");
const response = await request(app(boardActor)).get(`/api/companies/${company.id}/cases`).expect(403);
expect(response.body).toMatchObject({ error: "Cases are disabled", code: "cases_disabled" });
expect(response.body.remediation).toContain("this denial is final");
expect(response.body.remediation).toContain("Do not create a task, project or other record in place of the case before the person chooses");
expect(response.body.remediation).toContain("[Experimental settings](/<prefix>/company/settings/instance/experimental)");
expect(response.body.remediation).toContain("offer to track the request as an ordinary task instead");
});
it("returns not found for unknown case IDs on the former shared routes", async () => {
await enableCases();
const http = request(app(boardActor));
const unknownId = randomUUID();
await http.get(`/api/cases/${unknownId}`).expect(404);
await http.patch(`/api/cases/${unknownId}`).send({ status: "in_progress" }).expect(404);
await http.put(`/api/cases/${unknownId}/documents/body`).send({ body: "Body" }).expect(404);
await http.get(`/api/cases/${unknownId}/documents/body/revisions`).expect(404);
await http.get(`/api/cases/${unknownId}/events`).expect(404);
});
it("creates cases and upserts idempotently by type and key", async () => {
await enableCases();
const company = await seedCompany("UPS");
const http = request(app(boardActor));
const first = await http
.post(`/api/companies/${company.id}/cases`)
.send({
caseType: "security",
key: "CVE-1",
title: "Investigate report",
fields: { severity: "high" },
})
.expect(201);
const second = await http
.post(`/api/companies/${company.id}/cases`)
.send({
caseType: "security",
key: "CVE-1",
title: "Investigate report again",
fields: { severity: "critical" },
})
.expect(200);
expect(second.body.id).toBe(first.body.id);
expect(first.body.identifier).toBe(`${company.issuePrefix.toUpperCase()}-C1`);
const all = await db.select().from(cases);
expect(all).toHaveLength(1);
expect(all[0]!.title).toBe("Investigate report again");
expect(all[0]!.fields).toEqual({ severity: "critical" });
});
it("converges concurrent keyed upserts to one case", async () => {
await enableCases();
const company = await seedCompany("RCE");
const http = request(app(boardActor));
const requests = [
http.post(`/api/companies/${company.id}/cases`).send({
caseType: "release_note",
key: "2026-07-07",
title: "Release note A",
fields: { channel: "stable" },
}),
http.post(`/api/companies/${company.id}/cases`).send({
caseType: "release_note",
key: "2026-07-07",
title: "Release note B",
fields: { channel: "canary" },
}),
];
const responses = await Promise.all(requests);
expect(responses.map((res) => res.status).sort()).toEqual([200, 201]);
expect(responses[0]!.body.id).toBe(responses[1]!.body.id);
const all = await db.select().from(cases);
expect(all).toHaveLength(1);
expect(all[0]!.caseType).toBe("release_note");
expect(all[0]!.key).toBe("2026-07-07");
expect(["Release note A", "Release note B"]).toContain(all[0]!.title);
expect([{ channel: "stable" }, { channel: "canary" }]).toContainEqual(all[0]!.fields);
});
it("upserts keyless cases by company and type", async () => {
await enableCases();
const company = await seedCompany("NUL");
const http = request(app(boardActor));
const first = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "release_note", title: "Draft release note" })
.expect(201);
const second = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "release_note", title: "Updated release note" })
.expect(200);
expect(second.body.id).toBe(first.body.id);
expect(second.body.key).toBeNull();
expect(second.body.title).toBe("Updated release note");
const all = await db.select().from(cases);
expect(all).toHaveLength(1);
});
it("rejects the removed createOnly option without changing existing passive records", async () => {
await enableCases();
const company = await seedCompany("NEW"), http = request(app(boardActor));
const original = await http.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "research", title: "Existing passive case" }).expect(201);
await http.post(`/api/companies/${company.id}/cases`)
.send({ createOnly: true, caseType: "research", title: "Must not overwrite" }).expect(400);
const all = await db.select().from(cases).where(eq(cases.companyId, company.id));
expect(all).toMatchObject([{ id: original.body.id, title: "Existing passive case", workIssueId: null }]);
expect(await db.select().from(issues).where(eq(issues.companyId, company.id))).toHaveLength(0);
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, company.id))).toHaveLength(0);
});
it("resolves cases by identifier", async () => {
await enableCases();
const company = await seedCompany("REF");
const http = request(app(boardActor));
const created = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "blog_post", key: "launch", title: "Launch post" })
.expect(201);
const byIdentifier = await http.get(`/api/cases/${created.body.identifier}`).expect(200);
expect(byIdentifier.body.id).toBe(created.body.id);
expect(byIdentifier.body.identifier).toMatch(/^REF[A-Z0-9]{4}-C1$/);
});
it("auto-links run writes to their issue with a work link and event", async () => {
await enableCases();
const company = await seedCompany("RUN");
const agent = await seedAgent(company.id);
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
id: runId,
companyId: company.id,
agentId: agent.id,
status: "running",
});
const [issue] = await db.insert(issues).values({
companyId: company.id,
title: "Source task",
status: "in_progress",
executionRunId: runId,
}).returning();
const created = await request(app(boardActor))
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "bug", title: "Bug" })
.expect(201);
const agentActor: Express.Request["actor"] = {
type: "agent",
companyId: company.id,
agentId: agent.id,
runId,
source: "agent_jwt",
onBehalfOfUserId: null,
onBehalfOfMemberships: [],
};
await request(app(agentActor))
.patch(`/api/cases/${created.body.id}`)
.send({ fields: { rootCause: "missing coverage" } })
.expect(200);
const links = await db.select().from(caseIssueLinks);
expect(links).toHaveLength(1);
expect(links[0]!.caseId).toBe(created.body.id);
expect(links[0]!.issueId).toBe(issue!.id);
expect(links[0]!.role).toBe("work");
expect(links[0]!.createdByRunId).toBe(runId);
const linkedEvents = await db.select().from(caseEvents).where(eq(caseEvents.kind, "issue_linked"));
expect(linkedEvents).toHaveLength(1);
expect(linkedEvents[0]!.actorAgentId).toBe(agent.id);
expect(linkedEvents[0]!.runId).toBe(runId);
expect(linkedEvents[0]!.payload).toMatchObject({ issueId: issue!.id, role: "work", autoLinked: true });
});
it("lets a run-scoped agent JWT complete the case happy path without manual linking", async () => {
await enableCases();
const company = await seedCompany("JWT");
const agent = await seedAgent(company.id);
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
id: runId,
companyId: company.id,
agentId: agent.id,
status: "running",
});
const [issue] = await db.insert(issues).values({
companyId: company.id,
title: "Agent case source",
status: "in_progress",
executionRunId: runId,
}).returning();
const token = createLocalAgentJwt(agent.id, company.id, agent.adapterType, runId);
expect(token).toBeTruthy();
const http = request(authenticatedApp());
const createResponse = await http
.post(`/api/companies/${company.id}/cases`)
.set("Authorization", `Bearer ${token}`)
.set("X-Paperclip-Run-Id", runId)
.send({
caseType: "blog_post",
key: "launch-post",
title: "Launch post",
fields: { slug: "launch-post", target_audience: "operators" },
})
.expect(201);
const caseId = createResponse.body.id as string;
await http
.put(`/api/cases/${createResponse.body.identifier}/documents/body`)
.set("Authorization", `Bearer ${token}`)
.set("X-Paperclip-Run-Id", runId)
.send({ body: "# Launch\n\nDraft body." })
.expect(200);
await http
.patch(`/api/cases/${caseId}`)
.set("Authorization", `Bearer ${token}`)
.set("X-Paperclip-Run-Id", runId)
.send({
status: "in_review",
fields: { slug: "launch-post", target_audience: "operators", publish_url: "https://example.com/launch" },
})
.expect(200);
await http
.post(`/api/cases/${caseId}/attachments`)
.set("Authorization", `Bearer ${token}`)
.set("X-Paperclip-Run-Id", runId)
.attach("file", Buffer.from("asset"), "asset.txt")
.expect(201);
const links = await db.select().from(caseIssueLinks);
expect(links).toHaveLength(1);
expect(links[0]).toMatchObject({
companyId: company.id,
caseId,
issueId: issue!.id,
role: "origin",
createdByRunId: runId,
});
const detail = await http
.get(`/api/cases/${createResponse.body.identifier}`)
.set("Authorization", `Bearer ${token}`)
.set("X-Paperclip-Run-Id", runId)
.expect(200);
expect(detail.body.status).toBe("in_review");
expect(detail.body.documents).toHaveLength(1);
expect(detail.body.attachments).toHaveLength(1);
expect(detail.body.issueLinks).toHaveLength(1);
const eventRows = await db.select().from(caseEvents);
expect(eventRows.map((event) => event.kind)).toEqual(expect.arrayContaining([
"created",
"issue_linked",
"document_revised",
"status_changed",
"attachment_added",
]));
expect(eventRows.filter((event) => event.runId === runId)).toHaveLength(eventRows.length);
});
it("holds direct case status writes to the terminal handoff's primary-task authority", async () => {
await enableCases();
const company = await seedCompany("TRM");
const owner = await seedAgent(company.id);
const other = await seedAgent(company.id);
const [primary] = await db.insert(issues).values({
companyId: company.id,
identifier: `${company.issuePrefix.toUpperCase()}-1`,
title: "Primary case task",
status: "done",
assigneeAgentId: owner.id,
}).returning();
const [caseRow] = await db.insert(cases).values({
companyId: company.id,
caseNumber: 1,
identifier: `${company.issuePrefix.toUpperCase()}-C1`,
caseType: "customer",
key: "accepted-fix",
title: "Accepted fix",
status: "in_review",
workIssueId: primary!.id,
}).returning();
const agentActor = (agentId: string): Express.Request["actor"] => ({
type: "agent",
companyId: company.id,
agentId,
source: "agent_key",
keyId: `key-${agentId}`,
onBehalfOfUserId: null,
onBehalfOfMemberships: [],
});
const otherHttp = request(app(agentActor(other.id)));
const readCase = async () => (await db.select().from(cases).where(eq(cases.id, caseRow!.id)))[0]!;
for (const status of ["done", "cancelled"] as const) {
const denied = await otherHttp.patch(`/api/cases/${caseRow!.id}`).send({ status }).expect(403);
expect(denied.body.error).toBe(`Agents may complete only their own case work. ${owner.name} (agent ${owner.id}) owns primary task ${primary!.identifier} and records the terminal handoff. Ask that agent on ${primary!.identifier} to close the case.`);
}
const upsert = await otherHttp
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "customer", key: "accepted-fix", title: "Accepted fix", status: "done" })
.expect(403);
expect(upsert.body.error).toMatch(/^Agents may complete only their own case work\. /);
expect(await readCase()).toMatchObject({ status: "in_review", completedAt: null });
// Fact edits and non-terminal status remain ordinary case writes.
await otherHttp.patch(`/api/cases/${caseRow!.id}`).send({ title: "Accepted checkout fix" }).expect(200);
await otherHttp.patch(`/api/cases/${caseRow!.id}`).send({ status: "in_progress" }).expect(200);
// The owner still closes through the terminal handoff, not a raw status write.
const ownerHttp = request(app(agentActor(owner.id)));
await ownerHttp.patch(`/api/cases/${caseRow!.id}`).send({ status: "done" }).expect(422);
expect(await readCase()).toMatchObject({ status: "in_progress", completedAt: null });
await ownerHttp.patch(`/api/cases/${caseRow!.id}`).send({ status: "cancelled" }).expect(200);
const reopen = await otherHttp.patch(`/api/cases/${caseRow!.id}`).send({ status: "in_progress" }).expect(403);
expect(reopen.body.error).toMatch(/^Agents may complete only their own case work\. /);
await request(app(boardActor)).patch(`/api/cases/${caseRow!.id}`).send({ status: "done" }).expect(200);
expect(await readCase()).toMatchObject({ status: "done", completedAt: expect.any(Date) });
// An upsert without status keeps the locked row's status and completion.
await otherHttp
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "customer", key: "accepted-fix", title: "Accepted checkout fix" })
.expect(200);
expect(await readCase()).toMatchObject({ status: "done", completedAt: expect.any(Date) });
});
it("rejects cross-company agent access across the cases route surface", async () => {
await enableCases();
const ownCompany = await seedCompany("OWN");
const otherCompany = await seedCompany("OTH");
const agent = await seedAgent(ownCompany.id);
const [otherIssue] = await db.insert(issues).values({
companyId: otherCompany.id,
identifier: `${otherCompany.issuePrefix.toUpperCase()}-1`,
title: "Other company task",
status: "todo",
}).returning();
const [caseRow] = await db.insert(cases).values({
companyId: otherCompany.id,
caseNumber: 1,
identifier: `${otherCompany.issuePrefix.toUpperCase()}-C1`,
caseType: "bug",
title: "Other company case",
}).returning();
const [ownCase] = await db.insert(cases).values({
companyId: ownCompany.id,
caseNumber: 1,
identifier: `${ownCompany.issuePrefix.toUpperCase()}-C1`,
caseType: "bug",
title: "Own company case",
}).returning();
await db.insert(caseEvents).values({
companyId: otherCompany.id,
caseId: caseRow!.id,
kind: "created",
actorType: "system",
payload: {},
});
const agentActor: Express.Request["actor"] = {
type: "agent",
companyId: ownCompany.id,
agentId: agent.id,
source: "agent_key",
keyId: "key-1",
onBehalfOfUserId: "user-1",
onBehalfOfMemberships: [],
};
const http = request(app(agentActor));
await http.get(`/api/companies/${otherCompany.id}/cases`).expect(403);
await http
.post(`/api/companies/${otherCompany.id}/cases`)
.send({ caseType: "bug", title: "Wrong company create" })
.expect(403);
await http.get(`/api/cases/${caseRow!.id}`).expect(404);
await http.get(`/api/cases/${caseRow!.identifier}`).expect(404);
await http.patch(`/api/cases/${caseRow!.id}`).send({ status: "in_progress" }).expect(404);
await http.put(`/api/cases/${caseRow!.id}/documents/body`).send({ body: "Body" }).expect(404);
await http
.post(`/api/cases/${caseRow!.id}/links`)
.send({ issueId: otherIssue!.id, role: "reference" })
.expect(404);
await http
.post(`/api/cases/${caseRow!.id}/attachments`)
.attach("file", Buffer.from("artifact"), "artifact.txt")
.expect(404);
await http.get(`/api/cases/${caseRow!.id}/events`).expect(404);
await http.get(`/api/issues/${otherIssue!.id}/cases`).expect(404);
await http.get(`/api/issues/${otherIssue!.identifier}/cases`).expect(404);
const limitedBoardActor: Express.Request["actor"] = {
type: "board",
userId: "limited-board-user",
source: "session",
isInstanceAdmin: false,
companyIds: [ownCompany.id],
memberships: [{ companyId: ownCompany.id, membershipRole: "operator", status: "active" }],
};
await db.insert(companyMemberships).values({ companyId: ownCompany.id, principalType: "user", principalId: "limited-board-user", status: "active", membershipRole: "operator" });
const limitedBoardHttp = request(app(limitedBoardActor));
const ownCaseResponse = await limitedBoardHttp.get(`/api/cases/${ownCase!.id}`).expect(200);
expect(ownCaseResponse.body.id).toBe(ownCase!.id);
await limitedBoardHttp.get(`/api/cases/${caseRow!.id}`).expect(404);
await limitedBoardHttp.get(`/api/cases/${caseRow!.identifier}`).expect(404);
await limitedBoardHttp.get(`/api/issues/${otherIssue!.id}/cases`).expect(404);
await limitedBoardHttp.get(`/api/issues/${otherIssue!.identifier}/cases`).expect(404);
const scopedAdminHttp = request(app({ ...limitedBoardActor, isInstanceAdmin: true }));
await scopedAdminHttp.get(`/api/cases/${caseRow!.id}`).expect(404);
await scopedAdminHttp.get(`/api/cases/${caseRow!.identifier}`).expect(404);
await scopedAdminHttp.get(`/api/issues/${otherIssue!.id}/cases`).expect(404);
await scopedAdminHttp.get(`/api/issues/${otherIssue!.identifier}/cases`).expect(404);
expect(await db.select().from(cases)).toHaveLength(2);
expect(await db.select().from(caseDocuments)).toHaveLength(0);
expect(await db.select().from(documents)).toHaveLength(0);
expect(await db.select().from(caseIssueLinks)).toHaveLength(0);
expect(await db.select().from(caseAttachments)).toHaveLength(0);
expect(await db.select().from(assets)).toHaveLength(0);
expect(await db.select().from(caseEvents)).toHaveLength(1);
});
it("returns identical cross-company and absent case handoff responses without mutating passive cases", async () => {
await enableCases();
const own = await seedCompany("HANDOFF-OWN"), foreign = await seedCompany("HANDOFF-FOREIGN");
const agent = await seedAgent(own.id);
const [ownCase, foreignCase] = await db.insert(cases).values([
{ companyId: own.id, caseNumber: 1, identifier: `${own.issuePrefix}-C1`, caseType: "work", title: "Own passive case" },
{ companyId: foreign.id, caseNumber: 1, identifier: `${foreign.issuePrefix}-C1`, caseType: "work", title: "Foreign confidential case", fields: { privateFact: "Keep this private" } },
]).returning();
await db.insert(companyMemberships).values({ companyId: own.id, principalType: "user", principalId: "handoff-operator", status: "active", membershipRole: "operator" });
const scopedBoard: Express.Request["actor"] = { type: "board", source: "session", userId: "handoff-operator",
companyIds: [own.id], isInstanceAdmin: false,
memberships: [{ companyId: own.id, membershipRole: "operator", status: "active" }] };
const actors: Express.Request["actor"][] = [
{ type: "agent", companyId: own.id, agentId: agent.id, source: "agent_key" },
scopedBoard, { ...scopedBoard, isInstanceAdmin: true },
];
// An authorized passive case has an empty history, proving the real router
// is mounted rather than accepting Express's unmatched-route 404.
await request(app(boardActor)).get(`/api/cases/${ownCase!.id}/handoffs`).expect(200);
const absentCaseId = randomUUID(), handoffId = randomUUID();
for (const actor of actors) {
const http = request(app(actor));
const presentHistory = await http.get(`/api/cases/${foreignCase!.id}/handoffs`).expect(404);
const absentHistory = await http.get(`/api/cases/${absentCaseId}/handoffs`).expect(404);
expect(presentHistory.body).toEqual(absentHistory.body);
for (const action of ["retry", "cancel"]) {
const presentControl = await http.post(`/api/cases/${foreignCase!.id}/handoffs/${handoffId}/${action}`).send({}).expect(404);
const absentControl = await http.post(`/api/cases/${absentCaseId}/handoffs/${handoffId}/${action}`).send({}).expect(404);
expect(presentControl.body).toEqual(absentControl.body);
}
}
expect(await db.select().from(cases).where(eq(cases.id, foreignCase!.id))).toEqual([foreignCase]);
expect(await db.select().from(caseHandoffs)).toHaveLength(0);
expect(await db.select().from(caseEvents)).toHaveLength(0);
expect(await db.select().from(issues)).toHaveLength(0);
expect(await db.select().from(heartbeatRuns)).toHaveLength(0);
expect(await db.select().from(activityLog)).toHaveLength(0);
});
it("supports documents, manual issue links, attachment links, events, and list filters", async () => {
await enableCases();
const company = await seedCompany("SUR");
const [label] = await db.insert(labels).values({
companyId: company.id,
name: "Needs Review",
color: "#f59e0b",
}).returning();
const [issue] = await db.insert(issues).values({
companyId: company.id,
identifier: `${company.issuePrefix.toUpperCase()}-12`,
title: "Related task",
status: "todo",
}).returning();
const http = request(app(boardActor));
const created = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "incident", title: "Production incident", status: "in_progress" })
.expect(201);
await http.patch(`/api/cases/${created.body.id}`).send({ labels: [label!.id] }).expect(200);
await http.put(`/api/cases/${created.body.identifier}/documents/runbook`).send({ body: "Steps" }).expect(200);
await http.post(`/api/cases/${created.body.id}/links`).send({ issueId: issue!.id, role: "reference" }).expect(201);
await http.post(`/api/cases/${created.body.id}/attachments`).attach("file", Buffer.from("artifact"), "artifact.txt").expect(201);
const activeList = await http
.get(`/api/companies/${company.id}/cases`)
.query({ status: "active", label: label!.id, q: "Production" })
.expect(200);
expect(activeList.body).toHaveLength(1);
expect(activeList.body[0].id).toBe(created.body.id);
const [project] = await db.insert(projects).values({ companyId: company.id, name: "Launch" }).returning();
const [projectCase] = await db.insert(cases).values({
companyId: company.id,
projectId: project!.id,
caseNumber: 50,
identifier: `${company.issuePrefix.toUpperCase()}-C50`,
caseType: "brief",
title: "Project brief",
status: "draft",
}).returning();
const multiFiltered = await http
.get(`/api/companies/${company.id}/cases`)
.query({ types: ["incident", "brief"], statuses: ["in_progress", "draft"], projectIds: [project!.id], includeNoProject: "true" })
.expect(200);
expect(multiFiltered.body.map((row: { id: string }) => row.id).sort()).toEqual([created.body.id, projectCase!.id].sort());
await db.insert(cases).values(Array.from({ length: 205 }, (_, index) => ({
companyId: company.id,
caseNumber: 100 + index,
identifier: `${company.issuePrefix.toUpperCase()}-C${100 + index}`,
caseType: "incident",
title: `Filler incident ${index}`,
status: "in_progress",
updatedAt: new Date(`2030-01-01T00:${String(index % 60).padStart(2, "0")}:00.000Z`),
})));
const deepFiltered = await http
.get(`/api/companies/${company.id}/cases`)
.query({ q: "Production", limit: 1 })
.expect(200);
expect(deepFiltered.body).toHaveLength(1);
expect(deepFiltered.body[0].id).toBe(created.body.id);
const detail = await http.get(`/api/cases/${created.body.identifier}`).expect(200);
expect(detail.body.labels).toHaveLength(1);
expect(detail.body.documents).toHaveLength(1);
expect(detail.body.issueLinks).toHaveLength(1);
expect(detail.body.attachments).toHaveLength(1);
const events = await http.get(`/api/cases/${created.body.id}/events`).expect(200);
expect(events.body.map((event: { kind: string }) => event.kind)).toEqual(
expect.arrayContaining(["created", "label_added", "document_revised", "issue_linked", "attachment_added"]),
);
const linkedEvent = events.body.find((event: { kind: string }) => event.kind === "issue_linked");
expect(linkedEvent.issue).toMatchObject({
id: issue!.id,
identifier: issue!.identifier,
title: "Related task",
status: "todo",
});
});
it("enriches events and revisions with actor name and run→issue attribution", async () => {
await enableCases();
const company = await seedCompany("ATT");
const agent = await seedAgent(company.id);
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
id: runId,
companyId: company.id,
agentId: agent.id,
status: "running",
});
const [issue] = await db.insert(issues).values({
companyId: company.id,
title: "Attribution source task",
status: "in_progress",
executionRunId: runId,
}).returning();
const agentActor: Express.Request["actor"] = {
type: "agent",
companyId: company.id,
agentId: agent.id,
runId,
source: "agent_jwt",
onBehalfOfUserId: null,
onBehalfOfMemberships: [],
};
const http = request(app(agentActor));
const created = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "blog_post", title: "Attribution post" })
.expect(201);
// Two revisions on the body document.
const rev1 = await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({ body: "# v1" })
.expect(200);
await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({ body: "# v2", baseRevisionId: rev1.body.revision.id, changeSummary: "polish" })
.expect(200);
const events = await http.get(`/api/cases/${created.body.id}/events`).expect(200);
const revisedEvent = events.body.find((e: { kind: string }) => e.kind === "document_revised");
expect(revisedEvent.actorAgentName).toBe("Cases Agent");
expect(revisedEvent.issue).toMatchObject({ id: issue!.id, title: "Attribution source task" });
const revisions = await http
.get(`/api/cases/${created.body.id}/documents/body/revisions`)
.expect(200);
expect(revisions.body.revisions).toHaveLength(2);
expect(revisions.body.revisions[0].revisionNumber).toBe(2);
expect(revisions.body.revisions[0].body).toBe("# v2");
expect(revisions.body.revisions[0].changeSummary).toBe("polish");
expect(revisions.body.revisions[0].actorAgentName).toBe("Cases Agent");
expect(revisions.body.revisions[0].issue).toMatchObject({ id: issue!.id });
const activity = await db.select().from(activityLog).where(eq(activityLog.entityId, created.body.id));
const documentActivity = activity.filter(row => row.action === "case.document_revised");
expect(documentActivity).toHaveLength(2);
expect(documentActivity).toEqual(expect.arrayContaining([expect.objectContaining({
actorType: "agent", actorId: agent.id, agentId: agent.id, runId,
details: expect.objectContaining({ key: "body", documentKey: "body", documentId: rev1.body.document.id }),
})]));
});
it("locks, unlocks, deletes, and restores case documents through shared document controls", async () => {
await enableCases();
const company = await seedCompany("DOC");
const http = request(app(boardActor));
const created = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "blog_post", title: "Document controls" })
.expect(201);
const publishedRevisions: Record<string, unknown>[] = [];
liveEventUnsubscribers.push(subscribeCompanyLiveEvents(company.id, event => {
if (event.payload.action === "case.document_revised") publishedRevisions.push(event.payload);
}));
const firstRevision = await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({ body: "# v1" })
.expect(200);
const secondRevision = await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({ body: "# v2", baseRevisionId: firstRevision.body.revision.id })
.expect(200);
const loaded = await http.get(`/api/cases/${created.body.id}/documents/body`).expect(200);
expect(loaded.body.body).toBe("# v2");
expect(loaded.body.latestRevisionNumber).toBe(2);
const locked = await http.post(`/api/cases/${created.body.id}/documents/body/lock`).expect(200);
expect(locked.body.lockedAt).toBeTruthy();
await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({ body: "# blocked", baseRevisionId: secondRevision.body.revision.id })
.expect(409);
await http.delete(`/api/cases/${created.body.id}/documents/body`).expect(409);
const unlocked = await http.post(`/api/cases/${created.body.id}/documents/body/unlock`).expect(200);
expect(unlocked.body.lockedAt).toBeNull();
const restored = await http
.post(`/api/cases/${created.body.id}/documents/body/revisions/${firstRevision.body.revision.id}/restore`)
.expect(200);
expect(restored.body.document.body).toBe("# v1");
expect(restored.body.document.latestRevisionNumber).toBe(3);
expect(restored.body.restoredFromRevisionNumber).toBe(1);
const revisions = await http.get(`/api/cases/${created.body.id}/documents/body/revisions`).expect(200);
expect(revisions.body.revisions).toHaveLength(3);
expect(revisions.body.revisions[0].changeSummary).toBe("Restored from revision 1");
const activity = await db.select().from(activityLog).where(eq(activityLog.entityId, created.body.id));
const documentActivity = activity.filter(row => row.action === "case.document_revised");
expect(documentActivity).toHaveLength(3);
expect(publishedRevisions).toHaveLength(3);
const revisionsByNumber = new Map([firstRevision, secondRevision, restored].map((response, index) => [index + 1, response.body.revision.id]));
for (const row of documentActivity) {
const details = row.details as Record<string, unknown>;
expect(row).toMatchObject({ companyId: company.id, entityType: "case", entityId: created.body.id, actorType: "user", actorId: "board-user" });
expect(details).toMatchObject({ key: "body", documentKey: "body", documentId: firstRevision.body.document.id,
revisionId: revisionsByNumber.get(details.revisionNumber as number) });
expect(details).not.toHaveProperty("body");
expect(details).not.toHaveProperty("title");
expect(publishedRevisions).toContainEqual(expect.objectContaining({ entityType: "case", entityId: created.body.id,
action: "case.document_revised", details }));
}
expect(documentActivity.find(row => (row.details as Record<string, unknown>).revisionNumber === 3)?.details).toMatchObject({
restoredFromRevisionId: firstRevision.body.revision.id, restoredFromRevisionNumber: 1,
});
expect(JSON.stringify(publishedRevisions)).not.toMatch(/# v1|# v2|# blocked/);
await http.delete(`/api/cases/${created.body.id}/documents/body`).expect(200);
await http.get(`/api/cases/${created.body.id}`).expect(200).expect((res) => {
expect(res.body.documents).toHaveLength(0);
});
});
it("creates, replies to, resolves, reopens, and remaps case document annotations", async () => {
await enableCases();
const company = await seedCompany("ANN");
const http = request(app(boardActor));
const created = await http
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "brief", title: "Annotated case" })
.expect(201);
const document = await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({ body: "Alpha beta gamma" })
.expect(200);
const annotation = await http
.post(`/api/cases/${created.body.id}/documents/body/annotations`)
.send({
baseRevisionId: document.body.revision.id,
baseRevisionNumber: document.body.revision.revisionNumber,
selector: {
quote: { exact: "beta", prefix: "Alpha ", suffix: " gamma" },
position: { normalizedStart: 6, normalizedEnd: 10, markdownStart: 6, markdownEnd: 10 },
},
body: "Clarify this word.",
})
.expect(201);
expect(annotation.body.caseId).toBe(created.body.id);
expect(annotation.body.issueId).toBeNull();
expect(annotation.body.routineId).toBeNull();
expect(annotation.body.comments[0].caseId).toBe(created.body.id);
const listed = await http
.get(`/api/cases/${created.body.identifier}/documents/body/annotations?status=all&includeComments=true`)
.expect(200);
expect(listed.body).toHaveLength(1);
expect(listed.body[0].comments).toHaveLength(1);
const reply = await http
.post(`/api/cases/${created.body.id}/documents/body/annotations/${annotation.body.id}/comments`)
.send({ body: "Added context." })
.expect(201);
expect(reply.body.caseId).toBe(created.body.id);
const resolved = await http
.patch(`/api/cases/${created.body.id}/documents/body/annotations/${annotation.body.id}`)
.send({ status: "resolved" })
.expect(200);
expect(resolved.body.status).toBe("resolved");
const reopened = await http
.patch(`/api/cases/${created.body.id}/documents/body/annotations/${annotation.body.id}`)
.send({ status: "open" })
.expect(200);
expect(reopened.body.status).toBe("open");
const updatedDocument = await http
.put(`/api/cases/${created.body.id}/documents/body`)
.send({
body: "Alpha beta gamma delta",
baseRevisionId: document.body.revision.id,
})
.expect(200);
const remapped = await http
.get(`/api/cases/${created.body.id}/documents/body/annotations/${annotation.body.id}`)
.expect(200);
expect(remapped.body.currentRevisionNumber).toBe(updatedDocument.body.revision.revisionNumber);
expect(remapped.body.comments).toHaveLength(2);
const activities = await db
.select({ action: activityLog.action, entityType: activityLog.entityType, entityId: activityLog.entityId })
.from(activityLog)
.where(eq(activityLog.entityId, created.body.id));
expect(activities).toEqual(expect.arrayContaining([
expect.objectContaining({ entityType: "case", action: "case.document_annotation_thread_created" }),
expect.objectContaining({ entityType: "case", action: "case.document_annotation_comment_added" }),
expect.objectContaining({ entityType: "case", action: "case.document_annotation_thread_resolved" }),
expect.objectContaining({ entityType: "case", action: "case.document_annotation_thread_reopened" }),
expect.objectContaining({ entityType: "case", action: "case.document_annotation_remapped" }),
]));
});
it("lists children by parent, exposes parent in detail, and lists cases for an issue", async () => {
await enableCases();
const company = await seedCompany("TREE");
const boardHttp = request(app(boardActor));
const parent = await boardHttp
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "epic", title: "Parent epic" })
.expect(201);
const child = await boardHttp
.post(`/api/companies/${company.id}/cases`)
.send({ caseType: "task", title: "Child task", parentCaseId: parent.body.id })
.expect(201);
const children = await boardHttp
.get(`/api/companies/${company.id}/cases`)
.query({ parent: parent.body.id })
.expect(200);
expect(children.body).toHaveLength(1);
expect(children.body[0].id).toBe(child.body.id);
const searchOnly = await boardHttp
.get(`/api/companies/${company.id}/cases`)
.query({ q: "Child task" })
.expect(200);
expect(searchOnly.body.map((row: { id: string }) => row.id)).toEqual([child.body.id]);
const searchWithAncestors = await boardHttp
.get(`/api/companies/${company.id}/cases`)
.query({ q: "Child task", includeAncestors: "true" })
.expect(200);
expect(searchWithAncestors.body).toEqual(expect.arrayContaining([
expect.objectContaining({ id: child.body.id, matchesListFilters: true }),
expect.objectContaining({ id: parent.body.id, matchesListFilters: false }),
]));
const childDetail = await boardHttp.get(`/api/cases/${child.body.id}`).expect(200);
expect(childDetail.body.parent).toMatchObject({ id: parent.body.id, identifier: parent.body.identifier });
// Link the child case to an issue, then resolve cases-for-issue.
const [issue] = await db.insert(issues).values({
companyId: company.id,
title: "Issue with cases",
status: "todo",
}).returning();
await boardHttp
.post(`/api/cases/${child.body.id}/links`)
.send({ issueId: issue!.id, role: "work" })
.expect(201);
const forIssue = await boardHttp.get(`/api/issues/${issue!.id}/cases`).expect(200);
expect(forIssue.body).toHaveLength(1);
expect(forIssue.body[0]).toMatchObject({ role: "work" });
expect(forIssue.body[0].case).toMatchObject({ id: child.body.id, identifier: child.body.identifier, status: child.body.status });
});
it("carries glyph-only blocker state on a blocked case task without blocker details (BUG-07)", async () => {
await enableCases();
const company = await seedCompany("WORK-BLOCKED");
const [work, blocker] = await db.insert(issues).values([
{ companyId: company.id, title: "Blocked case work", identifier: `${company.issuePrefix}-1`, status: "blocked" },
{ companyId: company.id, title: "Hidden case blocker", identifier: `${company.issuePrefix}-2`, status: "todo" },
]).returning();
await db.insert(issueRelations).values({ companyId: company.id, issueId: blocker!.id, relatedIssueId: work!.id, type: "blocks" });
const [record] = await db.insert(cases).values({ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`,
caseType: "factory", title: "Blocked case", workIssueId: work!.id }).returning();
const detail = await request(app(boardActor)).get(`/api/cases/${record!.id}`).expect(200);
expect(detail.body.workIssue).toMatchObject({ id: work!.id, status: "blocked", blockerAttention: { state: "needs_attention", sampleBlockerIdentifier: null } });
expect(JSON.stringify(detail.body.workIssue)).not.toContain("Hidden case blocker");
expect(JSON.stringify(detail.body.workIssue)).not.toContain(blocker!.id);
// Related-task links and the activity feed row draw from the same state.
await db.insert(caseIssueLinks).values({ companyId: company.id, caseId: record!.id, issueId: work!.id, role: "reference" });
const withLink = await request(app(boardActor)).get(`/api/cases/${record!.id}`).expect(200);
expect(withLink.body.issueLinks[0].issue).toMatchObject({ id: work!.id, blockerAttention: { state: "needs_attention", sampleBlockerIdentifier: null } });
await db.insert(caseEvents).values({ companyId: company.id, caseId: record!.id, kind: "issue_linked",
actorType: "system", payload: { issueId: work!.id } });
const events = await request(app(boardActor)).get(`/api/cases/${record!.id}/events`).expect(200);
const linked = events.body.find((event: { kind: string }) => event.kind === "issue_linked");
expect(linked.issue).toMatchObject({ id: work!.id, status: "blocked", blockerAttention: { state: "needs_attention", sampleBlockerIdentifier: null } });
expect(JSON.stringify(linked.issue)).not.toContain("Hidden case blocker");
});
it("enriches case pages and detail with current work in bounded batch queries", async () => {
await enableCases();
const company = await seedCompany("WORK-LIST");
const sender = await seedAgent(company.id);
const recipient = await seedAgent(company.id);
await db.update(agents).set({ name: "Triage" }).where(eq(agents.id, sender.id));
await db.update(agents).set({ name: "Verifier" }).where(eq(agents.id, recipient.id));
const work = await db.insert(issues).values(Array.from({ length: 20 }, (_, index) => ({
companyId: company.id, title: `Task ${index}`, identifier: `${company.issuePrefix}-${index + 1}`,
assigneeAgentId: sender.id, status: "todo",
}))).returning();
const records = await db.insert(cases).values(work.map((issue, index) => ({
companyId: company.id, caseNumber: index + 1, identifier: `${company.issuePrefix}-C${index + 1}`,
caseType: "factory", title: `Case ${index}`, workIssueId: issue.id, phase: "triage", handoffVersion: 1,
}))).returning();
const pending = await db.insert(caseHandoffs).values(records.map((record, index) => ({
companyId: company.id, caseId: record.id, issueId: work[index]!.id, version: 1,
fromAgentId: sender.id, toAgentId: recipient.id, phase: "verification", instructions: "Check the saved work.",
actorType: "user" as const, actorId: "board-user", wakeupRequestId: randomUUID(),
actorContext: { schema: "paperclip.case-handoff-actor.v1", companyId: company.id, actorType: "user", actorId: "board-user",
source: "session", runId: null, keyId: null, sessionId: "INTERNAL_SESSION_CANARY", keyScope: { kind: "standard" },
onBehalfOfUserId: null, authorizingUserId: null },
idempotencyKey: randomUUID(), inputFingerprint: "a".repeat(64), state: "pending_dispatch" as const,
}))).returning();
const http = request(app(boardActor));
const select = vi.spyOn(db, "select");
try {
await http.get(`/api/companies/${company.id}/cases?limit=1`).expect(200);
const singleSelects = select.mock.calls.length;
select.mockClear();
const page = await http.get(`/api/companies/${company.id}/cases?limit=20`).expect(200);
expect(page.body).toHaveLength(20);
// These are actual Drizzle reads against PostgreSQL, observed without
// replacing their results. A task/handoff read per row fails this bound.
expect(select.mock.calls.length).toBeLessThanOrEqual(singleSelects + 1);
const first = page.body.find((row: { id: string }) => row.id === records[0]!.id);
expect(first).toMatchObject({ phase: "triage", handoffVersion: 1,
workIssue: { id: work[0]!.id, assigneeAgentId: sender.id, assigneeAgentName: "Triage", status: "todo" },
pendingHandoff: { id: pending[0]!.id, phase: "verification", state: "pending_dispatch", fromAgentName: "Triage", toAgentName: "Verifier" },
});
const detail = await http.get(`/api/cases/${records[0]!.id}`).expect(200);
expect(detail.body.workIssue).toEqual(first.workIssue);
expect(detail.body.pendingHandoff).toEqual(first.pendingHandoff);
for (const row of page.body) expect(row.pendingHandoff).not.toHaveProperty("actorContext");
expect(detail.body.pendingHandoff).not.toHaveProperty("actorContext");
expect(JSON.stringify([page.body, detail.body])).not.toContain("INTERNAL_SESSION_CANARY");
} finally {
select.mockRestore();
}
});
it("paginates authorized filtered cases and discovers types outside the current page", async () => {
await enableCases();
const company = await seedCompany("LIST-PAGE");
const owner = await seedAgent(company.id);
const reader = await seedAgent(company.id);
const [project] = await db.insert(projects).values({ companyId: company.id, name: "Project" }).returning();
const [privateSource] = await db.insert(issues).values({ companyId: company.id, title: "Private", visibility: "private", assigneeAgentId: owner.id }).returning();
const [hidden, first, second] = await db.insert(cases).values([
{ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, title: "Needle hidden", caseType: "factory", status: "in_progress", updatedAt: new Date("2030-01-04") },
{ companyId: company.id, caseNumber: 2, identifier: `${company.issuePrefix}-C2`, title: "Needle first", caseType: "factory", status: "in_progress", updatedAt: new Date("2030-01-03") },
{ companyId: company.id, caseNumber: 3, identifier: `${company.issuePrefix}-C3`, title: "Needle second", caseType: "factory", status: "in_progress", updatedAt: new Date("2030-01-02") },
{ companyId: company.id, caseNumber: 4, identifier: `${company.issuePrefix}-C4`, title: "Unrelated draft", caseType: "research", status: "draft" },
{ companyId: company.id, caseNumber: 5, identifier: `${company.issuePrefix}-C5`, title: "Needle project", caseType: "factory", status: "in_progress", projectId: project!.id, updatedAt: new Date("2030-01-05") },
]).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: hidden!.id, sourceKind: "issue", sourceId: privateSource!.id });
const [secretType] = await db.insert(cases).values({ companyId: company.id, caseNumber: 6,
identifier: `${company.issuePrefix}-C6`, title: "Private type", caseType: "secret-type" }).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: secretType!.id, sourceKind: "issue", sourceId: privateSource!.id });
const http = request(app({ type: "agent", companyId: company.id, agentId: reader.id, source: "agent_key" }));
const query = { q: "Needle", type: "factory", status: "in_progress", includeNoProject: "true", limit: 1 };
const firstPage = await http.get(`/api/companies/${company.id}/cases`).query(query).expect(200);
const secondPage = await http.get(`/api/companies/${company.id}/cases`).query({ ...query, offset: 1 }).expect(200);
expect(firstPage.body.map((row: { id: string }) => row.id)).toEqual([first!.id]);
expect(secondPage.body.map((row: { id: string }) => row.id)).toEqual([second!.id]);
await http.get(`/api/companies/${company.id}/cases`).query({ offset: -1 }).expect(400);
const types = await http.get(`/api/companies/${company.id}/cases/types`).expect(200);
expect(types.body).toEqual(["factory", "research"]);
await db.delete(issues).where(eq(issues.id, privateSource!.id));
expect((await http.get(`/api/companies/${company.id}/cases/types`).expect(200)).body).toEqual(types.body);
});
it("sorts and groups the full authorized result before pagination", async () => {
await enableCases();
const company = await seedCompany("LIST-ORDER");
const owner = await seedAgent(company.id);
const reader = await seedAgent(company.id);
const projectRows = await db.insert(projects).values([
{ companyId: company.id, name: "One" },
{ companyId: company.id, name: "Two" },
]).returning();
const [privateSource] = await db.insert(issues).values({
companyId: company.id, title: "Private", visibility: "private", assigneeAgentId: owner.id,
}).returning();
const statuses = ["draft", "in_progress", "in_review", "approved", "done", "cancelled"];
const types = ["factory", "incident", "research"];
const records = await db.insert(cases).values(Array.from({ length: 207 }, (_, index) => ({
companyId: company.id, caseNumber: index + 1, identifier: `${company.issuePrefix}-C${index + 1}`,
title: `${index % 2 ? "Alpha" : "beta"} ${String(207 - index).padStart(3, "0")}`,
caseType: types[index % types.length]!, status: statuses[index % statuses.length]!,
projectId: index % 3 === 2 ? null : projectRows[index % 3]!.id,
createdAt: new Date(Date.UTC(2030, 0, 1, 0, index)),
updatedAt: new Date(Date.UTC(2031, 0, 1, 0, 207 - index)),
}))).returning();
const [hidden] = await db.insert(cases).values({
companyId: company.id, caseNumber: 208, identifier: `${company.issuePrefix}-C208`,
title: "000 confidential", caseType: "a-secret", status: "draft",
updatedAt: new Date("2035-01-01"),
}).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: hidden!.id, sourceKind: "issue", sourceId: privateSource!.id });
const http = request(app({ type: "agent", companyId: company.id, agentId: reader.id, source: "agent_key" }));
type SortField = "updated" | "created" | "title" | "status" | "type";
type GroupBy = "none" | "status" | "type" | "project";
type RecordRow = (typeof records)[number];
const compare = (left: string | number, right: string | number) => left < right ? -1 : left > right ? 1 : 0;
const sortValue = (row: RecordRow, field: SortField) => ({
updated: row.updatedAt.getTime(), created: row.createdAt.getTime(), title: row.title.toLowerCase(),
status: statuses.indexOf(row.status), type: row.caseType,
})[field];
const options: Array<{ sortField?: SortField; sortDir?: "asc" | "desc"; groupBy?: GroupBy }> = [
{}, { sortField: "updated", sortDir: "asc" }, { sortField: "created", sortDir: "asc" },
{ sortField: "title", sortDir: "asc" }, { sortField: "status", sortDir: "desc" },
{ sortField: "type", sortDir: "asc" },
{ groupBy: "status", sortField: "title", sortDir: "asc" },
{ groupBy: "type", sortField: "created", sortDir: "desc" },
{ groupBy: "project", sortField: "updated", sortDir: "asc" },
];
for (const option of options) {
const expected = [...records].sort((left, right) => {
let grouped = 0;
if (option.groupBy === "status") grouped = compare(statuses.indexOf(left.status), statuses.indexOf(right.status));
if (option.groupBy === "type") grouped = compare(left.caseType, right.caseType);
if (option.groupBy === "project") grouped = left.projectId === null ? Number(right.projectId !== null)
: right.projectId === null ? -1 : compare(left.projectId, right.projectId);
return grouped || (option.sortDir === "asc" ? 1 : -1) * compare(sortValue(left, option.sortField ?? "updated"), sortValue(right, option.sortField ?? "updated"))
|| compare(right.updatedAt.getTime(), left.updatedAt.getTime())
|| compare(right.createdAt.getTime(), left.createdAt.getTime()) || compare(right.id, left.id);
});
const first = await http.get(`/api/companies/${company.id}/cases`).query({ ...option, limit: 200 }).expect(200);
const second = await http.get(`/api/companies/${company.id}/cases`).query({ ...option, offset: 200, limit: 200 }).expect(200);
expect([...first.body, ...second.body].map((row: { id: string }) => row.id), JSON.stringify(option)).toEqual(expected.map((row) => row.id));
}
for (const invalid of [{ sortField: "assignee" }, { sortDir: "sideways" }, { groupBy: "private" }]) {
await http.get(`/api/companies/${company.id}/cases`).query(invalid).expect(400);
}
});
it("does not enrich readable cases with inaccessible tasks or out-of-bound agent names", async () => {
await enableCases();
const company = await seedCompany("LIST-BOUNDARY");
const owner = await seedAgent(company.id);
const reader = await seedAgent(company.id);
const rootId = randomUUID();
await db.update(agents).set({ name: "Private owner" }).where(eq(agents.id, owner.id));
await db.update(agents).set({ permissions: { trustPreset: "low_trust_review", authorizationPolicy: {
trustBoundary: { mode: "low_trust_review", rootIssueId: rootId },
} } }).where(eq(agents.id, reader.id));
const [privateWork, allowedWork] = await db.insert(issues).values([
{ companyId: company.id, title: "Private task contents", visibility: "private", assigneeAgentId: owner.id },
{ id: rootId, companyId: company.id, title: "Allowed task", assigneeAgentId: reader.id },
]).returning();
const [malformed, allowed] = await db.insert(cases).values([
{ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, title: "Legacy public record", caseType: "factory", workIssueId: privateWork!.id },
{ companyId: company.id, caseNumber: 2, identifier: `${company.issuePrefix}-C2`, title: "Allowed record", caseType: "factory", workIssueId: allowedWork!.id },
]).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: allowed!.id, sourceKind: "issue", sourceId: allowedWork!.id });
await db.insert(caseHandoffs).values([malformed!, allowed!].map((record, index) => ({
companyId: company.id, caseId: record.id, issueId: record.workIssueId!, version: 1,
fromAgentId: owner.id, toAgentId: reader.id, phase: "verify", instructions: index ? "Allowed instruction" : "Private handoff contents",
actorType: "user" as const, actorId: "board-user", wakeupRequestId: randomUUID(),
idempotencyKey: randomUUID(), inputFingerprint: "a".repeat(64), state: "pending_dispatch" as const,
})));
const ordinary = request(app({ type: "agent", companyId: company.id, agentId: reader.id, source: "agent_key" }));
// A low-trust caller cannot read the unsourced company record at all.
const page = await ordinary.get(`/api/companies/${company.id}/cases`).expect(200);
expect(page.body).toHaveLength(1);
expect(page.body[0]).toMatchObject({ id: allowed!.id, workIssue: { id: allowedWork!.id },
pendingHandoff: { instructions: "Allowed instruction", fromAgentName: null, toAgentName: null } });
expect(JSON.stringify(page.body)).not.toContain("Private owner");
// Standard company access can read a legacy passive case, but its malformed
// primary link must not bypass the private task's independent boundary.
await db.update(agents).set({ permissions: {} }).where(eq(agents.id, reader.id));
const visible = await ordinary.get(`/api/cases/${malformed!.id}`).expect(200);
expect(visible.body.workIssue).toBeNull();
expect(visible.body.pendingHandoff).toBeNull();
expect(JSON.stringify(visible.body)).not.toContain("Private handoff contents");
});
it("filters retained private cases before pagination and protects nested content", async () => {
await enableCases();
const company = await seedCompany("PRIV");
const owner = await seedAgent(company.id);
const unrelated = await seedAgent(company.id);
const [source] = await db.insert(issues).values({ companyId: company.id, title: "Private source", visibility: "private", assigneeAgentId: owner.id }).returning();
const [hidden] = await db.insert(cases).values({ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, caseType: "work", title: "Secret case", updatedAt: new Date("2030-01-01") }).returning();
const [open] = await db.insert(cases).values({ companyId: company.id, caseNumber: 2, identifier: `${company.issuePrefix}-C2`, caseType: "work", title: "Open case" }).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: hidden!.id, sourceKind: "issue", sourceId: source!.id });
const actor = { type: "agent" as const, companyId: company.id, agentId: unrelated.id, source: "agent_key" as const };
const http = request(app(actor));
const list = await http.get(`/api/companies/${company.id}/cases?limit=1`).expect(200);
expect(list.body.map((row: { id: string }) => row.id)).toEqual([open!.id]);
await http.get(`/api/cases/${hidden!.id}`).expect(404);
await http.get(`/api/cases/${hidden!.id}/events`).expect(404);
await http.get(`/api/cases/${hidden!.id}/documents/spec/revisions`).expect(404);
await http.get(`/api/cases/${hidden!.id}/documents/spec/annotations`).expect(404);
await http.patch(`/api/cases/${hidden!.id}`).send({ fields: { stolen: true } }).expect(404);
expect(await canActorReadCase(db, { ...actor, agentId: owner.id }, hidden!.id)).toBe(true);
});
it("retains source boundaries after unlink and fails closed after source deletion", async () => {
const company = await seedCompany("KEEP");
const owner = await seedAgent(company.id);
const [source] = await db.insert(issues).values({ companyId: company.id, title: "Source", visibility: "private", assigneeAgentId: owner.id }).returning();
const [record] = await db.insert(cases).values({ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, caseType: "work", title: "Retained" }).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: record!.id, sourceKind: "issue", sourceId: source!.id });
await db.insert(caseIssueLinks).values({ companyId: company.id, caseId: record!.id, issueId: source!.id, role: "origin" });
await db.delete(caseIssueLinks).where(eq(caseIssueLinks.caseId, record!.id));
const actor = { type: "agent" as const, companyId: company.id, agentId: owner.id };
expect(await canActorReadCase(db, actor, record!.id)).toBe(true);
await db.delete(issues).where(eq(issues.id, source!.id));
expect(await canActorReadCase(db, actor, record!.id)).toBe(false);
expect(await db.select().from(caseAccessSources).where(eq(caseAccessSources.caseId, record!.id))).toHaveLength(1);
});
it("prospectively grants only the transferred task, preserving unrelated retained boundaries", async () => {
const company = await seedCompany("DEST");
const owner = await seedAgent(company.id);
const recipient = await seedAgent(company.id);
const [primary, unrelated] = await db.insert(issues).values([
{ companyId: company.id, title: "Primary", visibility: "private", assigneeAgentId: owner.id },
{ companyId: company.id, title: "Other source", visibility: "private", assigneeAgentId: owner.id },
]).returning();
const [record] = await db.insert(cases).values({ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, caseType: "work", title: "Prospective", workIssueId: primary!.id }).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: record!.id, sourceKind: "issue", sourceId: primary!.id });
const actor = { type: "agent" as const, companyId: company.id, agentId: owner.id };
expect(await canActorReceiveCase(db, actor, record!, primary!.id, recipient.id)).toBe(true);
await retainCaseAccessSource(db, { companyId: company.id, caseId: record!.id, sourceKind: "issue", sourceId: unrelated!.id });
expect(await canActorReceiveCase(db, actor, record!, primary!.id, recipient.id)).toBe(false);
});
it("keeps case assets and activity private, including detached attachment links", async () => {
const company = await seedCompany("EXTERNAL");
const owner = await seedAgent(company.id);
const outsider = await seedAgent(company.id);
const [source] = await db.insert(issues).values({ companyId: company.id, title: "Private source", visibility: "private", assigneeAgentId: owner.id }).returning();
const [privateCase, openCase] = await db.insert(cases).values([
{ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, caseType: "work", title: "Private" },
{ companyId: company.id, caseNumber: 2, identifier: `${company.issuePrefix}-C2`, caseType: "work", title: "Open" },
]).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: privateCase!.id, sourceKind: "issue", sourceId: source!.id });
const [asset] = await db.insert(assets).values({ companyId: company.id, provider: "local_disk", objectKey: `${company.id}/cases/${privateCase!.id}/2026/10/19/evidence.txt`, contentType: "text/plain", byteSize: 1, sha256: "a".repeat(64), originalFilename: "evidence.txt", createdByAgentId: owner.id }).returning();
await db.insert(caseAttachments).values({ companyId: company.id, caseId: privateCase!.id, assetId: asset!.id });
const actor = { type: "agent" as const, companyId: company.id, agentId: outsider.id };
expect(await canActorReadAssetContent(db, actor, asset!)).toBe(false);
expect(await canActorReadAssetContent(db, { ...actor, agentId: owner.id }, asset!)).toBe(true);
await db.delete(caseAttachments).where(eq(caseAttachments.assetId, asset!.id));
expect(await canActorReadAssetContent(db, actor, asset!)).toBe(false);
await db.insert(activityLog).values([
{ companyId: company.id, actorType: "agent", actorId: owner.id, agentId: owner.id, action: "case.updated", entityType: "case", entityId: privateCase!.id, details: { title: "Secret" }, createdAt: new Date("2030-01-01") },
{ companyId: company.id, actorType: "agent", actorId: owner.id, agentId: owner.id, action: "case.updated", entityType: "case", entityId: openCase!.id, details: { title: "Open" } },
]);
const caseReadCondition = await caseActivityReadSqlCondition(db, actor);
const activity = await activityService(db).list({ companyId: company.id, caseReadCondition, limit: 1 });
expect(activity.map(row => row.entityId)).toEqual([openCase!.id]);
const audit = await agentActionAuditService(db).list({ companyId: company.id, caseReadCondition, limit: 1 });
expect(audit.items.map(row => row.entityId)).toEqual([openCase!.id]);
expect(audit.nextCursor).toBeNull();
});
it("keeps source-scoped cases inside the canonical low-trust read boundary", async () => {
await enableCases();
const company = await seedCompany("SCOPE");
const scopedAgent = await seedAgent(company.id);
const rootId = randomUUID();
await db.update(agents).set({ permissions: { trustPreset: "low_trust_review", authorizationPolicy: { trustBoundary: { mode: "low_trust_review", rootIssueId: rootId } } } }).where(eq(agents.id, scopedAgent.id));
const [root, outside] = await db.insert(issues).values([
{ id: rootId, companyId: company.id, title: "Allowed root", assigneeAgentId: scopedAgent.id },
{ companyId: company.id, title: "Outside root" },
]).returning();
const [allowedCase, outsideCase] = await db.insert(cases).values([
{ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, caseType: "work", title: "Allowed" },
{ companyId: company.id, caseNumber: 2, identifier: `${company.issuePrefix}-C2`, caseType: "work", title: "Outside", updatedAt: new Date("2030-01-01") },
]).returning();
await retainCaseAccessSource(db, { companyId: company.id, caseId: allowedCase!.id, sourceKind: "issue", sourceId: root!.id });
await retainCaseAccessSource(db, { companyId: company.id, caseId: outsideCase!.id, sourceKind: "issue", sourceId: outside!.id });
const actor = { type: "agent" as const, companyId: company.id, agentId: scopedAgent.id, source: "agent_key" as const };
expect(await canActorReadCase(db, actor, allowedCase!.id)).toBe(true);
expect(await canActorReadCase(db, actor, outsideCase!.id)).toBe(false);
const list = await request(app(actor)).get(`/api/companies/${company.id}/cases?limit=1`).expect(200);
expect(list.body.map((row: { id: string }) => row.id)).toEqual([allowedCase!.id]);
});
it("rejects quarantined run writes across standalone case routes without restricting trusted writes", async () => {
await enableCases();
const company = await seedCompany("WRITE-TRUST");
const agent = await seedAgent(company.id);
const [source] = await db.insert(issues).values({ companyId: company.id, title: "Review source", assigneeAgentId: agent.id }).returning();
const policy = { trustPreset: "low_trust_review", authorizationPolicy: { trustBoundary: { mode: "low_trust_review", rootIssueId: source!.id } } };
const [containedRun, trustedRun] = await db.insert(heartbeatRuns).values([
{ companyId: company.id, agentId: agent.id, issueId: source!.id, status: "running", contextSnapshot: { executionPolicy: policy } },
{ companyId: company.id, agentId: agent.id, issueId: source!.id, status: "running", contextSnapshot: { executionPolicy: {} } },
]).returning();
const board = request(app(boardActor));
const record = await board.post(`/api/companies/${company.id}/cases`).send({ caseType: "review", key: "existing", title: "Passive case", summary: "Trusted summary", fields: { trusted: true } }).expect(201);
await board.post(`/api/cases/${record.body.id}/links`).send({ issueId: source!.id, role: "work" }).expect(201);
await board.put(`/api/cases/${record.body.id}/documents/existing`).send({ body: "Trusted document" }).expect(200);
const eventsBefore = await db.select().from(caseEvents);
const sourcesBefore = await db.select().from(caseAccessSources);
const token = createLocalAgentJwt(agent.id, company.id, agent.adapterType, containedRun!.id);
const http = request(authenticatedApp());
const authorization = `Bearer ${token}`;
await http.get(`/api/cases/${record.body.id}`).set("Authorization", authorization).expect(200);
await http.post(`/api/companies/${company.id}/cases`).set("Authorization", authorization)
.send({ caseType: "review", key: "new", title: "Raw case", fields: { raw: "quarantined" } }).expect(403);
await http.post(`/api/companies/${company.id}/cases`).set("Authorization", authorization)
.send({ caseType: "review", key: "existing", title: "Raw upsert", summary: "Raw instructions" }).expect(403);
await http.patch(`/api/cases/${record.body.id}`).set("Authorization", authorization)
.send({ summary: "Raw instructions", fields: { raw: "quarantined" } }).expect(403);
await http.put(`/api/cases/${record.body.id}/documents/raw`).set("Authorization", authorization)
.send({ body: "Quarantined instructions" }).expect(403);
await http.post(`/api/cases/${record.body.id}/documents/existing/lock`).set("Authorization", authorization).expect(403);
await http.delete(`/api/cases/${record.body.id}/documents/existing`).set("Authorization", authorization).expect(403);
await http.post(`/api/cases/${record.body.id}/links`).set("Authorization", authorization)
.send({ issueId: source!.id, role: "work" }).expect(403);
await http.post(`/api/cases/${record.body.id}/attachments`).set("Authorization", authorization)
.attach("file", Buffer.from("Quarantined attachment"), "raw.txt").expect(403);
const [unchanged] = await db.select().from(cases).where(eq(cases.id, record.body.id));
expect(unchanged).toMatchObject({ title: "Passive case", summary: "Trusted summary", fields: { trusted: true }, workIssueId: null });
expect(await db.select().from(cases)).toHaveLength(1);
expect(await db.select().from(documents)).toHaveLength(1);
expect(await db.select().from(assets)).toHaveLength(0);
expect(await db.select().from(caseEvents)).toEqual(eventsBefore);
expect(await db.select().from(caseAccessSources)).toEqual(sourcesBefore);
const trusted = `Bearer ${createLocalAgentJwt(agent.id, company.id, agent.adapterType, trustedRun!.id)}`;
await http.patch(`/api/cases/${record.body.id}`).set("Authorization", trusted).send({ fields: { sanitized: true } }).expect(200);
await http.put(`/api/cases/${record.body.id}/documents/sanitized`).set("Authorization", trusted).send({ body: "Trusted finding" }).expect(200);
await expect(assertCaseTrustedForExecution(db, { id: record.body.id, companyId: company.id })).resolves.toBeUndefined();
});
it("blocks historical low-trust case output from execution while preserving authorized reads", async () => {
await enableCases();
const company = await seedCompany("HISTORICAL-TRUST");
const agent = await seedAgent(company.id);
const [source] = await db.insert(issues).values({ companyId: company.id, title: "Retained source", assigneeAgentId: agent.id }).returning();
const record = await request(app(boardActor)).post(`/api/companies/${company.id}/cases`)
.send({ caseType: "review", title: "Historical case", fields: { unreviewed: "Old raw output" } }).expect(201);
await retainCaseAccessSource(db, { companyId: company.id, caseId: record.body.id, sourceKind: "issue", sourceId: source!.id });
await expect(assertCaseTrustedForExecution(db, { id: record.body.id, companyId: company.id })).resolves.toBeUndefined();
const policy = { trustPreset: "low_trust_review", authorizationPolicy: { trustBoundary: { mode: "low_trust_review", rootIssueId: source!.id } } };
await db.update(issues).set({ executionPolicy: policy }).where(eq(issues.id, source!.id));
await expect(assertCaseTrustedForExecution(db, { id: record.body.id, companyId: company.id })).rejects.toMatchObject({ status: 403 });
await db.update(issues).set({ executionPolicy: {} }).where(eq(issues.id, source!.id));
const [oldRun] = await db.insert(heartbeatRuns).values({ companyId: company.id, agentId: agent.id, issueId: source!.id,
status: "succeeded", contextSnapshot: { executionPolicy: policy } }).returning();
// Represents output persisted before cases enforced quarantine; current task
// and agent settings are now standard, but the recorded source run is not.
await db.insert(caseEvents).values({ companyId: company.id, caseId: record.body.id, kind: "fields_changed",
actorType: "agent", actorAgentId: agent.id, runId: oldRun!.id });
await expect(assertCaseTrustedForExecution(db, { id: record.body.id, companyId: company.id })).rejects.toMatchObject({ status: 403 });
const readable = await request(app(boardActor)).get(`/api/cases/${record.body.id}`).expect(200);
expect(readable.body.fields).toEqual({ unreviewed: "Old raw output" });
});
it("preserves historical source attribution for a delivered board-originated handoff", async () => {
const company = await seedCompany("HISTORY");
const previous = await seedAgent(company.id), recipient = await seedAgent(company.id);
const [task] = await db.insert(issues).values({ companyId: company.id, title: "Historical workflow", assigneeAgentId: recipient.id }).returning();
const [record] = await db.insert(cases).values({ companyId: company.id, caseNumber: 1, identifier: `${company.issuePrefix}-C1`, caseType: "work", title: "History", workIssueId: task!.id }).returning();
const [handoff] = await db.insert(caseHandoffs).values({ companyId: company.id, caseId: record!.id, issueId: task!.id, version: 1,
fromAgentId: previous.id, toAgentId: recipient.id, phase: "implementation", instructions: "Continue this work", actorType: "user", actorId: "fixture-board",
wakeupRequestId: randomUUID(), idempotencyKey: "history", inputFingerprint: "history", state: "delivered" }).returning();
await expect(db.delete(agents).where(eq(agents.id, previous.id))).rejects.toThrow();
expect((await db.select().from(caseHandoffs).where(eq(caseHandoffs.id, handoff!.id)))[0]!.fromAgentId).toBe(previous.id);
});
});