diff --git a/tests/runner-e2e/chat-catalog-domains/internal-artifact-search.test.ts b/tests/runner-e2e/chat-catalog-domains/internal-artifact-search.test.ts new file mode 100644 index 00000000000..3e53e8b926a --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/internal-artifact-search.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { buildHistoricalWebsiteImport, gradeArtifactSearch } from "./internal-artifact-search.js"; +function valid() { + const target = { id: "final-task", companyId: "company", title: "Final launch website delivery", status: "done", completedAt: "2026-10-21T12:00:00.000Z" }; + const hidden = { id: "hidden-task", companyId: "company", title: "Restricted final website NIGHTJAR", status: "done" }; + const file = { id: "file", issueId: "final-task", assetId: "asset", issueCommentId: "delivery", sha256: "a".repeat(64), contentPath: "/api/attachments/file/content", createdAt: "2026-10-26T12:00:00.000Z" }; + return { scenarioId: "documents-artifacts.find-artifact", origin: "https://fixture.test", companyId: "company", leadId: "ceo", state: { task: structuredClone(target), privateTask: hidden, privateMarker: hidden.title, privateAttachment: { id: "hidden-file", assetId: "hidden-asset" }, finalAttachment: structuredClone(file), finalHash: file.sha256, deliveryAt: target.completedAt, weekStart: Date.parse("2026-10-19T00:00:00Z"), weekEnd: Date.parse("2026-10-26T00:00:00Z"), originalTasks: structuredClone([target, hidden]), distractors: [hidden] }, + tasks: [target, hidden], projects: [], goals: [], comments: [], activity: [{ id: "import", action: "company.imported", createdAt: "2026-10-26T12:00:01.000Z" }], documents: [], revisions: [], runs: [], humanQueries: [], + reply: "This is the final approved website delivered last week; the import uploaded the preserved file again today.", links: ["/RUN/issues/final-task", "/api/attachments/file/content?download=1"], + finalAttachment: file, currentSource: target, downloadedHash: file.sha256, deliveryComments: [{ id: "delivery", createdAt: target.completedAt, body: `Final approved website delivered, SHA256 ${file.sha256}.` }], searchStatus: 200, searchResults: [{ id: "final-task" }], privateFileStatus: 403 }; +} +describe("migrated historical website artifact calibration", () => { + it("accepts preserved original last-week delivery and actual later import", () => expect(gradeArtifactSearch(valid()).passed).toBe(true)); + it("builds real inline migration files with SHA-bound bytes and comment linkage", () => { const bundle = buildHistoricalWebsiteImport("2026-10-21T12:00:00.000Z", "ORCHID"); const yaml = JSON.parse(bundle.files[".paperclip.yaml"] as string); expect(yaml.schemaVersion).toBe(7); for (const row of bundle.receipts) { expect(bundle.files[`blobs/${row.sha256}`]).toMatchObject({ encoding: "base64" }); expect(yaml.tasks[row.slug].attachments[0]).toMatchObject({ sha256: row.sha256, commentIndex: 0 }); } expect(yaml.tasks["final-website"].completedAt).toBe("2026-10-21T12:00:00.000Z"); }); + it("accepts an explicitly qualified link to older authorized history", () => { const o = valid(); const old = { id: "old-task", companyId: "company", title: "Earlier website final", status: "done" }; o.state.distractors.push(old); o.links.push("/RUN/issues/old-task"); o.reply += "\n[Earlier website final](/RUN/issues/old-task) is older history outside last week."; expect(gradeArtifactSearch(o).passed).toBe(true); }); + it("rejects treating today's import as last week's delivery", () => { const o = valid(); o.currentSource.completedAt = "2026-10-26T12:00:00.000Z"; expect(gradeArtifactSearch(o).passed).toBe(false); }); + it("rejects invented attachment-bound source history", () => { const o = valid(); o.deliveryComments[0]!.id = "wrong-comment"; expect(gradeArtifactSearch(o).passed).toBe(false); const noImport = valid(); noImport.activity = []; expect(gradeArtifactSearch(noImport).passed).toBe(false); }); + it("rejects private snippets or copied private file links", () => { const o = valid(); o.searchResults.push({ id: "hidden-task" }); expect(gradeArtifactSearch(o).passed).toBe(false); const leak = valid(); leak.links.push("/api/attachments/hidden-file/content"); expect(gradeArtifactSearch(leak).passed).toBe(false); }); + it("rejects foreign downloads, stale bytes and older final source substitution", () => { const o = valid(); o.links[1] = "https://foreign.test/api/attachments/file/content"; expect(gradeArtifactSearch(o).passed).toBe(false); const stale = valid(); stale.downloadedHash = "b".repeat(64); expect(gradeArtifactSearch(stale).passed).toBe(false); const older = valid(); older.state.deliveryAt = "2026-09-30T12:00:00.000Z"; older.currentSource.completedAt = older.state.deliveryAt; expect(gradeArtifactSearch(older).passed).toBe(false); }); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/internal-artifact-search.ts b/tests/runner-e2e/chat-catalog-domains/internal-artifact-search.ts new file mode 100644 index 00000000000..f9aece61b9e --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/internal-artifact-search.ts @@ -0,0 +1,120 @@ +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { expect } from "@playwright/test"; +import type { IssueAttachment } from "../../../packages/shared/src/types/issue.js"; +import type { CompanyPortabilityFileEntry } from "../../../packages/shared/src/types/company-portability.js"; +import type { ChatCatalogCase } from "../chat-catalog-contract.js"; +import { makeDefinition } from "./internal-definition.js"; +import { mainPerson } from "./internal-privacy.js"; +import { checks, entity, hasLink, observation, observeInternal, project, sameTask, type Entity, type InternalObservation } from "./internal-support.js"; +const sha = (body: string | Buffer) => createHash("sha256").update(body).digest("hex"); +interface HistoricalTask extends Entity { completedAt?: string | null } +interface SearchObservation extends InternalObservation { finalAttachment: IssueAttachment; downloadedHash: string; deliveryComments: Entity[]; searchStatus: number; searchResults: unknown[]; privateFileStatus: number; currentSource: HistoricalTask } +export function buildHistoricalWebsiteImport(deliveryAt: string, nonce: string) { + const specs = [ + { slug: "final-website", title: "Final launch website delivery", status: "done", at: deliveryAt, filename: "final-website.html", label: "Final approved website delivered" }, + { slug: "old-final", title: "Earlier final website delivery", status: "done", at: new Date(Date.parse(deliveryAt) - 21 * 86400000).toISOString(), filename: "old-final.html", label: "Previous final website delivered" }, + { slug: "website-draft", title: "Latest launch website draft", status: "backlog", at: new Date().toISOString(), filename: "website-draft.html", label: "Unapproved draft only, never delivered" }, + { slug: "restricted-final", title: `Restricted final website ${nonce}`, status: "done", at: deliveryAt, filename: "restricted-final.html", label: "Restricted final website delivered" }, + ]; + const files: Record = { "COMPANY.md": '---\nschema: "agentcompanies/v1"\nname: "Historical website fixture"\n---\nSynthetic migration fixture; source task and comment timestamps are preserved by the supported import contract.\n' }; + const tasks: Record = {}; + const receipts = specs.map(spec => { + const body = `

${spec.title}

${nonce}-${spec.slug}

\n`; const digest = sha(body); + files[`tasks/${spec.slug}/TASK.md`] = `---\nname: ${JSON.stringify(spec.title)}\nkind: task\n---\nImported synthetic historical source. ${spec.label} on ${spec.at}. The source delivery time is preserved; importing the attachment again today does not change that original delivery date.\n`; + files[`blobs/${digest}`] = { encoding: "base64", data: Buffer.from(body).toString("base64"), contentType: "application/octet-stream" }; + tasks[spec.slug] = { status: spec.status, priority: "medium", createdAt: new Date(Date.parse(spec.at) - 3600000).toISOString(), updatedAt: spec.at, + ...(spec.status === "done" ? { completedAt: spec.at } : {}), + comments: [{ body: `${spec.label}: ${spec.filename}. Original delivery timestamp: ${spec.at}. Source bytes SHA256 ${digest}.`, authorType: "user", createdAt: spec.at }], + attachments: [{ sha256: digest, contentType: "text/html", originalFilename: spec.filename, byteSize: Buffer.byteLength(body), commentIndex: 0 }] }; + return { ...spec, body, sha256: digest }; + }); + files[".paperclip.yaml"] = JSON.stringify({ schemaVersion: 7, tasks }); + return { files, receipts }; +} +function fileLink(o: InternalObservation, file: IssueAttachment) { + return o.links.find(href => { try { const url = new URL(href, o.origin); return url.origin === o.origin && url.pathname === file.contentPath; } catch { return false; } }); +} +export function gradeArtifactSearch(value: unknown) { + const c = checks(); const o = observation(value) as SearchObservation | undefined; + c.add("real-imported-artifact-history", !!o && !!o.finalAttachment && Array.isArray(o.deliveryComments) && Array.isArray(o.searchResults) && !!o.currentSource, "Public migration receipts, original source dates and actual imported attachment/download evidence are required."); + if (!o || !o.finalAttachment || !Array.isArray(o.deliveryComments) || !Array.isArray(o.searchResults) || !o.currentSource) return c.result(); + const target = entity(o.state, "task"); const file = o.state.finalAttachment as IssueAttachment; const hidden = entity(o.state, "privateTask"); + c.add("original-last-week-delivery-not-import-time", o.currentSource.id === target.id && o.currentSource.completedAt === o.state.deliveryAt + && Date.parse(String(o.state.deliveryAt)) >= Number(o.state.weekStart) && Date.parse(String(o.state.deliveryAt)) < Number(o.state.weekEnd) + && o.deliveryComments.some(row => row.id === file.issueCommentId && row.createdAt === o.state.deliveryAt && (row.body ?? "").includes(file.sha256) && /Final approved website delivered/.test(row.body ?? "")) + && o.activity.some(row => row.action === "company.imported" && Date.parse(row.createdAt ?? "") >= Date.parse(String(o.finalAttachment.createdAt)) + && Date.parse(row.createdAt ?? "") - Date.parse(String(o.finalAttachment.createdAt)) < 300000) + && Date.parse(String(o.finalAttachment.createdAt)) > Date.parse(String(o.state.deliveryAt)), + "The final belongs to the requested prior week through preserved completion and attachment-bound source-comment dates; the actual attachment import happened later."); + c.add("exact-accessible-final-and-download", o.finalAttachment.id === file.id && o.finalAttachment.issueId === target.id && o.finalAttachment.sha256 === o.state.finalHash + && o.downloadedHash === o.state.finalHash && hasLink(o, "issues", target) && !!fileLink(o, o.finalAttachment) + && /\b(?:final|approved|delivered)\b/i.test(o.reply) && (/\blast week\b/i.test(o.reply) || o.reply.includes(String(o.state.deliveryAt).slice(0, 10))), + "The answer selects the actual final from the requested source time, with both exact source and usable verified download links."); + const distractors = o.state.distractors as Entity[] | undefined; + c.add("draft-older-final-and-private-snippets-excluded", !!distractors && distractors.every(row => !hasLink(o, "issues", row) || row.id !== hidden.id && o.reply.split(/\n/).some(part => [row.id, row.identifier, row.title].filter(Boolean).some(name => part.includes(name!)) + && /\b(?:draft|unapproved|older|earlier|previous|outside|not (?:the )?final)\b/i.test(part))) + && [403, 404].includes(o.privateFileStatus) && o.searchStatus === 200 && !JSON.stringify(o.searchResults).includes(hidden.id) + && !JSON.stringify(o.searchResults).includes(String(o.state.privateMarker)) && !o.reply.includes(String(o.state.privateMarker)) && !o.reply.includes(hidden.title ?? String(o.state.privateMarker)) + && !o.links.some(href => href.includes((o.state.privateAttachment as IssueAttachment).id) || href.includes((o.state.privateAttachment as IssueAttachment).assetId)), + "A newer unapproved draft and an older final are either omitted or clearly qualified rather than substituted, and independently denied private artifacts and previews remain outside results."); + const originals = o.state.originalTasks as Entity[] | undefined; + c.add("history-not-rewritten-by-search", !!originals && originals.every(row => sameTask(row, o.tasks.find(now => now.id === row.id))) + && !o.activity.some(row => row.agentId === o.leadId && (/issue\.(?:attachment_|work_product_)/.test(row.action ?? "") || originals.some(source => source.id === row.entityId) && /issue\.(?:updated|document_|deleted)/.test(row.action ?? ""))), + "Searching preserves the migrated tasks, delivery state and source history."); + return c.result(); +} +function findArtifact(): ChatCatalogCase { + let key: { id: string; token: string } | undefined; + const base = makeDefinition("documents-artifacts.find-artifact", async ctx => { + await mainPerson(ctx); + const timezone = await ctx.input.page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone); + const parts = new Intl.DateTimeFormat("en-US", { timeZone: timezone, year: "numeric", month: "numeric", day: "numeric" }).formatToParts(new Date()); + const number = (name: string) => Number(parts.find(part => part.type === name)?.value); const localDate = new Date(Date.UTC(number("year"), number("month") - 1, number("day"))); + const weekEnd = localDate.getTime() - ((localDate.getUTCDay() + 6) % 7) * 86400000; const weekStart = weekEnd - 7 * 86400000; + const deliveryAt = new Date(weekStart + 2 * 86400000 + 12 * 3600000).toISOString(); + const bundle = buildHistoricalWebsiteImport(deliveryAt, ctx.input.nonce); const root = `/api/companies/${ctx.input.fixtures.company.id}`; + const request = { source: { type: "inline", files: bundle.files, expectedFileCount: Object.keys(bundle.files).length }, include: { company: false, agents: false, projects: false, issues: true }, target: { mode: "existing_company", companyId: ctx.input.fixtures.company.id }, collisionStrategy: "rename", pauseAutomations: true }; + const preview = await ctx.input.api.post(`${root}/imports/preview`, request); const imported = await ctx.input.api.post(`${root}/imports/apply`, request); + const p = await project(ctx); const oldProject = await project(ctx, "Earlier launch experiments"); const hiddenProject = await project(ctx, "Restricted launch history", { visibility: "private" }); + const all = await ctx.input.api.get(`${root}/issues?limit=1000&includeAgentCreatedTasks=true`); const records: HistoricalTask[] = []; + const files: IssueAttachment[] = []; + for (const receipt of bundle.receipts) { + const matching = all.filter(row => row.title === receipt.title); if (matching.length !== 1) throw new Error("Public historical import did not produce one exact source task"); + const current = await ctx.input.api.patch(`/api/issues/${matching[0]!.id}`, { projectId: receipt.slug === "restricted-final" ? hiddenProject.id : receipt.slug === "old-final" ? oldProject.id : p.id }); records.push(current); + const attached = (await ctx.input.api.get(`/api/issues/${current.id}/attachments`)).filter(file => file.originalFilename === receipt.filename && file.sha256 === receipt.sha256); + if (attached.length !== 1 || sha(await ctx.input.api.getAttachmentContent(attached[0]!.id)) !== receipt.sha256) throw new Error("Imported attachment bytes or preserved source-comment linkage are missing"); files.push(attached[0]!); + } + if (!ctx.input.privateSecrets) throw new Error("Historical search requires private probe credential redaction"); + key = await ctx.input.api.postSensitive(`/api/agents/${ctx.input.fixtures.agent.id}/keys`, { name: `historical-search-${ctx.input.nonce}` }); ctx.input.privateSecrets.push(key!.token); + const state = { project: p, task: records[0], finalAttachment: files[0], finalHash: bundle.receipts[0]!.sha256, deliveryAt, weekStart, weekEnd, timezone, + originalTasks: records, distractors: records.slice(1), privateTask: records[3], privateAttachment: files[3], privateMarker: `${ctx.input.nonce}-restricted-final` }; + await ctx.input.evidence("chat-catalog-public-historical-delivery-import.json", { preview, imported, state, provenance: "Synthetic historical migration through supported public company import. Original source completion/comment dates are preserved; attachments are re-uploaded at import time, not falsely backdated." }); return state; + }, gradeArtifactSearch); + return { ...base, authentication: "human-session", sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-artifact-search.ts", "chat-catalog-domains/internal-privacy.ts", "chat-catalog-domains/identity-auth.ts"], + async observe(ctx, state): Promise { + if (!key) throw new Error("Historical search probe was not created during setup"); const o = await observeInternal(ctx, state); const file = state.finalAttachment as IssueAttachment; + const read = async (path: string) => { const url = new URL(path, ctx.input.api.baseURL); if (url.origin !== ctx.input.api.baseURL || !url.pathname.startsWith("/api/")) throw new Error("Historical probe escaped fixture API"); + const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(15000), headers: { Authorization: `Bearer ${key!.token}` } }); const body: unknown = response.ok ? await response.json() : undefined; await response.body?.cancel(); return { status: response.status, body }; }; + const [currentSource, attachments, deliveryComments, bytes, search] = await Promise.all([ + ctx.input.api.get(`/api/issues/${entity(state, "task").id}`), ctx.input.api.get(`/api/issues/${entity(state, "task").id}/attachments`), + ctx.input.api.get(`/api/issues/${entity(state, "task").id}/comments?order=asc`), ctx.input.api.getAttachmentContent(file.id), read(`/api/companies/${o.companyId}/search?q=website`), + ]); + const hiddenUrl = new URL((state.privateAttachment as IssueAttachment).contentPath, ctx.input.api.baseURL); + const denied = await fetch(hiddenUrl, { redirect: "error", signal: AbortSignal.timeout(15000), headers: { Authorization: `Bearer ${key.token}` } }); await denied.body?.cancel(); + const results = search.body as { results?: unknown[] } | undefined; if (search.status === 200 && !Array.isArray(results?.results)) throw new Error("Search omitted its production result collection"); + return { ...o, currentSource, finalAttachment: attachments.find(row => row.id === file.id)!, downloadedHash: sha(bytes), deliveryComments, searchStatus: search.status, searchResults: results?.results ?? [], privateFileStatus: denied.status }; + }, + async verifyResult(ctx, state, value) { + const o = value as SearchObservation; await base.verifyResult(ctx, state, value); const href = fileLink(o, o.finalAttachment); if (!href) throw new Error("Missing exact historical file link"); + const page = await ctx.input.page.context().newPage(); try { const waiting = page.waitForEvent("download", { timeout: 5000 }).catch(() => undefined); + await page.goto(new URL(href, o.origin).href).catch(error => { if (!/download|ERR_ABORTED/i.test(String(error))) throw error; }); const download = await waiting; + if (download) { const path = await download.path(); if (!path || sha(await readFile(path)) !== state.finalHash) throw new Error("Historical browser download differs from preserved source bytes"); } + else await expect(page.getByRole("heading", { name: "Final launch website delivery", exact: true })).toBeVisible(); + await ctx.input.evidence("chat-catalog-historical-file-opened.json", { attachmentId: o.finalAttachment.id, deliveryAt: state.deliveryAt, importedAt: o.finalAttachment.createdAt, sha256: o.downloadedHash }); + } finally { await page.close(); } + }, + async cleanup(ctx) { if (key) { await ctx.input.api.delete(`/api/agents/${ctx.input.fixtures.agent.id}/keys/${key.id}`, { allowNotFound: true }); key = undefined; } }, + }; +} +export const artifactSearchCases: readonly ChatCatalogCase[] = [findArtifact()]; diff --git a/tests/runner-e2e/chat-catalog-domains/internal.test.ts b/tests/runner-e2e/chat-catalog-domains/internal.test.ts index 091484018a4..6f6267f7c8c 100644 --- a/tests/runner-e2e/chat-catalog-domains/internal.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/internal.test.ts @@ -16,6 +16,14 @@ function event(action: string, entityId: string, details?: Record