Files
client-web/src/modules/order-management/receiptBrowserAcceptance.mjs
T

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.