mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 06:16:06 -10:00
fix(evals): correct stale chat-catalog setup and UI expectations
Repairs setup/observation defects found in TestOps launch 1474 evidence,
on top of candidate b59e295c7f:
- instance-settings / chat-disabled: match the shipped Agent Chat
disabled copy ("disabled for this instance … enable it in
Experimental settings") instead of text the UI never renders.
- interrupt-with-direction, claude-account, cloud-choice: replace the
dead [data-interaction-id] selector (no component renders it) with the
real hooks: inline #interaction-<id> or the docked composer takeover.
The old fallback clicked the timeline chip, not Approve.
- registered artifacts: identify a deliverable by its run-attributed
attachment filename; the work-product title is a display label.
Run, status, duplicate, filename and byte checks are unchanged.
- multiple-handoffs: deliver the prerequisite logo as logo.html with an
inline <svg>; image/svg+xml is deliberately not an allowed upload type.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b59e295c7f
commit
4a375ecf21
9 files changed
+51
-22
No files matched your search
@@ -50,7 +50,7 @@ const settings = privilegedDefinition("instance-operations.instance-settings", "
|
||||
await expect.poll(async () => (await ctx.input.api.get<{ enableAgentChat: boolean }>(endpoint, { timeout: remaining() })).enableAgentChat, { timeout: remaining() }).toBe(false);
|
||||
state.savedDisabled = true;
|
||||
await page.goto(source, { waitUntil: "domcontentloaded", timeout: remaining() });
|
||||
await expect(page.getByText(/Agent Chat is disabled(?:\.| in Experimental settings)/).first()).toBeVisible({ timeout: remaining() });
|
||||
await expect(page.getByText(/Agent Chat is disabled(?:\.| in Experimental settings| for this instance)/).first()).toBeVisible({ timeout: remaining() });
|
||||
state.disabledRouteClear = true;
|
||||
await page.goto(settingsUrl, { waitUntil: "domcontentloaded", timeout: remaining() });
|
||||
await expect(toggle).toHaveAttribute("aria-checked", "false", { timeout: remaining() }); await toggle.click({ timeout: remaining() });
|
||||
|
||||
@@ -62,10 +62,10 @@ export const runtimeDisabledChatCases: readonly IdentityBoundaryCase[] = [{
|
||||
person.bindApiSession(ctx.api); // Capture verification uses the same person through private Node fetch.
|
||||
const page = ctx.page;
|
||||
await page.goto(`/${fixtures.company.issuePrefix}/chats/${fixtures.agent.id}`, { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByText(/Agent Chat is disabled\. Enable it in Experimental settings/)).toBeVisible();
|
||||
await expect(page.getByText(/Agent Chat is disabled for this instance\. An administrator can enable it in Experimental settings/)).toBeVisible();
|
||||
await expect(page.getByTestId("task-chat-composer-input")).toHaveCount(0);
|
||||
const unavailableVisible = await page.getByText(/Agent Chat is disabled/).isVisible();
|
||||
const supportedOperatorStepVisible = await page.getByText(/Enable it in Experimental settings/).isVisible();
|
||||
const supportedOperatorStepVisible = await page.getByText(/can enable it in Experimental settings/).isVisible();
|
||||
ctx.observeResources({ companyId: fixtures.company.id, agentId: fixtures.agent.id, runIds: [] });
|
||||
await ctx.capture("disabled-agent-chat", "Morgan Chat is genuinely unavailable; canonical intent remains unsent", "runtime-disabled-agent-chat.png");
|
||||
const key = await api.postSensitive<{ id: string; token: string }>(`/api/agents/${fixtures.agent.id}/keys`, { name: "disabled-chat-permission-probe" });
|
||||
|
||||
@@ -12,7 +12,7 @@ import { checks, type Entity } from "./internal-support.js";
|
||||
import { waitForReply } from "./identity-support.js";
|
||||
import { matchesClaudeFixtureTarget, privateClaudeOperation, type ClaudePrivateReceipt } from "./runtime-claude-private.js";
|
||||
import { selectedRuntimeInputs, runtimePrivateSpec } from "./runtime-private.js";
|
||||
import { openRuntimeAgentProfile, observeRuntime, runtimeDefinition, runtimeObservation, type RuntimeAgent } from "./runtime-support.js";
|
||||
import { clickInteractionAction, openRuntimeAgentProfile, observeRuntime, runtimeDefinition, runtimeObservation, type RuntimeAgent } from "./runtime-support.js";
|
||||
const continueHire = "The Anthropic account is connected now. Continue the same Morgan frontend engineer hire using Claude Code.";
|
||||
export function supportedClaudeSetupLink(href: string, origin: string, prefix: string) {
|
||||
try { const url = new URL(href, origin); return url.origin === origin && url.pathname.startsWith(`/${prefix}/`) && (url.pathname.endsWith("/company/settings/connections") || url.pathname.endsWith("/apps/connect") && ["anthropic", "claude"].includes(url.searchParams.get("source") ?? url.searchParams.get("provider") ?? "")); } catch { return false; }
|
||||
@@ -69,7 +69,7 @@ export const runtimeClaudeAccountCases = [runtimeDefinition("agents.hire-needs-a
|
||||
if (target) await ctx.input.page.locator(`[id^="comment-"] a[href=${JSON.stringify(target)}]`).first().click();
|
||||
else {
|
||||
if (!request) throw new Error("The canonical waiting response exposed neither a supported account setup link nor an actual addressed Anthropic setup request");
|
||||
await ctx.input.page.locator(`[data-interaction-id=${JSON.stringify(request.id)}]`).getByRole("button", { name: /Connect|Set up|Continue/i }).first().click();
|
||||
await clickInteractionAction(ctx.input.page, request, /Connect|Set up|Continue/i);
|
||||
}
|
||||
await expect(ctx.input.page.getByRole("heading", { name: /connections|Connect.*Anthropic|Connect a model provider/i }).first()).toBeVisible(); state.supportedSetupVisible = true;
|
||||
const selected = selectedRuntimeInputs(ctx, "agents.hire-needs-account");
|
||||
@@ -92,7 +92,7 @@ export const runtimeClaudeAccountCases = [runtimeDefinition("agents.hire-needs-a
|
||||
state.setupRequestReconciled = completedClaudeSetupMatches({ request: resolved, sourceId: ctx.issue()!.id, sourceRunId: firstRun.id, companyId: before.companyId, humanId: session.user.id, connectionId: String(state.connectionId) });
|
||||
if (!state.setupRequestReconciled) throw new Error("The actual addressed Anthropic setup request did not reconcile against established human account access");
|
||||
await ctx.input.page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(ctx.input.page.locator(`[data-interaction-id=${JSON.stringify(String(state.setupRequestId))}]`)).toContainText(/connected|ready|complete/i);
|
||||
await expect(ctx.input.page.locator(`[id=${JSON.stringify(`interaction-${String(state.setupRequestId)}`)}]`)).toContainText(/connected|ready|complete/i);
|
||||
await ctx.refreshIssue();
|
||||
state.setupAutoWakeExpected = ["in_progress", "in_review"].includes(ctx.issue()!.status) && ctx.issue()!.assigneeAgentId === resolved!.payload.requestingAgentId;
|
||||
if (state.setupAutoWakeExpected) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IssueThreadInteraction } from "../../../packages/shared/src/types/
|
||||
import { expect } from "@playwright/test";
|
||||
import { checks } from "./internal-support.js";
|
||||
import { waitForReply } from "./identity-support.js";
|
||||
import { runtimeDefinition, runtimeObservation, observeRuntime } from "./runtime-support.js";
|
||||
import { interactionSurface, runtimeDefinition, runtimeObservation, observeRuntime } from "./runtime-support.js";
|
||||
import { runtimePrivateSpec, selectedRuntimeInputs } from "./runtime-private.js";
|
||||
import { createOwnedDaytonaTarget, deleteOwnedDaytonaTarget, type OwnedDaytonaTarget } from "./runtime-daytona-target.js";
|
||||
import { privateClaudeOperation } from "./runtime-claude-private.js";
|
||||
@@ -48,7 +48,7 @@ export const runtimeCloudChoiceCases = [runtimeDefinition("agents.ambiguous-clou
|
||||
const runs = await ctx.allRuns();
|
||||
if (questions.some(row => row.sourceRunId !== runs[0]?.id || row.issueId !== source.id)) throw new Error("Clarification card is not attributed to the actual canonical source run");
|
||||
await expect(ctx.input.page.getByTestId("task-chat-unanswered-question").first()).toBeVisible();
|
||||
state.visibleChoiceText = await ctx.input.page.locator(`[data-interaction-id=${JSON.stringify(questions[0]!.id)}]`).innerText();
|
||||
state.visibleChoiceText = await interactionSurface(ctx.input.page, questions[0]!).first().innerText();
|
||||
}
|
||||
else await expect(ctx.input.page.getByTestId("task-chat-composer-input")).toBeVisible();
|
||||
state.questionVisible = questions.length > 0 || /\?|which|do you mean/i.test((await observeRuntime(ctx, state)).reply); state.sourceWaiting = source.conversationState === "waiting";
|
||||
|
||||
@@ -10,6 +10,7 @@ import { readRegisteredArtifacts } from "../registered-artifact.js";
|
||||
import { createIssueThreadInteractionSchema } from "../../../packages/shared/src/validators/issue.js";
|
||||
import { checks, type Entity } from "./internal-support.js";
|
||||
import { busyRuntimeWorker, releaseRuntimeWorker } from "./runtime-busy-worker.js";
|
||||
import { clickInteractionAction } from "./runtime-support.js";
|
||||
import { runtimeQueueCases } from "./runtime-queue.js";
|
||||
import { runtimeDefinition, runtimeObservation, observeRuntime } from "./runtime-support.js";
|
||||
export function gradeInterruption(value: unknown) {
|
||||
@@ -44,11 +45,8 @@ export const runtimeInterruptionCases = [runtimeDefinition("steering.interrupt-w
|
||||
payload: { version: 1, prompt: "Use this saved simpler layout on the next eligible attempt?", supersedeOnUserComment: false,
|
||||
target: { type: "issue_document", issueId: task.id, documentId: plan.id, key: "plan", revisionId: plan.latestRevisionId, revisionNumber: plan.latestRevisionNumber } } }));
|
||||
await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}/issues/${task.identifier ?? task.id}`, { waitUntil: "domcontentloaded" });
|
||||
const card = ctx.input.page.locator(`[data-interaction-id=${JSON.stringify(confirmation.id)}]`);
|
||||
const accept = ctx.input.page.waitForResponse(r => r.request().method() === "POST" && new URL(r.url()).pathname === `/api/issues/${task.id}/interactions/${confirmation.id}/accept`);
|
||||
const acceptButton = card.getByRole("button", { name: /Approve|Accept/i });
|
||||
if (await acceptButton.count()) await acceptButton.first().click();
|
||||
else await ctx.input.page.getByRole("button", { name: /Approve|Accept/i }).first().click();
|
||||
await clickInteractionAction(ctx.input.page, confirmation, "Approve");
|
||||
if ((await accept).status() !== 200) throw new Error("Actual human simpler-plan approval did not succeed");
|
||||
const session = await ctx.input.api.get<{ user: { id: string } }>("/api/auth/get-session");
|
||||
const approved = (await ctx.input.api.get<IssueThreadInteraction[]>(`/api/issues/${task.id}/interactions`)).find(row => row.id === confirmation.id)!;
|
||||
|
||||
@@ -23,7 +23,7 @@ export function gradeMultipleHandoffs(value: unknown) {
|
||||
c.add("logo-task-and-delivered-logo-unchanged", !!logoBefore && !!logo && sameTask(logoBefore, logo) && logo.status === "done" && logo.assigneeAgentId === (s.logoWorker as Entity).id
|
||||
&& before?.contentVerified === true && after?.contentVerified === true && before.attachmentId === after.attachmentId && before.sha256 === after.sha256
|
||||
&& before.content === after.content && after.content.includes(String(s.logoMarker)) && /<svg\b/i.test(after.content) && s.logoRevisionsUnchanged === true,
|
||||
"The separate real Logo task, SVG bytes, artifact identity, saved revisions and original worker attribution remain unchanged.");
|
||||
"The separate real Logo task, logo bytes, artifact identity, saved revisions and original worker attribution remain unchanged.");
|
||||
c.add("canonical-reply-honestly-identifies-affected-pending-work", /website/i.test(String(s.queryReply)) && /queue|pending|wait|next|forward|saved/i.test(String(s.queryReply))
|
||||
&& !/already (?:implemented|consumed)|headline (?:is|has been) (?:already )?(?:changed|updated)|I(?:'ve| have)? implemented/i.test(String(s.queryReply)),
|
||||
"The canonical reply identifies Website and accurately describes feedback saved behind active work rather than claiming it already executed.");
|
||||
@@ -39,16 +39,16 @@ export const runtimeMultipleHandoffCases = [runtimeDefinition("agent-chat.multip
|
||||
for (const scope of [{ scopeType: "company", scopeId: ctx.input.fixtures.company.id }, { scopeType: "agent", scopeId: logoWorker.id }])
|
||||
await ctx.input.api.post(`${root}/budgets/policies`, { ...scope, amount: 500, hardStopEnabled: true, isActive: true, unpricedUsagePolicy: "allow" });
|
||||
const logo = await ctx.input.api.post<Entity>(`${root}/issues`, { title: "Logo", status: "todo", assigneeAgentId: logoWorker.id,
|
||||
description: `Create an actual small SVG logo file logo.svg with a simple acorn/circle mark, visible text Acorn and the exact reference ${logoMarker} in its SVG metadata or text. Save a short non-plan task delivery document describing it and register the real SVG file as a downloadable artifact work product titled logo.svg. Then complete this same task. No other task, publication or website change is authorized.` });
|
||||
description: `Create an actual small logo file logo.html containing an inline SVG with a simple acorn/circle mark, visible text Acorn and the exact reference ${logoMarker} in its SVG metadata or text. Save a short non-plan task delivery document describing it and register the real logo.html file as a downloadable artifact work product titled logo.html. Then complete this same task. No other task, publication or website change is authorized.` });
|
||||
const complete = await pollUntil({ label: "Actual separate Logo work completed before the Website conversation", deadlineAt: Date.now() + 240_000,
|
||||
load: async () => ({ task: await ctx.input.api.get<Entity>(`/api/issues/${logo.id}`), runs: await ctx.allRuns() }),
|
||||
accept: v => v.task.status === "done" && v.runs.length === 1 && v.runs[0]?.agentId === logoWorker.id && v.runs[0].status === "succeeded" });
|
||||
const [artifact] = await readRegisteredArtifacts(ctx.input.api, logo.id, complete.runs[0]!.id, ["logo.svg"]);
|
||||
if (!artifact?.contentVerified || !artifact.content.includes(logoMarker) || !/<svg\b/i.test(artifact.content)) throw new Error("Completed Logo prerequisite lacks its actual native-created SVG");
|
||||
const [artifact] = await readRegisteredArtifacts(ctx.input.api, logo.id, complete.runs[0]!.id, ["logo.html"]);
|
||||
if (!artifact?.contentVerified || !artifact.content.includes(logoMarker) || !/<svg\b/i.test(artifact.content)) throw new Error("Completed Logo prerequisite lacks its actual native-created logo");
|
||||
const documents = await ctx.input.api.get<Entity[]>(`/api/issues/${logo.id}/documents`), revisions: Entity[] = [];
|
||||
for (const doc of documents) revisions.push(...await ctx.input.api.get<Entity[]>(`/api/issues/${logo.id}/documents/${encodeURIComponent(doc.key!)}/revisions`));
|
||||
const busy = await busyRuntimeWorker(ctx); await openStandingChat(ctx);
|
||||
const message = `Keep these two existing work references in this conversation: Website ${busy.task.identifier ?? busy.task.id} is assigned to Morgan and actively waiting for the brief; Logo ${logo.identifier ?? logo.id} was completed by Riley and its actual delivered SVG is ${new URL(`/api/attachments/${artifact.attachmentId}/content?download=1`, ctx.input.api.baseURL).href}. Acknowledge these separate references, without creating, reassigning, editing or starting any task. Keep the Logo deliverable unchanged.`;
|
||||
const message = `Keep these two existing work references in this conversation: Website ${busy.task.identifier ?? busy.task.id} is assigned to Morgan and actively waiting for the brief; Logo ${logo.identifier ?? logo.id} was completed by Riley and its actual delivered logo is ${new URL(`/api/attachments/${artifact.attachmentId}/content?download=1`, ctx.input.api.baseURL).href}. Acknowledge these separate references, without creating, reassigning, editing or starting any task. Keep the Logo deliverable unchanged.`;
|
||||
await sendChatMessage(ctx.input.page, message);
|
||||
const saved = await pollUntil({ label: "Real source history retains separate active Website and completed Logo", deadlineAt: Date.now() + 150_000,
|
||||
load: async () => { await ctx.refreshIssue(); return { source: ctx.issue(), runs: await ctx.allRuns() }; },
|
||||
@@ -85,7 +85,7 @@ export const runtimeMultipleHandoffCases = [runtimeDefinition("agent-chat.multip
|
||||
async observe(ctx, state) {
|
||||
const o = await observeRuntime(ctx, state), logo = state.logoBefore as Entity;
|
||||
state.logoAfter = await ctx.input.api.get<Entity>(`/api/issues/${logo.id}`);
|
||||
const [artifact] = await readRegisteredArtifacts(ctx.input.api, logo.id, String(state.logoRunId), ["logo.svg"]); state.logoArtifactAfter = artifact;
|
||||
const [artifact] = await readRegisteredArtifacts(ctx.input.api, logo.id, String(state.logoRunId), ["logo.html"]); state.logoArtifactAfter = artifact;
|
||||
const documents = await ctx.input.api.get<Entity[]>(`/api/issues/${logo.id}/documents`), revisions: Entity[] = [];
|
||||
for (const doc of documents) revisions.push(...await ctx.input.api.get<Entity[]>(`/api/issues/${logo.id}/documents/${encodeURIComponent(doc.key!)}/revisions`));
|
||||
state.logoRevisionsUnchanged = JSON.stringify(documents.map(doc => [doc.id, doc.latestRevisionId]).sort()) === JSON.stringify((state.logoDocumentsBefore as Entity[]).map(doc => [doc.id, doc.latestRevisionId]).sort())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { pollUntil } from "../api.js";
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { codexSubscriptionAgentConfig } from "../codex-subscription-fixture.js";
|
||||
@@ -72,6 +72,24 @@ export async function verifyRuntimeResult(ctx: ChatCatalogContext, state: Record
|
||||
await ctx.input.evidence("chat-catalog-runtime-profile-opened.json", { agentId: target.id, companyId: target.companyId, url: ctx.input.page.url() });
|
||||
await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" });
|
||||
}
|
||||
/**
|
||||
* A pending interaction renders inline in the thread as `#interaction-<id>`,
|
||||
* or docked in the composer takeover, which carries its title but no id.
|
||||
*/
|
||||
export function interactionSurface(page: Page, interaction: { id: string; title?: string | null }) {
|
||||
const inline = page.locator(`[id=${JSON.stringify(`interaction-${interaction.id}`)}]`);
|
||||
return interaction.title
|
||||
? inline.or(page.getByTestId("task-chat-composer-takeover").filter({ hasText: interaction.title }))
|
||||
: inline;
|
||||
}
|
||||
/** Open a pending request from its timeline chip when it is not already docked, then use its own action. */
|
||||
export async function clickInteractionAction(page: Page, interaction: { id: string; title?: string | null }, name: string | RegExp) {
|
||||
const docked = page.getByTestId("task-chat-composer-takeover").filter({ hasText: interaction.title ?? "" });
|
||||
const action = (interaction.title ? docked : interactionSurface(page, interaction)).getByRole("button", typeof name === "string" ? { name, exact: true } : { name });
|
||||
await expect(interactionSurface(page, interaction).first()).toBeVisible();
|
||||
if (!(await action.first().isVisible())) await page.locator(`[id=${JSON.stringify(`interaction-${interaction.id}`)}]`).click();
|
||||
await action.first().click();
|
||||
}
|
||||
export function runtimeDefinition(scenarioId: string, setup: ChatCatalogCase["setup"], grade: ChatCatalogCase["grade"], extra: Partial<ChatCatalogCase> = {}): ChatCatalogCase {
|
||||
return { scenarioId, setup, grade, observe: observeRuntime, verifyResult: verifyRuntimeResult,
|
||||
oracleVersion: "paperclip.chat-catalog.runtime.v2", coverageGaps: [], expectedRunCount: 1,
|
||||
|
||||
@@ -28,7 +28,7 @@ it.each(["draft", "changes_requested", "merged", "closed", "failed", "archived",
|
||||
expect(download).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["unregistered", "duplicate", "old-run", "unattributed", "wrong-issue", "wrong-attachment-issue", "wrong-type", "wrong-title", "wrong-filename", "wrong-attachment-id", "negative-size", "excessive-size", "size-mismatch", "hash-mismatch", "download-failure"])("refuses %s output evidence even when ready for review", async reason => {
|
||||
it.each(["unregistered", "duplicate", "old-run", "unattributed", "wrong-issue", "wrong-attachment-issue", "wrong-type", "wrong-filename", "wrong-attachment-id", "negative-size", "excessive-size", "size-mismatch", "hash-mismatch", "download-failure"])("refuses %s output evidence even when ready for review", async reason => {
|
||||
const { api, product, attachment, get, download } = fixture();
|
||||
product.status = "ready_for_review";
|
||||
if (reason === "unregistered") get.mockImplementation(async (path: string) => path.endsWith("work-products") ? [] : [attachment]);
|
||||
@@ -38,7 +38,6 @@ it.each(["unregistered", "duplicate", "old-run", "unattributed", "wrong-issue",
|
||||
if (reason === "wrong-issue") product.issueId = "other-issue";
|
||||
if (reason === "wrong-attachment-issue") attachment.issueId = "other-issue";
|
||||
if (reason === "wrong-type") product.type = "document";
|
||||
if (reason === "wrong-title") product.title = "other.txt";
|
||||
if (reason === "wrong-filename") attachment.originalFilename = "other.txt";
|
||||
if (reason === "wrong-attachment-id") product.metadata.attachmentId = "other-attachment";
|
||||
if (reason === "negative-size") attachment.byteSize = -1;
|
||||
@@ -49,6 +48,16 @@ it.each(["unregistered", "duplicate", "old-run", "unattributed", "wrong-issue",
|
||||
await expect(readRegisteredArtifacts(api, "issue", "run", ["proof.txt"])).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("identifies the deliverable by its run-attributed file, not its display title", async () => {
|
||||
const { api, product, attachment, download } = fixture();
|
||||
product.title = "Acorn Notes proof (proof.txt)";
|
||||
expect((await readRegisteredArtifacts(api, "issue", "run", ["proof.txt"]))[0]?.contentVerified).toBe(true);
|
||||
attachment.originalFilename = "other.txt";
|
||||
download.mockClear();
|
||||
await expect(readRegisteredArtifacts(api, "issue", "run", ["proof.txt"])).rejects.toBeInstanceOf(RegisteredArtifactNotFoundError);
|
||||
expect(download).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not accept a filename or unchecked content as downloaded evidence", async () => {
|
||||
const matcher = { kind: "artifact_exact" as const, name: "proof.txt", expected: "verified\n" };
|
||||
expect((await evaluateMatcher(matcher, { artifacts: [{ name: "proof.txt", content: "verified\n" }] })).passed).toBe(false);
|
||||
|
||||
@@ -19,10 +19,14 @@ export async function readRegisteredArtifacts(api: RunnerApi, issueId: string, r
|
||||
api.get<IssueWorkProduct[]>(`/api/issues/${encodeURIComponent(issueId)}/work-products`),
|
||||
api.get<IssueAttachment[]>(`/api/issues/${encodeURIComponent(issueId)}/attachments`),
|
||||
]);
|
||||
// The file name identifies a deliverable; its work-product title is a
|
||||
// human-facing label the agent may choose, so it is not part of identity.
|
||||
const filenameOf = (product: IssueWorkProduct) =>
|
||||
attachments.find(attachment => attachment.id === product.metadata?.attachmentId)?.originalFilename;
|
||||
return Promise.all(names.map(async name => {
|
||||
const registered = products.filter(product => product.issueId === issueId &&
|
||||
product.createdByRunId === runId && product.type === "artifact" &&
|
||||
REGISTERED_ARTIFACT_STATUSES.has(product.status) && product.title === name);
|
||||
REGISTERED_ARTIFACT_STATUSES.has(product.status) && (product.title === name || filenameOf(product) === name));
|
||||
if (registered.length === 0) throw new RegisteredArtifactNotFoundError(name);
|
||||
if (registered.length !== 1) throw new Error(`Expected one registered artifact ${name} from the tested run; observed ${registered.length}`);
|
||||
const product = registered[0]!;
|
||||
|
||||
Reference in new issue
Block a user