From c115f1c5044396b55f7e3d23a99ca95a122ed7da Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 2 Oct 2026 11:25:12 +0800 Subject: [PATCH] fix(client): label delivery fees and verify receipt downloads --- .../receiptBrowserAcceptance.mjs | 60 ++++++ .../order-management/receiptDownload.test.mjs | 190 ++++++++++++++++++ .../views/DeliveryOrderDetailView.vue | 7 +- 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 src/modules/order-management/receiptBrowserAcceptance.mjs create mode 100644 src/modules/order-management/receiptDownload.test.mjs diff --git a/src/modules/order-management/receiptBrowserAcceptance.mjs b/src/modules/order-management/receiptBrowserAcceptance.mjs new file mode 100644 index 0000000..8944ad1 --- /dev/null +++ b/src/modules/order-management/receiptBrowserAcceptance.mjs @@ -0,0 +1,60 @@ +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('') +// 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. diff --git a/src/modules/order-management/receiptDownload.test.mjs b/src/modules/order-management/receiptDownload.test.mjs new file mode 100644 index 0000000..b862594 --- /dev/null +++ b/src/modules/order-management/receiptDownload.test.mjs @@ -0,0 +1,190 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { readFileSync } from 'node:fs' +import { createHash } from 'node:crypto' +import ts from 'typescript' +import axios from 'axios' +import { ref, computed } from 'vue' +import { mkdtemp, writeFile, utimes, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { verifyReceiptFile, acceptReceiptDownload } from './receiptBrowserAcceptance.mjs' + +// Execute production HTTP, API and SFC setup code. Only transport, browser saving +// and UI feedback are replaced; this is not a browser/server isolation test. +function compile(path, dependencies, globals = {}) { + let source = readFileSync(new URL(path, import.meta.url), 'utf8') + if (path.endsWith('.vue')) { + source = source.match(/