Merge pull request #7553 from paperclipai/codex/pap-10343-operator-qol-pr
[codex] Group operator QoL fixes
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Readable } from "node:stream";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -190,6 +191,13 @@ function makeAttachment(contentType: string, originalFilename: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseBinaryResponse(res: IncomingMessage, callback: (error: Error | null, body?: Buffer) => void) {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
res.on("end", () => callback(null, Buffer.concat(chunks)));
|
||||
res.on("error", callback);
|
||||
}
|
||||
|
||||
describe("normalizeIssueAttachmentMaxBytes", () => {
|
||||
it("keeps the process-level attachment cap as the final cap", async () => {
|
||||
const previous = process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES;
|
||||
@@ -362,7 +370,10 @@ describe("issue attachment routes", () => {
|
||||
mockIssueService.getAttachmentById.mockResolvedValue(makeAttachment("text/html", "report.html"));
|
||||
|
||||
const app = await createApp(storage);
|
||||
const res = await request(app).get("/api/attachments/attachment-1/content");
|
||||
const res = await request(app)
|
||||
.get("/api/attachments/attachment-1/content")
|
||||
.buffer(true)
|
||||
.parse(parseBinaryResponse);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect([
|
||||
|
||||
@@ -453,8 +453,12 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
companyId,
|
||||
}, created.id, {
|
||||
answers: [
|
||||
{ questionId: "scope", optionIds: ["phase-1"] },
|
||||
{ questionId: "extras", optionIds: ["docs", "tests", "docs"] },
|
||||
{ questionId: "scope", optionIds: [], otherText: "Custom Phase 1" },
|
||||
{
|
||||
questionId: "extras",
|
||||
optionIds: ["docs", "tests", "docs"],
|
||||
otherText: " Pair with release notes ",
|
||||
},
|
||||
],
|
||||
summaryMarkdown: "Ship Phase 1 with tests and docs.",
|
||||
}, {
|
||||
@@ -465,8 +469,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
expect(answered.result).toEqual({
|
||||
version: 1,
|
||||
answers: [
|
||||
{ questionId: "scope", optionIds: ["phase-1"] },
|
||||
{ questionId: "extras", optionIds: ["docs", "tests"] },
|
||||
{ questionId: "scope", optionIds: [], otherText: "Custom Phase 1" },
|
||||
{ questionId: "extras", optionIds: ["docs", "tests"], otherText: "Pair with release notes" },
|
||||
],
|
||||
summaryMarkdown: "Ship Phase 1 with tests and docs.",
|
||||
});
|
||||
@@ -824,7 +828,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("expires request confirmations opted into user-comment supersede after creation", async () => {
|
||||
it("expires request confirmations by default when a user comments after creation", async () => {
|
||||
const { companyId, issueId } = await seedConfirmationIssue();
|
||||
const commentId = randomUUID();
|
||||
|
||||
@@ -836,12 +840,17 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Proceed with the current draft?",
|
||||
supersedeOnUserComment: true,
|
||||
},
|
||||
}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
expect(created).toMatchObject({
|
||||
payload: {
|
||||
supersedeOnUserComment: true,
|
||||
},
|
||||
});
|
||||
|
||||
const expired = await interactionsSvc.expireRequestConfirmationsSupersededByComment({
|
||||
id: issueId,
|
||||
companyId,
|
||||
@@ -866,7 +875,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps request confirmations pending unless user-comment supersede is explicitly enabled", async () => {
|
||||
it("keeps request confirmations pending when user-comment supersede is explicitly disabled", async () => {
|
||||
const { companyId, issueId } = await seedConfirmationIssue("Comment supersede opt-out");
|
||||
|
||||
await interactionsSvc.create({
|
||||
@@ -877,6 +886,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Proceed with the current draft?",
|
||||
supersedeOnUserComment: false,
|
||||
},
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -899,6 +909,40 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
expect(rows[0]?.status).toBe("pending");
|
||||
});
|
||||
|
||||
it("keeps legacy request confirmations pending when comment supersede was not stored", async () => {
|
||||
const { companyId, issueId } = await seedConfirmationIssue("Legacy confirmation without comment supersede flag");
|
||||
|
||||
await db.insert(issueThreadInteractions).values({
|
||||
id: randomUUID(),
|
||||
companyId,
|
||||
issueId,
|
||||
kind: "request_confirmation",
|
||||
status: "pending",
|
||||
continuationPolicy: { kind: "none" },
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Proceed with the current draft?",
|
||||
},
|
||||
createdByUserId: "local-board",
|
||||
});
|
||||
|
||||
const expired = await interactionsSvc.expireRequestConfirmationsSupersededByComment({
|
||||
id: issueId,
|
||||
companyId,
|
||||
}, {
|
||||
id: randomUUID(),
|
||||
createdAt: new Date(Date.now() + 1_000),
|
||||
authorUserId: "local-board",
|
||||
}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
expect(expired).toHaveLength(0);
|
||||
const rows = await db.select().from(issueThreadInteractions);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]?.status).toBe("pending");
|
||||
});
|
||||
|
||||
it("does not supersede request confirmations for agent, system, or older user comments", async () => {
|
||||
const { companyId, issueId } = await seedConfirmationIssue("Comment supersede exclusions");
|
||||
|
||||
@@ -910,7 +954,6 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Proceed with the current draft?",
|
||||
supersedeOnUserComment: true,
|
||||
},
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -966,7 +1009,6 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Proceed with the current draft?",
|
||||
supersedeOnUserComment: true,
|
||||
},
|
||||
}, {
|
||||
userId: "local-board",
|
||||
|
||||
@@ -149,9 +149,11 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||
afterEach(async () => {
|
||||
await db.delete(issueComments);
|
||||
await db.delete(issueRelations);
|
||||
await db.delete(issueDocuments);
|
||||
await db.delete(issueInboxArchives);
|
||||
await db.delete(activityLog);
|
||||
await db.delete(issues);
|
||||
await db.delete(documents);
|
||||
await db.delete(executionWorkspaces);
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(projects);
|
||||
@@ -386,6 +388,76 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||
expect(result.map((issue) => issue.id)).toEqual([titleMatchId, descriptionMatchId]);
|
||||
});
|
||||
|
||||
it("filters issues by whether they have a plan document", async () => {
|
||||
const companyId = randomUUID();
|
||||
const withPlanId = randomUUID();
|
||||
const withoutPlanId = randomUUID();
|
||||
const otherDocumentId = randomUUID();
|
||||
const planDocumentId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: withPlanId,
|
||||
companyId,
|
||||
title: "Issue with plan",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
},
|
||||
{
|
||||
id: withoutPlanId,
|
||||
companyId,
|
||||
title: "Issue without plan",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(documents).values([
|
||||
{
|
||||
id: planDocumentId,
|
||||
companyId,
|
||||
title: null,
|
||||
format: "markdown",
|
||||
latestBody: "# Plan",
|
||||
},
|
||||
{
|
||||
id: otherDocumentId,
|
||||
companyId,
|
||||
title: "Notes",
|
||||
format: "markdown",
|
||||
latestBody: "# Notes",
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(issueDocuments).values([
|
||||
{
|
||||
companyId,
|
||||
issueId: withPlanId,
|
||||
documentId: planDocumentId,
|
||||
key: "plan",
|
||||
},
|
||||
{
|
||||
companyId,
|
||||
issueId: withoutPlanId,
|
||||
documentId: otherDocumentId,
|
||||
key: "notes",
|
||||
},
|
||||
]);
|
||||
|
||||
const withPlan = await svc.list(companyId, { hasPlanDocument: true });
|
||||
const withoutPlan = await svc.list(companyId, { hasPlanDocument: false });
|
||||
|
||||
expect(withPlan.map((issue) => issue.id)).toEqual([withPlanId]);
|
||||
expect(withoutPlan.map((issue) => issue.id)).toEqual([withoutPlanId]);
|
||||
});
|
||||
|
||||
it("can page issues by most recently updated before priority", async () => {
|
||||
const companyId = randomUUID();
|
||||
const oldCriticalIssueId = randomUUID();
|
||||
|
||||
Reference in New Issue
Block a user