61 lines
3.7 KiB
JavaScript
61 lines
3.7 KiB
JavaScript
import { readFile, stat } from 'node:fs/promises'
|
|
import { basename } from 'node:path'
|
|
import { createHash } from 'node:crypto'
|
|
|
|
// Local-only verification. Expectations must come from this run's uploaded
|
|
// fixture bytes, not from the downloaded file or a previous Downloads entry.
|
|
export async function verifyReceiptFile(path, expected, startedAt) {
|
|
const metadata = await stat(path)
|
|
const bytes = await readFile(path)
|
|
const sha256 = createHash('sha256').update(bytes).digest('hex')
|
|
const checks = {
|
|
currentRun: metadata.mtimeMs >= startedAt,
|
|
fileName: basename(path) === expected.fileName,
|
|
size: bytes.length === expected.sizeBytes,
|
|
content: sha256 === expected.sha256,
|
|
}
|
|
return { passed: Object.values(checks).every(Boolean), checks, sizeBytes: bytes.length, sha256, savedFileName: basename(path) }
|
|
}
|
|
|
|
// Run only inside the approved computer-use session, passing its existing Tab.
|
|
// This module never connects to a browser, logs in, fetches HTTP or changes data.
|
|
// Root supplies fresh fixture metadata and logs in through the normal page UI.
|
|
// Example in cua_repl after reading the skill and selecting the customer tab:
|
|
// const { acceptReceiptDownload } = await import('<absolute path to this module>')
|
|
// const result = await acceptReceiptDownload(tab, expected)
|
|
// expected = { deliveryNo, fileName, sizeBytes, sha256 } from the source fixture.
|
|
// Save result with the current run ID/version; timeout is BLOCKED, not PASS.
|
|
export async function acceptReceiptDownload(tab, expected) {
|
|
if (!expected.deliveryNo || !expected.fileName || !Number.isSafeInteger(expected.sizeBytes)
|
|
|| expected.sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(expected.sha256)) {
|
|
throw new Error('Fresh fixture delivery number, filename, byte length and SHA-256 are required')
|
|
}
|
|
const snapshot = await tab.playwright.domSnapshot()
|
|
const heading = tab.playwright.getByRole('heading', { name: expected.deliveryNo, exact: true })
|
|
if (!(await heading.isVisible())) throw new Error('Open the exact fixture delivery detail first')
|
|
const row = tab.playwright.getByRole('row').filter({ hasText: expected.fileName })
|
|
if (await row.count() !== 1) throw new Error('Expected exactly one receipt row')
|
|
const startedAt = Date.now()
|
|
const [download, click] = await Promise.allSettled([
|
|
tab.playwright.waitForEvent('download', { timeoutMs: 20000 }).then(d => d.path({ timeoutMs: 10000 })),
|
|
row.getByRole('button', { name: '下载', exact: true }).click({ timeoutMs: 15000 }),
|
|
])
|
|
const base = { deliveryNo: expected.deliveryNo, fileName: expected.fileName, startedAt,
|
|
clickCompleted: click.status === 'fulfilled', receiptVisible: snapshot.includes(expected.fileName) }
|
|
if (click.status !== 'fulfilled' || download.status !== 'fulfilled' || !download.value) {
|
|
return { ...base, status: 'BLOCKED', reason: 'No completed browser save with a current local path', savedFileVerified: false }
|
|
}
|
|
const verification = await verifyReceiptFile(download.value, expected, startedAt)
|
|
return { ...base, status: verification.passed ? 'PASS' : 'FAIL', verification }
|
|
}
|
|
|
|
// Real isolation companion procedure (root-owned environment run):
|
|
// 1. A logs in, opens fresh A delivery, runs acceptReceiptDownload; retain result.
|
|
// 2. Log out via UI, log in as unrelated B, open the same A detail URL.
|
|
// 3. Require denied detail/no receipt filename/no download control and record DOM.
|
|
// 4. Root's HTTP suite must independently verify B->A binary endpoint rejection.
|
|
// A hidden button alone is not server isolation. Do not insert tokens into DOM,
|
|
// use raw CDP, reuse an old file, or replace missing UI save with HTTP success.
|
|
// If tool paths have opaque basenames, filename check remains FAIL/unproven;
|
|
// obtain browser download record plus original saved file through the user.
|