185 lines
7.6 KiB
TypeScript
185 lines
7.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
ISSUE_WRITE_DENIAL_CODES,
|
|
describeIssueWriteDenial,
|
|
isIssueWriteDenialCode,
|
|
issueWriteDenialApiMessage,
|
|
issueWriteDenialCodeForResponsibleUserDenial,
|
|
issueWriteDenialResponse,
|
|
} from "./issue-write-denial.js";
|
|
|
|
describe("describeIssueWriteDenial", () => {
|
|
it("answers all three plan §6 obligations for every code", () => {
|
|
for (const code of ISSUE_WRITE_DENIAL_CODES) {
|
|
const copy = describeIssueWriteDenial(code);
|
|
expect(copy.code, code).toBe(code);
|
|
// Boundary that fired, who can act, sanctioned path — none may be empty.
|
|
expect(copy.boundary.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.whoCanAct.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.sanctionedPath.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.title.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.description.trim().length, code).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("never leaks a raw id or placeholder when labels are unknown", () => {
|
|
for (const code of ISSUE_WRITE_DENIAL_CODES) {
|
|
const copy = describeIssueWriteDenial(code);
|
|
const prose = `${copy.description} ${copy.whoCanAct} ${copy.sanctionedPath}`;
|
|
expect(prose, code).not.toMatch(/undefined|null/);
|
|
// Unknown labels degrade to generic nouns, never a bare id.
|
|
expect(prose, code).toMatch(/this task|this agent|the current assignee|the responsible user/);
|
|
}
|
|
});
|
|
|
|
it("keeps the boundary free of parentheses and distinct from the title", () => {
|
|
for (const code of ISSUE_WRITE_DENIAL_CODES) {
|
|
const copy = describeIssueWriteDenial(code, { cap: 20 });
|
|
// Surfaces render the boundary inside their own parens — nesting stutters.
|
|
expect(copy.boundary, code).not.toMatch(/[()]/);
|
|
// A title echoed verbatim as its own boundary reads as a mistake.
|
|
expect(copy.boundary.toLowerCase(), code).not.toBe(copy.title.toLowerCase());
|
|
}
|
|
});
|
|
|
|
it("names the actor, assignee, and task when they are known", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_not_visible", {
|
|
actorLabel: "Fable",
|
|
assigneeLabel: "CodexCoder",
|
|
issueIdentifier: "TASK-482",
|
|
});
|
|
expect(copy.description).toContain("TASK-482");
|
|
expect(copy.description).toContain("Fable");
|
|
expect(copy.whoCanAct).toContain("CodexCoder");
|
|
});
|
|
|
|
it("points a visibility denial at the sanctioned child-issue path", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_not_visible");
|
|
// The incident detour was discovering exactly this workaround.
|
|
expect(copy.sanctionedPath).toContain("child issue");
|
|
});
|
|
|
|
it("frames the per-run cap as a rate backstop, not a permission decision", () => {
|
|
const copy = describeIssueWriteDenial("cross_issue_influence_cap_exceeded", {
|
|
cap: 20,
|
|
count: 21,
|
|
actorLabel: "Fable",
|
|
});
|
|
expect(copy.status).toBe(429);
|
|
expect(copy.tone).toBe("cap");
|
|
expect(copy.boundary).toContain("20");
|
|
expect(copy.description).toContain("attempt 21");
|
|
expect(copy.description).toContain("still allowed");
|
|
expect(copy.sanctionedPath).toContain("next heartbeat");
|
|
});
|
|
|
|
it("defaults the cap to the shipped limit when context omits it", () => {
|
|
const copy = describeIssueWriteDenial("cross_issue_influence_cap_exceeded");
|
|
expect(copy.boundary).toContain("20");
|
|
expect(copy.description).not.toContain("attempt");
|
|
});
|
|
|
|
it("gives the run-context denial a copy-pasteable fix", () => {
|
|
const copy = describeIssueWriteDenial("cross_issue_influence_run_context_required");
|
|
expect(copy.sanctionedPath).toContain("X-Paperclip-Run-Id");
|
|
expect(copy.sanctionedPath).toContain("PAPERCLIP_RUN_ID");
|
|
});
|
|
|
|
it("tells a spoof attempt that the write itself was fine", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_attribution_spoof_rejected", {
|
|
actorLabel: "Fable",
|
|
responsibleUserName: "Dotta",
|
|
});
|
|
expect(copy.status).toBe(422);
|
|
expect(copy.whoCanAct).toContain("Fable");
|
|
expect(copy.sanctionedPath).toContain("Dotta");
|
|
expect(copy.sanctionedPath).toContain("onBehalfOfUserId");
|
|
});
|
|
|
|
it("routes a run lock to comments, which stay open", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_assignee_run_lock", {
|
|
assigneeLabel: "CodexCoder",
|
|
});
|
|
expect(copy.status).toBe(409);
|
|
expect(copy.tone).toBe("lock");
|
|
expect(copy.sanctionedPath).toContain("Comment instead");
|
|
expect(copy.sanctionedPath).toContain("CodexCoder");
|
|
});
|
|
|
|
it("reuses responsible-user ceiling copy and keeps on-behalf-of terminology", () => {
|
|
const ceiling = describeIssueWriteDenial("issue_write_responsible_user_ceiling", {
|
|
responsibleUserName: "Dotta",
|
|
issueIdentifier: "TASK-517",
|
|
});
|
|
expect(ceiling.title).toBe("Responsible user not authorized");
|
|
expect(ceiling.description).toContain("on behalf");
|
|
expect(ceiling.description).toContain("TASK-517");
|
|
expect(ceiling.description).not.toContain("impersonat");
|
|
expect(ceiling.whoCanAct).toContain("Dotta");
|
|
|
|
const unavailable = describeIssueWriteDenial("issue_write_responsible_user_unavailable", {
|
|
responsibleUserName: "Dotta",
|
|
});
|
|
expect(unavailable.title).toBe("Responsible user unavailable");
|
|
expect(unavailable.sanctionedPath).toContain("blocked");
|
|
});
|
|
|
|
it("falls back to generic phrasing when the responsible user is unknown", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_responsible_user_ceiling");
|
|
expect(copy.description).toContain("the responsible user");
|
|
});
|
|
});
|
|
|
|
describe("issueWriteDenialCodeForResponsibleUserDenial", () => {
|
|
it("bridges both authorization-layer ceiling codes", () => {
|
|
expect(issueWriteDenialCodeForResponsibleUserDenial("RESPONSIBLE_USER_UNAUTHORIZED"))
|
|
.toBe("issue_write_responsible_user_ceiling");
|
|
expect(issueWriteDenialCodeForResponsibleUserDenial("RESPONSIBLE_USER_UNAVAILABLE"))
|
|
.toBe("issue_write_responsible_user_unavailable");
|
|
});
|
|
});
|
|
|
|
describe("isIssueWriteDenialCode", () => {
|
|
it("accepts shipped codes and rejects everything else", () => {
|
|
expect(isIssueWriteDenialCode("issue_write_not_visible")).toBe(true);
|
|
expect(isIssueWriteDenialCode("RESPONSIBLE_USER_UNAUTHORIZED")).toBe(false);
|
|
expect(isIssueWriteDenialCode(null)).toBe(false);
|
|
expect(isIssueWriteDenialCode(undefined)).toBe(false);
|
|
expect(isIssueWriteDenialCode("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("issueWriteDenialApiMessage", () => {
|
|
it("keeps boundary, who-can-act, and sanctioned path in the flattened error", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_not_visible", {
|
|
actorLabel: "Fable",
|
|
issueIdentifier: "TASK-482",
|
|
});
|
|
const message = issueWriteDenialApiMessage(copy);
|
|
expect(message).toContain(copy.boundary);
|
|
expect(message).toContain("Who can act:");
|
|
expect(message).toContain("Try this:");
|
|
expect(message).toContain(copy.sanctionedPath);
|
|
});
|
|
});
|
|
|
|
describe("issueWriteDenialResponse", () => {
|
|
it("pairs the status with a machine-readable details payload", () => {
|
|
const { status, body } = issueWriteDenialResponse("cross_issue_influence_cap_exceeded", {
|
|
cap: 20,
|
|
count: 21,
|
|
});
|
|
expect(status).toBe(429);
|
|
expect(body.details.code).toBe("cross_issue_influence_cap_exceeded");
|
|
expect(body.details.boundary).toContain("20");
|
|
expect(body.error).toContain("Who can act:");
|
|
});
|
|
|
|
it("uses the status each code declares", () => {
|
|
expect(issueWriteDenialResponse("issue_write_assignee_run_lock").status).toBe(409);
|
|
expect(issueWriteDenialResponse("issue_write_attribution_spoof_rejected").status).toBe(422);
|
|
expect(issueWriteDenialResponse("issue_write_not_visible").status).toBe(403);
|
|
});
|
|
});
|