mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
test(evals): distinguish committed reassignment from admission
This commit is contained in:
1 parent
56b3942467
commit
a69d4fd46e
4 files changed
+111
-10
No files matched your search
@@ -1,16 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { gradeEditOwnership } from "./internal-edit-ownership.js";
|
||||
import { gradeEditOwnership, expectedEditReassignmentCancellation, assignmentDenials } from "./internal-edit-ownership.js";
|
||||
import { grade_tasks } from "./internal-tasks.js";
|
||||
import type { InternalObservation, Entity } from "./internal-support.js";
|
||||
import type { ChatRun } from "../chat-flow.js";
|
||||
function valid() {
|
||||
const initial = { id: "task", companyId: "company", title: "Landing page", status: "backlog", priority: "medium", assigneeAgentId: null };
|
||||
const first: InternalObservation = { scenarioId: "tasks.edit-properties", origin: "https://fixture.test", companyId: "company", leadId: "ceo", state: { task: initial, worker: { id: "morgan" } }, tasks: [{ ...initial, priority: "high", assigneeAgentId: "morgan" }], projects: [], goals: [], comments: [], activity: [{ id: "first-edit", entityId: "task", action: "issue.updated", agentId: "ceo", details: { priority: "high" } }], documents: [], revisions: [], links: ["/RUN/issues/task"], humanQueries: [], reply: "Saved High priority and Morgan assignment.", runs: [{ id: "first", companyId: "company", agentId: "ceo", status: "succeeded" }] };
|
||||
const held = { ...initial, assigneeAgentId: "riley", status: "in_progress", executionRunId: "riley-run", checkoutRunId: "riley-run" };
|
||||
return { ...structuredClone(first), state: { ...first.state, firstEdit: first, riley: { id: "riley" }, heldTask: structuredClone(held), heldRun: { id: "riley-run" }, gateCalls: [{ authorized: true }] }, tasks: [held], currentTask: held,
|
||||
assignmentDenials: [{ runId: "second", taskId: "task", assigneeId: "morgan", callId: "denied", reason: "Cannot stop the owned checkout safely" }],
|
||||
phaseReply: "Riley holds the running execution checkout. I cannot change priority or reassign to Morgan until that ownership is released.",
|
||||
runs: [...first.runs, { id: "riley-run", companyId: "company", agentId: "riley", status: "running" }, { id: "second", companyId: "company", agentId: "ceo", status: "succeeded" }] };
|
||||
}
|
||||
const grade = (o: unknown) => gradeEditOwnership(o, grade_tasks);
|
||||
function graceful() {
|
||||
const o = valid(); o.currentTask.priority = "high"; o.currentTask.assigneeAgentId = "morgan"; o.currentTask.executionRunId = ""; o.currentTask.checkoutRunId = "";
|
||||
const query = "Make it high priority and assign Morgan."; Object.assign(o.state, { canonicalEditQuery: query });
|
||||
const comments = [{ id: "q1", body: query, createdAt: "2026-10-27T08:00:00Z" }, { id: "q2", body: query, createdAt: "2026-10-27T08:02:00Z" }];
|
||||
Object.assign(o.runs[1]!, { status: "cancelled", errorCode: "issue_reassigned", contextSnapshot: { issueId: "task" }, resultJson: { reassignmentStopConfirmed: true, nativeCancellation: { schema: "paperclip.native-cancellation.v1", dispatchState: "acknowledged", runId: "riley-run", issueId: "task", companyId: "company", acknowledgedAt: "2026-10-27T08:02:02Z" } } });
|
||||
Object.assign(o.runs[2]!, { contextSnapshot: { issueId: "source", wakeCommentId: "q2" } });
|
||||
o.activity.push({ id: "reassigned", companyId: "company", actorType: "agent", actorId: "ceo", action: "issue.reassigned", entityId: "task", agentId: "ceo", runId: "second", createdAt: "2026-10-27T08:02:03Z", details: { changed: true, previousAssigneeAgentId: "riley", assigneeAgentId: "morgan" } } as Entity,
|
||||
{ id: "final-priority", companyId: "company", actorType: "agent", actorId: "ceo", action: "issue.updated", entityId: "task", agentId: "ceo", runId: "second", createdAt: "2026-10-27T08:02:04Z", details: { priority: "high" } } as Entity);
|
||||
o.phaseReply = "Priority is high and the task is assigned to Morgan. New execution is pending the accounting boundary; the assignment is saved.";
|
||||
return { ...o, sourceComments: comments };
|
||||
}
|
||||
describe("property edit execution-boundary conditional calibration", () => {
|
||||
it("accepts actual primary success and truthful later denial of both changes", () => expect(grade(valid()).passed).toBe(true));
|
||||
it("accepts an independently saved priority with denied reassignment", () => { const o = valid(); o.currentTask.priority = "high"; o.phaseReply = "Priority is now high. Riley holds the running checkout; I cannot assign Morgan until that execution ends."; o.activity.push({ id: "final-edit", entityId: "task", action: "issue.updated", agentId: "ceo", details: { priority: "high" }, runId: "second" } as Entity); expect(grade(o).passed).toBe(true); });
|
||||
@@ -19,4 +32,27 @@ describe("property edit execution-boundary conditional calibration", () => {
|
||||
it("rejects stealing the real checkout or admitting Morgan concurrently", () => { const o = valid(); o.currentTask.executionRunId = "other"; expect(grade(o).passed).toBe(false); const extra = valid(); extra.runs.push({ id: "morgan-run", companyId: "company", agentId: "morgan", status: "running" }); expect(grade(extra).passed).toBe(false); });
|
||||
it("does not credit a prior phase's priority audit as the conditional save", () => { const o = valid(); o.currentTask.priority = "high"; o.phaseReply = "Priority is now high. Riley holds the running checkout; I cannot assign Morgan until it ends."; expect(grade(o).passed).toBe(false); });
|
||||
it("rejects a wrong-phase-only explanation", () => { const o = valid(); o.reply += o.phaseReply; o.phaseReply = "Done."; expect(grade(o).passed).toBe(false); });
|
||||
it("accepts exact acknowledged graceful reassignment with truthful saved state and pending new admission", () => { const o = graceful(); expect(grade(o).passed).toBe(true); expect(expectedEditReassignmentCancellation(o)).toEqual({ runId: "riley-run", taskId: "task", workerId: "riley", sourceRunId: "second" }); });
|
||||
it("accepts actual task-bound Morgan admission where it occurs", () => { const o = graceful(); o.runs.push({ id: "morgan-run", companyId: "company", agentId: "morgan", status: "running", contextSnapshot: { issueId: "task" } } as ChatRun); o.currentTask.executionRunId = "morgan-run"; o.phaseReply = "High priority saved; assigned to Morgan, who is running the task now."; expect(grade(o).passed).toBe(true); });
|
||||
it("rejects wrong cancelled run/reason/task/actor rather than allowing any expected cancellation", () => {
|
||||
for (const patch of [{ errorCode: "cancelled" }, { id: "unrelated" }, { agentId: "other" }, { contextSnapshot: { issueId: "wrong-task" } }]) { const o = graceful(); Object.assign(o.runs[1]!, patch); expect(expectedEditReassignmentCancellation(o)).toBeUndefined(); expect(grade(o).passed).toBe(false); }
|
||||
const wrong = graceful(); wrong.activity[1]!.agentId = "other"; expect(grade(wrong).passed).toBe(false);
|
||||
});
|
||||
it("rejects cancellation before the actual second human request and an unacknowledged stop", () => {
|
||||
const o = graceful(); const result = (o.runs[1] as ChatRun).resultJson!; (result.nativeCancellation as Record<string, unknown>).acknowledgedAt = "2026-10-27T08:01:00Z"; expect(grade(o).passed).toBe(false);
|
||||
const wrong = graceful(); ((wrong.runs[1] as ChatRun).resultJson!.nativeCancellation as Record<string, unknown>).dispatchState = "pending"; expect(grade(wrong).passed).toBe(false);
|
||||
});
|
||||
it("rejects missing final priority/assignment, prior phase audit and invented Morgan execution", () => {
|
||||
const o = graceful(); o.currentTask.priority = "medium"; expect(grade(o).passed).toBe(false);
|
||||
const wrong = graceful(); wrong.currentTask.assigneeAgentId = "riley"; expect(grade(wrong).passed).toBe(false);
|
||||
const earlier = graceful(); (earlier.activity[2] as Entity & { runId?: string }).runId = "first"; expect(grade(earlier).passed).toBe(false);
|
||||
const invented = graceful(); invented.phaseReply = "High priority saved; assigned Morgan, who is running now."; expect(grade(invented).passed).toBe(false);
|
||||
});
|
||||
it("requires a real denial receipt instead of inferring denial from a stoppable checkout", () => { const o = valid(); o.assignmentDenials = []; expect(grade(o).passed).toBe(false); });
|
||||
it("binds genuine ownership-conflict results to exact native task/assignee/call and rejects postcommit accounting errors", () => {
|
||||
const outer = (id: string, eventType: string, item: Record<string, unknown>) => ({ runId: "second", sourceEventId: id, payload: { prpEvent: { schema: "paperclip.prp.event.v1", sourceEventId: id, runId: "second", eventType, itemId: "call", payload: { kind: "dynamicToolCall", item } } } });
|
||||
const events = [outer("input", "item.started", { id: "call", type: "tool_use", name: "reassign_task", input: { taskId: "task", assigneeActorId: "morgan" } }), outer("result", "item.completed", { id: "call", tool_use_id: "call", type: "tool_result", is_error: true, error: "Cannot stop the owned checkout safely" })];
|
||||
expect(assignmentDenials(events, "second", "task", "morgan")).toHaveLength(1); expect(assignmentDenials(events, "second", "wrong", "morgan")).toEqual([]);
|
||||
events[1]!.payload.prpEvent.payload.item.error = "New work must wait while a native run recovers its unfinished accounting."; expect(assignmentDenials(events, "second", "task", "morgan")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,75 @@
|
||||
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
|
||||
import { pollUntil } from "../api.js";
|
||||
import { collectRunEvents } from "../run-observations.js";
|
||||
import { fixtureWorker } from "./internal-definition.js";
|
||||
import { startRunBriefGate } from "./internal-run-brief.js";
|
||||
import { checks, entity, hasLink, observation, observeInternal, sameTask, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
interface OwnedTask extends Entity { executionRunId?: string | null; checkoutRunId?: string | null }
|
||||
interface EditOwnership extends InternalObservation { currentTask: OwnedTask; phaseReply: string }
|
||||
interface EditOwnership extends InternalObservation { currentTask: OwnedTask; phaseReply: string; sourceComments?: Entity[]; assignmentDenials?: { runId: string; taskId: string; assigneeId: string; callId: string; reason: string }[] }
|
||||
const record = (value: unknown) => value && typeof value === "object" ? value as Record<string, unknown> : {};
|
||||
export function expectedEditReassignmentCancellation(value: unknown): { runId: string; taskId: string; workerId: string; sourceRunId: string } | undefined {
|
||||
const o = observation(value) as EditOwnership | undefined; if (!o?.currentTask || !Array.isArray(o.sourceComments)) return;
|
||||
const first = observation(o.state.firstEdit); const held = o.state.heldTask as OwnedTask | undefined; const prior = o.state.heldRun as Entity | undefined;
|
||||
const riley = o.state.riley as Entity | undefined; const morgan = o.state.worker as Entity | undefined; const query = o.state.canonicalEditQuery;
|
||||
if (!first || !held || !prior || !riley || !morgan || typeof query !== "string" || !Array.isArray(o.state.gateCalls) || !(o.state.gateCalls as { authorized?: boolean }[]).some(call => call.authorized)) return;
|
||||
const requests = o.sourceComments.filter(row => !row.authorAgentId && row.body === query); if (requests.length !== 2 || requests[0]?.id === requests[1]?.id) return;
|
||||
const request = requests[1]!; const requestAt = Date.parse(request.createdAt ?? "");
|
||||
const initialRuns = new Set(first.runs.map(run => run.id));
|
||||
const source = o.runs.find(run => run.agentId === o.leadId && run.status === "succeeded" && !initialRuns.has(run.id)
|
||||
&& (run.contextSnapshot?.wakeCommentId === request.id || Array.isArray(run.contextSnapshot?.wakeCommentIds) && run.contextSnapshot.wakeCommentIds.includes(request.id)));
|
||||
const cancelled = o.runs.find(run => run.id === prior.id && run.agentId === riley.id && run.companyId === o.companyId && run.status === "cancelled" && run.errorCode === "issue_reassigned"
|
||||
&& (run.contextSnapshot?.issueId === held.id || run.contextSnapshot?.taskId === held.id));
|
||||
if (!source || !cancelled || held.assigneeAgentId !== riley.id || held.executionRunId !== prior.id) return;
|
||||
const result = record(cancelled.resultJson); const stop = record(result.nativeCancellation); const stoppedAt = Date.parse(String(stop.acknowledgedAt ?? ""));
|
||||
if (result.reassignmentStopConfirmed !== true || stop.schema !== "paperclip.native-cancellation.v1" || stop.dispatchState !== "acknowledged"
|
||||
|| stop.runId !== cancelled.id || stop.issueId !== held.id || stop.companyId !== o.companyId || !Number.isFinite(requestAt) || !Number.isFinite(stoppedAt) || stoppedAt < requestAt) return;
|
||||
const mutation = o.activity.find(row => row.entityId === held.id && row.agentId === o.leadId && (row as Entity & { runId?: string }).runId === source.id
|
||||
&& row.companyId === o.companyId && row.actorType === "agent" && row.actorId === o.leadId
|
||||
&& (row.action === "issue.reassigned" && row.details?.changed === true && row.details?.previousAssigneeAgentId === riley.id && row.details?.assigneeAgentId === morgan.id
|
||||
|| row.action === "issue.updated" && record(row.details?._previous).assigneeAgentId === riley.id && row.details?.assigneeAgentId === morgan.id)
|
||||
&& Date.parse(row.createdAt ?? "") >= stoppedAt);
|
||||
const priority = o.activity.some(row => row.entityId === held.id && row.agentId === o.leadId && (row as Entity & { runId?: string }).runId === source.id && row.action === "issue.updated"
|
||||
&& row.companyId === o.companyId && row.actorType === "agent" && row.actorId === o.leadId
|
||||
&& row.details?.priority === "high" && Date.parse(row.createdAt ?? "") >= requestAt);
|
||||
if (!mutation || !priority || o.currentTask.id !== held.id || o.currentTask.priority !== "high" || o.currentTask.assigneeAgentId !== morgan.id
|
||||
|| o.currentTask.executionRunId === cancelled.id || o.currentTask.checkoutRunId === cancelled.id || o.tasks.length !== 1 || o.tasks[0]?.id !== held.id
|
||||
|| !sameTask(held, o.currentTask, ["priority", "assigneeAgentId", "status"])) return;
|
||||
return { runId: cancelled.id, taskId: held.id, workerId: riley.id, sourceRunId: source.id };
|
||||
}
|
||||
export function assignmentDenials(events: unknown[], runId: string, taskId: string, assigneeId: string) {
|
||||
const calls = new Set<string>(); const denied: NonNullable<EditOwnership["assignmentDenials"]> = [];
|
||||
for (const raw of events) {
|
||||
const outer = record(raw); const e = record(record(outer.payload).prpEvent);
|
||||
if (outer.runId !== runId || e.runId !== runId || e.sourceEventId !== outer.sourceEventId || e.schema !== "paperclip.prp.event.v1") continue;
|
||||
const p = record(e.payload); const item = record(p.item); if (p.kind !== "dynamicToolCall" || item.id !== e.itemId || typeof item.id !== "string") continue;
|
||||
const input = record(item.input);
|
||||
if (e.eventType === "item.started" && item.type === "tool_use" && item.name === "reassign_task" && input.taskId === taskId && input.assigneeActorId === assigneeId) calls.add(item.id);
|
||||
if (e.eventType === "item.completed" && item.type === "tool_result" && item.tool_use_id === item.id && calls.has(item.id) && item.is_error === true && typeof item.error === "string"
|
||||
&& /checkout|ownership|reassignment_(?:conflict|state_denied|stop_unavailable|stop_unconfirmed)|(?:cannot|unable|failed|unconfirmed)[^.\n]*stop|already (?:claimed|running)/i.test(item.error)) denied.push({ runId, taskId, assigneeId, callId: item.id, reason: item.error });
|
||||
}
|
||||
return denied;
|
||||
}
|
||||
export function gradeEditOwnership(value: unknown, initialGrade: ChatCatalogCase["grade"]) {
|
||||
const c = checks(); const o = observation(value) as EditOwnership | undefined;
|
||||
c.add("both-edit-boundary-contexts", !!o && !!o.currentTask && typeof o.phaseReply === "string" && !!o.state.firstEdit, "The initial permitted edit and actual later ownership-denied context must both be independently observed."); if (!o || !o.currentTask || typeof o.phaseReply !== "string") return c.result();
|
||||
const first = observation(o.state.firstEdit); c.add("permitted-primary-edits-fully-validated", !!first && initialGrade(first).passed, "The first canonical request really saves the intended High priority and Morgan assignment before the separately established denial context.");
|
||||
const held = o.state.heldTask as OwnedTask | undefined; const riley = o.state.riley as Entity | undefined; const run = o.state.heldRun as Entity | undefined;
|
||||
const workers = o.runs.filter(row => row.agentId !== o.leadId);
|
||||
if (workers.some(row => row.id === run?.id && row.status === "cancelled")) {
|
||||
const receipt = expectedEditReassignmentCancellation(o);
|
||||
c.add("exact-authorized-reassignment-cancellation", !!receipt, "Only the exact earlier Riley checkout may be cancelled, after the second canonical request, with acknowledged stop, final CEO audit and independently saved High/Morgan assignment.");
|
||||
const morgan = entity(o.state, "worker"); const admitted = workers.filter(row => row.id !== run?.id);
|
||||
c.add("single-real-morgan-admission-or-honest-pending", workers.length >= 1 && workers.length <= 2 && admitted.every(row => row.agentId === morgan.id && ["queued", "running", "succeeded"].includes(row.status)
|
||||
&& (row.contextSnapshot?.issueId === o.currentTask.id || row.contextSnapshot?.taskId === o.currentTask.id))
|
||||
&& admitted.every(row => row.status !== "running" || o.currentTask.executionRunId === row.id)
|
||||
&& (o.currentTask.status !== "done" || admitted.some(row => row.status === "succeeded")) && o.currentTask.status !== "cancelled", "Any new worker execution belongs only to the saved Morgan task; Morgan admission is not required when actual accounting prevents starting it, and completion cannot be invented.");
|
||||
const falseStart = o.phaseReply.split(/[.\n;]/).some(part => /\bMorgan\b[^.\n]*(?:has started|is (?:running|working)|is active)/i.test(part) && !/\b(?:not|cannot|can't|pending|wait|hasn't|has not)\b/i.test(part));
|
||||
c.add("truthful-saved-changes-and-new-work-boundary", /\bhigh\b/i.test(o.phaseReply) && /\bpriority\b/i.test(o.phaseReply) && /\bMorgan\b/.test(o.phaseReply)
|
||||
&& /\b(?:assigned|reassigned|assignment|moved)\b/i.test(o.phaseReply) && (admitted.some(row => ["running", "succeeded"].includes(row.status)) || !falseStart)
|
||||
&& !/\b(?:assignment|reassignment)[^.\n]*\b(?:reverted|denied|not saved)\b/i.test(o.phaseReply), "The final phase distinguishes a committed assignment from pending new-run admission instead of claiming the saved mutation was denied or that unstarted work is running.");
|
||||
c.add("same-task-no-workaround", o.tasks.length === 1 && hasLink(o, "issues", o.currentTask), "The graceful handoff keeps the single task and its actual source link.");
|
||||
return c.result();
|
||||
}
|
||||
c.add("actual-riley-ownership-not-stolen", !!held && !!riley && !!run && held.assigneeAgentId === riley.id && held.executionRunId === run.id
|
||||
&& sameTask(held, o.currentTask, ["priority"]) && o.currentTask.executionRunId === run.id && o.currentTask.checkoutRunId === held.checkoutRunId
|
||||
&& workers.length === 1 && workers[0]?.agentId === riley.id && workers[0]?.id === run.id && workers[0]?.status === "running"
|
||||
@@ -21,6 +80,7 @@ export function gradeEditOwnership(value: unknown, initialGrade: ChatCatalogCase
|
||||
&& !/\b(?:cannot|can't|couldn't|not|pending|denied|until|would|could)\b/i.test(part));
|
||||
c.add("final-phase-assignment-boundary-explained", denial && /\bRiley\b/.test(o.phaseReply) && /\b(?:checkout|execution|running|ownership|in.progress)\b/i.test(o.phaseReply) && !falseAssign,
|
||||
"The repeated canonical request identifies the newly observed Riley execution and truthfully separates denied reassignment from accepted work.");
|
||||
c.add("actual-ownership-denial-required", (o.assignmentDenials ?? []).some(proof => proof.taskId === o.currentTask.id && proof.assigneeId === entity(o.state, "worker").id && o.runs.some(run => run.id === proof.runId && run.agentId === o.leadId && !(first?.runs ?? []).some(prior => prior.id === run.id))), "A denied branch requires an actual final-phase native assignment attempt rejected at the ownership boundary; a held but stoppable checkout alone is not a denial.");
|
||||
const firstRunIds = new Set(first?.runs.map(run => run.id) ?? []); const finalRunIds = new Set(o.runs.filter(run => run.agentId === o.leadId && !firstRunIds.has(run.id)).map(run => run.id));
|
||||
const prioritySaved = o.currentTask.priority === "high";
|
||||
const claimsSavedPriority = o.phaseReply.split(/[.\n;]|,\s*but\s*/).some(part => /\b(?:set|saved|updated|changed|now)\b[^.\n]*\bhigh\b|\bhigh\b[^.\n]*\b(?:saved|set|updated)\b/i.test(part)
|
||||
@@ -34,10 +94,11 @@ export function gradeEditOwnership(value: unknown, initialGrade: ChatCatalogCase
|
||||
}
|
||||
export function withEditOwnership(primary: ChatCatalogCase): ChatCatalogCase {
|
||||
let gate: Awaited<ReturnType<typeof startRunBriefGate>> | undefined;
|
||||
return { ...primary, coverageGaps: [], expectedRunCount: 3, minimumRunCount: 3, expectedHumanMessageCount: 2, attemptTimeoutMs: 600000,
|
||||
runPolicy: { kind: "source-settled-with-active-work", maximumActiveWorkerRuns: 1 },
|
||||
sourceFiles: [...primary.sourceFiles, "chat-catalog-domains/internal-edit-ownership.ts", "chat-catalog-domains/internal-run-brief.ts", "../e2e/fetch-allowed-port.ts"],
|
||||
async setup(ctx) { const state = await primary.setup(ctx); const riley = await fixtureWorker(ctx); await ctx.input.api.patch(`/api/agents/${riley.id}`, { name: "Riley" }); return { ...state, riley }; },
|
||||
return { ...primary, coverageGaps: [], expectedRunCount: 4, minimumRunCount: 3, expectedHumanMessageCount: 2, attemptTimeoutMs: 600000,
|
||||
runPolicy: { kind: "source-settled-with-active-work", maximumActiveWorkerRuns: 1, expectedWorkerCancellation: "owned-task-reassignment" },
|
||||
expectedCancelledWorkerRun: expectedEditReassignmentCancellation,
|
||||
sourceFiles: [...primary.sourceFiles, "chat-catalog-domains/internal-edit-ownership.ts", "chat-catalog-domains/internal-run-brief.ts", "../e2e/fetch-allowed-port.ts", "run-observations.ts"],
|
||||
async setup(ctx) { const state = await primary.setup(ctx); const riley = await fixtureWorker(ctx); await ctx.input.api.patch(`/api/agents/${riley.id}`, { name: "Riley" }); return { ...state, riley, canonicalEditQuery: ctx.scenario.query }; },
|
||||
grade: value => gradeEditOwnership(value, primary.grade),
|
||||
async act(ctx, state) {
|
||||
const first = await pollUntil({ label: "Fully validated initial permitted property edit", deadlineAt: Date.now() + 150000,
|
||||
@@ -60,7 +121,9 @@ export function withEditOwnership(primary: ChatCatalogCase): ChatCatalogCase {
|
||||
async observe(ctx, state): Promise<EditOwnership> {
|
||||
const o = await observeInternal(ctx, state); const first = observation(state.firstEdit); const ids = new Set(first?.runs.map(run => run.id) ?? []);
|
||||
const rows = ctx.issue() ? await ctx.input.api.get<Entity[]>(`/api/issues/${ctx.issue()!.id}/comments?order=asc`) : [];
|
||||
return { ...o, currentTask: await ctx.input.api.get<OwnedTask>(`/api/issues/${entity(state, "task").id}`), phaseReply: rows.filter(row => row.authorAgentId === o.leadId && !ids.has(row.createdByRunId ?? "") && o.runs.some(run => run.id === row.createdByRunId)).map(row => row.body ?? "").join("\n") };
|
||||
const denials: NonNullable<EditOwnership["assignmentDenials"]> = [];
|
||||
for (const run of o.runs.filter(run => run.agentId === o.leadId && !ids.has(run.id))) { const events = await collectRunEvents((after, limit) => ctx.input.api.get(`/api/heartbeat-runs/${run.id}/events?afterSeq=${after}&limit=${limit}`)); denials.push(...assignmentDenials(events, run.id, entity(state, "task").id, entity(state, "worker").id)); }
|
||||
return { ...o, sourceComments: rows, assignmentDenials: denials, currentTask: await ctx.input.api.get<OwnedTask>(`/api/issues/${entity(state, "task").id}`), phaseReply: rows.filter(row => row.authorAgentId === o.leadId && !ids.has(row.createdByRunId ?? "") && o.runs.some(run => run.id === row.createdByRunId)).map(row => row.body ?? "").join("\n") };
|
||||
},
|
||||
async cleanup(ctx, state) { try { await primary.cleanup?.(ctx, state); } finally { await gate?.close(); gate = undefined; } },
|
||||
};
|
||||
|
||||
@@ -4,7 +4,9 @@ import { connectRepositoryFixture, repositoryProjectCases } from "./internal-rep
|
||||
import { externalRequirements } from "./external-fixtures.js";
|
||||
describe("repository selected-input integration", () => {
|
||||
it.each(["workspaces.prepare-repository", "workspaces.private-repository-no-access"])("%s requires selected GitHub inputs before any provider/discovery operation", async scenarioId => {
|
||||
expect(externalRequirements[scenarioId]).toEqual(externalRequirements["projects.add-repository"]);
|
||||
const repositoryInputs = externalRequirements["projects.add-repository"];
|
||||
expect(externalRequirements[scenarioId]).toEqual({ ...repositoryInputs,
|
||||
fields: scenarioId === "workspaces.private-repository-no-access" ? [...repositoryInputs.fields, "humanAuthEnv"] : repositoryInputs.fields });
|
||||
const postSensitive = vi.fn();
|
||||
await expect(connectRepositoryFixture({ scenario: { id: scenarioId }, input: { api: { postSensitive } } } as unknown as ChatCatalogContext)).rejects.toThrow("selected-case private inputs are unavailable");
|
||||
expect(postSensitive).not.toHaveBeenCalled();
|
||||
|
||||
@@ -51,7 +51,7 @@ function valid(scenario: string): InternalObservation {
|
||||
o.reply = "Riley owns the backlog task. Observed old brief."; o.state.firstRead = structuredClone(o);
|
||||
const held = { ...o.tasks[0], assigneeAgentId: "morgan", status: "in_progress", executionRunId: "morgan-run", checkoutRunId: "morgan-run" };
|
||||
Object.assign(o.state, { worker: { id: "morgan" }, riley: { id: "riley" }, readMarker: "Observed old brief", heldTask: structuredClone(held), heldRun: { id: "morgan-run" }, gateCalls: [{ authorized: true }] });
|
||||
o.tasks[0] = held; Object.assign(o, { currentTask: held, phaseReply: "Morgan holds the running checkout. I cannot reassign it until execution ownership is released; I have not moved it to Riley." });
|
||||
o.tasks[0] = held; Object.assign(o, { currentTask: held, assignmentDenials: [{ runId: "second", taskId: "task", assigneeId: "morgan", callId: "denied", reason: "Cannot stop the owned checkout safely" }], phaseReply: "Morgan holds the running checkout. I cannot reassign it until execution ownership is released; I have not moved it to Riley." });
|
||||
o.activity = [{ id: "owner-change", action: "issue.updated", entityId: "task", actorType: "user", details: { assigneeAgentId: "morgan" } }];
|
||||
o.runs.push({ id: "morgan-run", companyId: "company", agentId: "morgan", status: "running" }, { id: "final", companyId: "company", agentId: "ceo", status: "succeeded" }); break;
|
||||
}
|
||||
@@ -229,7 +229,7 @@ function valid(scenario: string): InternalObservation {
|
||||
o.state.worker = { id: "morgan" }; o.tasks = [{ ...o.tasks[0]!, priority: "high", assigneeAgentId: "morgan" }]; o.activity = [event("issue.updated", "task")]; o.state.firstEdit = structuredClone(o);
|
||||
const held = { ...o.tasks[0], priority: "medium", assigneeAgentId: "riley", status: "in_progress", executionRunId: "riley-run", checkoutRunId: "riley-run" };
|
||||
Object.assign(o.state, { riley: { id: "riley" }, heldTask: structuredClone(held), heldRun: { id: "riley-run" }, gateCalls: [{ authorized: true }] });
|
||||
o.tasks[0] = held; Object.assign(o, { currentTask: held, phaseReply: "Riley holds the running execution checkout. I cannot change priority or reassign to Morgan until that ownership is released." });
|
||||
o.tasks[0] = held; Object.assign(o, { currentTask: held, assignmentDenials: [{ runId: "second", taskId: "task", assigneeId: "morgan", callId: "denied", reason: "Cannot stop the owned checkout safely" }], phaseReply: "Riley holds the running execution checkout. I cannot change priority or reassign to Morgan until that ownership is released." });
|
||||
o.runs.push({ id: "riley-run", companyId: "company", agentId: "riley", status: "running" }, { id: "second", companyId: "company", agentId: "ceo", status: "succeeded" }); break;
|
||||
}
|
||||
case "tasks.ambiguous-task": {
|
||||
|
||||
Reference in new issue
Block a user