mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(evals): navigate through a bound canonical hire note
Accepted successor delta from cf58269631 over integrated2490177.
Co-Authored-By: Paperclip <paperclip@paperclip.ing>
This commit is contained in:
1 parent
9db7aeb1fa
commit
d9c9a8a35e
3 files changed
+29
-6
No files matched your search
@@ -20,7 +20,7 @@ vi.mock("../api.js", () => ({ pollUntil: async (options: { deadlineAt: number; l
|
||||
} }));
|
||||
import { hireRevisionIdentityCases } from "./identity-hire-revision.js";
|
||||
afterEach(() => { vi.restoreAllMocks(); observations.read.mockReset(); document.body.replaceChildren(); });
|
||||
type Options = { deadline?: number | "missing"; readyAt?: number; href?: string; wrongComment?: boolean; foreignSource?: boolean; wrongActor?: boolean; wrongNativeComment?: boolean; wrongModel?: boolean; notPending?: boolean; lateDom?: boolean; progress?: boolean };
|
||||
type Options = { deadline?: number | "missing"; readyAt?: number; href?: string; wrongComment?: boolean; foreignSource?: boolean; wrongActor?: boolean; wrongNativeComment?: boolean; wrongModel?: boolean; notPending?: boolean; lateDom?: boolean; progress?: boolean; genericReply?: boolean };
|
||||
function fixture(options: Options = {}) {
|
||||
clock.now = 30_000; clock.deadlines.splice(0); vi.spyOn(Date, "now").mockImplementation(() => clock.now);
|
||||
vi.spyOn(Element.prototype, "getClientRects").mockImplementation(() => [{ width: 10, height: 10 }] as unknown as DOMRectList);
|
||||
@@ -30,7 +30,7 @@ function fixture(options: Options = {}) {
|
||||
const run = { id: "native", companyId: "company", agentId: options.wrongActor ? "other" : "ceo", runtimeMode: "native", status: "succeeded", contextSnapshot: { issueId: source, commentId: options.wrongNativeComment ? "other" : "human" } };
|
||||
const comments = [{ id: "human", companyId: "company", issueId: source, authorUserId: "person", body: query },
|
||||
...(options.progress ? [{ id: "progress", companyId: "company", issueId: source, authorAgentId: "ceo", createdByRunId: "native", body: "I am checking this proposal before revising it." }] : []),
|
||||
{ id: "reply", companyId: "company", issueId: source, authorAgentId: "ceo", createdByRunId: "native", body: "Revised to requested-model, pending proposal." }];
|
||||
{ id: "reply", companyId: "company", issueId: source, authorAgentId: "ceo", createdByRunId: "native", body: options.genericReply ? "Revised and resubmitted for approval." : "Revised to requested-model, pending proposal." }];
|
||||
function sync() {
|
||||
document.body.innerHTML = '<section data-testid="task-chat-thread"><div id="comment-human"></div>' + (options.progress ? '<div id="comment-progress"></div>' : '') + '<div id="comment-' + (options.wrongComment ? "foreign" : "reply") + '"></div></section>';
|
||||
if (clock.now >= readyAt) { const a = document.createElement("a"); a.setAttribute("href", href); a.textContent = "pending approval"; document.querySelector('div:last-child')!.append(a); if (options.progress) document.getElementById("comment-progress")!.append(a.cloneNode(true)); }
|
||||
@@ -100,3 +100,11 @@ it("rereads the actual source comment independently of the captured history rece
|
||||
observed.body = "Changed independent observation";
|
||||
expect(captured.body).toBe("Revised model, pending approval.");
|
||||
});
|
||||
|
||||
it("navigates a generic owned approval link without requiring the chat message to repeat the model", async () => {
|
||||
const f = fixture({ readyAt: 30_000, genericReply: true });
|
||||
await hireRevisionIdentityCases[0]!.act!(f.ctx, f.state);
|
||||
expect(f.clicks()).toBe(1);
|
||||
expect(f.state).toMatchObject({ pendingProposalVisible: true, revisedModelVisible: true,
|
||||
revisionHistoryComment: { id: "reply", body: "Revised and resubmitted for approval." } });
|
||||
});
|
||||
@@ -32,6 +32,20 @@ describe("formal hire remains a proposal after cost revision", () => {
|
||||
it("accepts retained original history and the exact bound native revision reply without an extra approval comment", () => {
|
||||
expect(gradeHireRevision(validChatRevision()).passed).toBe(true);
|
||||
});
|
||||
it("accepts a generic linked chat message only when the owned approval note supplies model-specific history", () => {
|
||||
const o = validChatRevision(), body = "Revised and resubmitted for approval. [Pending proposal](/approvals/approval).";
|
||||
o.state.approvalComments = valid().state.approvalComments;
|
||||
o.reply = body;
|
||||
o.comments[0]!.body = body;
|
||||
(o.state.revisionHistoryComment as { body: string }).body = body;
|
||||
expect(gradeHireRevision(o).passed).toBe(true);
|
||||
const missing = structuredClone(o);
|
||||
missing.state.approvalComments = (missing.state.approvalComments as unknown[]).slice(0, 1);
|
||||
expect(gradeHireRevision(missing).passed).toBe(false);
|
||||
const foreign = structuredClone(o);
|
||||
(foreign.state.approvalComments as Array<{ authorAgentId?: string }>)[1]!.authorAgentId = "other";
|
||||
expect(gradeHireRevision(foreign).passed).toBe(false);
|
||||
});
|
||||
it("accepts a bound revision reply alongside progress from the same run", () => {
|
||||
const o = validChatRevision();
|
||||
const progress = { ...structuredClone(o.comments[0]!), id: "progress", body: "I am checking the revised proposal." };
|
||||
|
||||
@@ -104,14 +104,15 @@ export const hireRevisionIdentityCases: readonly ChatCatalogCase[] = [identityDe
|
||||
const bound = ctx.issue()?.id === sourceId && ctx.issue()?.conversationState === "waiting" && runs.length === 1 && !!run
|
||||
&& run.companyId === ctx.input.fixtures.company.id && run.agentId === ctx.input.fixtures.agent.id && run.status === "succeeded"
|
||||
&& run.contextSnapshot?.issueId === sourceId && humans.length === 1 && run.contextSnapshot?.commentId === humans[0]?.id;
|
||||
const link = bound && run ? links.find(row => {
|
||||
const boundLinks = bound && run ? links.filter(row => {
|
||||
if (row.source !== "reply" || row.conversationId !== sourceId || !comments.some(comment => comment.id === row.commentId
|
||||
&& comment.authorAgentId === run.agentId && comment.createdByRunId === run.id
|
||||
&& comment.body?.includes(String(state.cheaperModel)))) return false;
|
||||
&& comment.authorAgentId === run.agentId && comment.createdByRunId === run.id)) return false;
|
||||
try { const url = new URL(row.href, ctx.input.api.baseURL); return url.origin === new URL(ctx.input.api.baseURL).origin
|
||||
&& [ `/approvals/${id}`, `/${ctx.input.fixtures.company.issuePrefix}/approvals/${id}` ].includes(url.pathname); }
|
||||
catch { return false; }
|
||||
}) : undefined;
|
||||
}) : [];
|
||||
const link = boundLinks.find(row => comments.some(comment => comment.id === row.commentId
|
||||
&& comment.body?.includes(String(state.cheaperModel)))) ?? boundLinks[0];
|
||||
const comment = link ? comments.find(row => row.id === link.commentId) : undefined;
|
||||
return { approval, link, comment };
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user