R74-A2 A3 A4 压榨试运行自动化空间
这个提交包含在:
@@ -0,0 +1,15 @@
|
||||
# R70 深层自动辅助试运行报告
|
||||
|
||||
生成时间:2026-04-28T13:59:08.610Z
|
||||
|
||||
## 结论
|
||||
|
||||
深层自动辅助试运行未发现阻断步骤;仍需人工判断整体体验是否顺手。
|
||||
|
||||
## 步骤结果
|
||||
|
||||
| Step | Status | Screenshot | Detail |
|
||||
| --- | --- | --- | --- |
|
||||
| 周报保存-生成待办-提交-完成闭环 | PASS | deploy/trialrun/screenshots/r70-auto-trialrun-deep/01-周报保存-生成待办-提交-完成闭环.png | weekStart=2027-03-26; token=Playwright 深层联动 1777384727632 |
|
||||
| 公告-团队状态-Portal 汇总联动 | PASS | deploy/trialrun/screenshots/r70-auto-trialrun-deep/02-公告-团队状态-Portal-汇总联动.png | Playwright 深层公告 1777384727632 |
|
||||
| 真实全局搜索命中公告并跳转 | PASS | deploy/trialrun/screenshots/r70-auto-trialrun-deep/03-真实全局搜索命中公告并跳转.png | Playwright 深层公告 1777384727632 |
|
||||
@@ -0,0 +1,20 @@
|
||||
# R70 批量 live OCR 辅助复核报告
|
||||
|
||||
生成时间:2026-04-28T13:59:45.495Z
|
||||
|
||||
## 结果
|
||||
|
||||
- 总文件数:1
|
||||
- 成功:0
|
||||
- 失败:1
|
||||
|
||||
## 明细
|
||||
|
||||
| 文件 | 状态 | 耗时(ms) | 票据ID | 发票代码 | 发票号码 | 销售方 | 开票日期 | 备注 |
|
||||
| --- | --- | ---: | --- | --- | --- | --- | --- | --- |
|
||||
| 北京悦海堂餐饮管理有限公司_发票金额1683.00元.pdf | ocr_failed | 696 | 3afbde2e-3e64-4578-8c0d-a05711e5934f | - | - | - | - | The SecretId is not found, please ensure that your SecretId is correct. 可在下方直接重试 OCR,或补齐关键字段后标记为可用。 |
|
||||
|
||||
## 说明
|
||||
|
||||
- 本报告用于真实腾讯 OCR 的本地辅助复核,不进入常规本地回归。
|
||||
- 如某张票失败,会继续处理后续文件,并在本报告统一汇总。
|
||||
@@ -140,3 +140,9 @@
|
||||
- 当前 trialrun 数据库内的 `feature_flags` 与 `workspace_settings` 已具备小团队试运行配置;若页面仍显示模块缺失,优先检查 server 进程的运行时 env,而不是先怀疑数据库种子。
|
||||
- 本地 HTTP 辅助复核不要强制 `COOKIE_SECURE=true`,否则 `127.0.0.1` / `192.168.123.7` 下登录态会失效;该项只在 HTTPS 试运行部署时开启。
|
||||
- 真实腾讯 OCR 已用运行时注入方式接入旧项目本地配置完成验证;不要把腾讯密钥写回仓库内 `.env`。
|
||||
- 本地先跑门禁:
|
||||
- `cd server && TRIALRUN_RUNTIME_MODE=local node src/scripts/smoke-trialrun-runtime.js`
|
||||
- 深层真实业务辅助回归:
|
||||
- `cd client && PLAYWRIGHT_MANUAL=1 R70_TRIALRUN_DEEP_ASSIST=1 E2E_BASE_URL=http://127.0.0.1:3001 npm run test:e2e -- tests/e2e/manual/r70-trialrun-deep-assist.spec.ts`
|
||||
- 批量真实腾讯 OCR 辅助回归:
|
||||
- `cd client && PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_LIVE_BATCH=1 PLAYWRIGHT_OCR_LIVE_DIR=<目录> E2E_BASE_URL=http://127.0.0.1:3001 npm run test:e2e -- tests/e2e/manual/live-invoice-ocr-batch.spec.ts`
|
||||
|
||||
@@ -64,3 +64,13 @@ R70 初次试运行暂停项已在 2026-04-28 的 R71 定向修复后关闭。
|
||||
- 运行方式:server 进程在本地 `3001` 端口以运行时注入方式加载旧项目本地腾讯配置;密钥未写入本仓库 `server/.env`。
|
||||
- 自动巡检:`client/tests/e2e/manual/r70-trialrun-assist.spec.ts` 在补齐本地运行时开关后,员工 / 经理 / 管理员 10 个辅助步骤全部通过。
|
||||
- 环境结论:本地试运行如果出现 Portal、周报、公告、团队、人员管理等入口缺失,先检查 server 运行时 `*_ENABLED` 开关是否齐全;本次已确认数据库开关与工作区配置本身无异常。
|
||||
|
||||
## R74 自动化压榨补充
|
||||
|
||||
- 本地门禁:`server/src/scripts/smoke-trialrun-runtime.js` 已支持 `TRIALRUN_RUNTIME_MODE=local`,并在本地 HTTP 口径下校验 env、feature flag、workspace setting 与导航基线,通过。
|
||||
- 深层真实业务辅助回归:`client/tests/e2e/manual/r70-trialrun-deep-assist.spec.ts` 已通过,覆盖:
|
||||
- 周报保存 -> 生成待办 -> 提交 -> 完成待办
|
||||
- 公告发布 -> 员工可见 -> 团队状态更新 -> Portal 汇总可见
|
||||
- 真实全局搜索命中公告并跳转
|
||||
- 批量真实腾讯 OCR 辅助回归:`client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts` 已落地并产出 `artifacts/r70/live-ocr-batch-report.md`。
|
||||
- 当前外部环境结论:本次 batch live OCR 执行时,服务端返回 `AuthFailure.SecretIdNotFound`,说明当前 3001 运行时注入的腾讯凭据未通过远端鉴权;框架已验证“整批不中断、失败统一汇总”的行为,但未在本轮验证到成功识别路径。
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"test:e2e:smoke": "playwright test tests/e2e/login-portal-ux.spec.ts tests/e2e/wecom-entry.spec.ts",
|
||||
"test:e2e:full": "playwright test",
|
||||
"test:e2e:r70-assist": "R70_TRIALRUN_ASSIST=1 playwright test tests/e2e/manual/r70-trialrun-assist.spec.ts --workers=1 --reporter=list",
|
||||
"test:e2e:r70-deep-assist": "PLAYWRIGHT_MANUAL=1 R70_TRIALRUN_DEEP_ASSIST=1 playwright test tests/e2e/manual/r70-trialrun-deep-assist.spec.ts --workers=1 --reporter=list",
|
||||
"test:e2e:live-ocr-batch": "PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_LIVE_BATCH=1 playwright test tests/e2e/manual/live-invoice-ocr-batch.spec.ts --workers=1 --reporter=list",
|
||||
"test:e2e:live": "playwright test tests/e2e/live-smoke.spec.ts",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import { defineConfig } from "@playwright/test";
|
||||
const e2eMode = process.env.E2E_MODE === "live" ? "live" : "local";
|
||||
const manualE2e = process.env.PLAYWRIGHT_MANUAL === "1";
|
||||
const r70TrialrunAssist = process.env.R70_TRIALRUN_ASSIST === "1";
|
||||
const r70TrialrunDeepAssist = process.env.R70_TRIALRUN_DEEP_ASSIST === "1";
|
||||
const liveOcrBatchAssist = process.env.PLAYWRIGHT_OCR_LIVE_BATCH === "1";
|
||||
const baseURL = e2eMode === "live"
|
||||
? process.env.LIVE_SMOKE_BASE_URL?.trim() || "https://oa.hyinda.com"
|
||||
: process.env.E2E_BASE_URL?.trim() || "http://127.0.0.1:3000";
|
||||
@@ -14,7 +16,7 @@ export default defineConfig({
|
||||
expect: {
|
||||
timeout: 30_000,
|
||||
},
|
||||
workers: r70TrialrunAssist ? 1 : process.env.CI ? 2 : 4,
|
||||
workers: r70TrialrunAssist || r70TrialrunDeepAssist || liveOcrBatchAssist ? 1 : process.env.CI ? 2 : 4,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
use: {
|
||||
baseURL,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
export type LiveInvoiceOcrStatus = "available" | "ocr_failed";
|
||||
|
||||
export type LiveInvoiceOcrResult = {
|
||||
status: LiveInvoiceOcrStatus;
|
||||
detailText: string;
|
||||
bannerText: string;
|
||||
durationMs: number;
|
||||
invoiceId: string | null;
|
||||
invoiceCode: string | null;
|
||||
invoiceNumber: string | null;
|
||||
sellerName: string | null;
|
||||
totalAmount: string | null;
|
||||
invoiceDate: string | null;
|
||||
};
|
||||
|
||||
function extractField(detailText: string, label: string) {
|
||||
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = detailText.match(new RegExp(`${escaped}([^\\n]+)`));
|
||||
return match?.[1]?.trim() || null;
|
||||
}
|
||||
|
||||
export async function openInvoicePool(page: Page) {
|
||||
await page.goto("/invoice-pool");
|
||||
await expect(page.getByTestId("invoice-pool-table")).toBeVisible();
|
||||
}
|
||||
|
||||
export async function uploadLiveInvoiceAndWait(page: Page, invoiceFilePath: string, timeout = 180_000): Promise<LiveInvoiceOcrResult> {
|
||||
const startedAt = Date.now();
|
||||
|
||||
await page.getByTestId("invoice-upload-file").setInputFiles(invoiceFilePath);
|
||||
await page.getByTestId("invoice-upload-submit").click();
|
||||
|
||||
const detailPanel = page.getByTestId("invoice-detail-panel");
|
||||
await expect(detailPanel).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const text = await detailPanel.innerText().catch(() => "");
|
||||
if (text.includes("状态:available")) return "available";
|
||||
if (text.includes("状态:ocr_failed")) return "ocr_failed";
|
||||
return "pending_ocr";
|
||||
}, { timeout })
|
||||
.not.toBe("pending_ocr");
|
||||
|
||||
const detailText = await detailPanel.innerText();
|
||||
const bannerText = await page.getByTestId("invoice-ocr-status-banner").innerText().catch(() => "");
|
||||
const status = detailText.includes("状态:ocr_failed") ? "ocr_failed" : "available";
|
||||
const invoiceNo = extractField(detailText, "发票号:");
|
||||
const [invoiceCode, invoiceNumber] = invoiceNo && invoiceNo.includes("-")
|
||||
? invoiceNo.split("-", 2).map((item) => item.trim() || null)
|
||||
: [invoiceNo, null];
|
||||
|
||||
return {
|
||||
status,
|
||||
detailText,
|
||||
bannerText,
|
||||
durationMs: Date.now() - startedAt,
|
||||
invoiceId: extractField(detailText, "票据ID:"),
|
||||
invoiceCode,
|
||||
invoiceNumber,
|
||||
sellerName: extractField(detailText, "销售方:"),
|
||||
totalAmount: extractField(detailText, "总金额:"),
|
||||
invoiceDate: extractField(detailText, "开票日期:"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { expect, test } from "../helpers/fixtures";
|
||||
import { EMPLOYEE, login, logout } from "../helpers/auth";
|
||||
import { openInvoicePool, uploadLiveInvoiceAndWait, type LiveInvoiceOcrResult } from "../helpers/liveInvoiceOcr";
|
||||
|
||||
type BatchResult = LiveInvoiceOcrResult & {
|
||||
filePath: string;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
const rootDir = path.resolve(process.cwd(), "..");
|
||||
const reportPath = path.join(rootDir, "artifacts/r70/live-ocr-batch-report.md");
|
||||
|
||||
function resolveInvoiceFiles() {
|
||||
const files = new Set<string>();
|
||||
const directFile = process.env.PLAYWRIGHT_OCR_LIVE_FILE?.trim();
|
||||
const batchFiles = process.env.PLAYWRIGHT_OCR_LIVE_FILES?.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) ?? [];
|
||||
const batchDir = process.env.PLAYWRIGHT_OCR_LIVE_DIR?.trim() ?? "";
|
||||
|
||||
if (directFile) {
|
||||
files.add(path.resolve(directFile));
|
||||
}
|
||||
|
||||
for (const file of batchFiles) {
|
||||
files.add(path.resolve(file));
|
||||
}
|
||||
|
||||
if (batchDir && existsSync(batchDir) && statSync(batchDir).isDirectory()) {
|
||||
for (const entry of readdirSync(batchDir).sort((left, right) => left.localeCompare(right, "zh-CN"))) {
|
||||
const fullPath = path.join(batchDir, entry);
|
||||
if (!statSync(fullPath).isFile()) continue;
|
||||
if (!/\.(pdf|png|jpg|jpeg|webp)$/i.test(entry)) continue;
|
||||
files.add(path.resolve(fullPath));
|
||||
}
|
||||
}
|
||||
|
||||
return [...files].filter((filePath) => existsSync(filePath));
|
||||
}
|
||||
|
||||
function writeReport(results: BatchResult[]) {
|
||||
mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
|
||||
const lines = [
|
||||
"# R70 批量 live OCR 辅助复核报告",
|
||||
"",
|
||||
`生成时间:${new Date().toISOString()}`,
|
||||
"",
|
||||
"## 结果",
|
||||
"",
|
||||
`- 总文件数:${results.length}`,
|
||||
`- 成功:${results.filter((item) => item.status === "available").length}`,
|
||||
`- 失败:${results.filter((item) => item.status === "ocr_failed").length}`,
|
||||
"",
|
||||
"## 明细",
|
||||
"",
|
||||
"| 文件 | 状态 | 耗时(ms) | 票据ID | 发票代码 | 发票号码 | 销售方 | 开票日期 | 备注 |",
|
||||
"| --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
|
||||
...results.map((item) => {
|
||||
const remark = item.status === "ocr_failed" ? (item.bannerText || item.detailText).replaceAll("|", "\\|") : "ok";
|
||||
return `| ${item.fileName} | ${item.status} | ${item.durationMs} | ${item.invoiceId ?? "-"} | ${item.invoiceCode ?? "-"} | ${item.invoiceNumber ?? "-"} | ${item.sellerName ?? "-"} | ${item.invoiceDate ?? "-"} | ${remark} |`;
|
||||
}),
|
||||
"",
|
||||
"## 说明",
|
||||
"",
|
||||
"- 本报告用于真实腾讯 OCR 的本地辅助复核,不进入常规本地回归。",
|
||||
"- 如某张票失败,会继续处理后续文件,并在本报告统一汇总。",
|
||||
];
|
||||
|
||||
writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf-8");
|
||||
}
|
||||
|
||||
test("手工 live 腾讯 OCR 批量辅助复核:真实票样逐张上传并输出报告", async ({ page }) => {
|
||||
test.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行");
|
||||
test.skip(process.env.PLAYWRIGHT_OCR_LIVE !== "1", "仅在显式 live OCR 辅助复核模式下执行");
|
||||
|
||||
const invoiceFiles = resolveInvoiceFiles();
|
||||
test.skip(!invoiceFiles.length, "需要提供 PLAYWRIGHT_OCR_LIVE_FILE / PLAYWRIGHT_OCR_LIVE_FILES / PLAYWRIGHT_OCR_LIVE_DIR");
|
||||
|
||||
const results: BatchResult[] = [];
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
try {
|
||||
await openInvoicePool(page);
|
||||
|
||||
for (const invoiceFilePath of invoiceFiles) {
|
||||
await openInvoicePool(page);
|
||||
const result = await uploadLiveInvoiceAndWait(page, invoiceFilePath);
|
||||
results.push({
|
||||
...result,
|
||||
filePath: invoiceFilePath,
|
||||
fileName: path.basename(invoiceFilePath),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
writeReport(results);
|
||||
await logout(page, EMPLOYEE.name).catch(() => undefined);
|
||||
}
|
||||
|
||||
const failures = results.filter((item) => item.status === "ocr_failed");
|
||||
expect(failures, JSON.stringify(failures, null, 2)).toHaveLength(0);
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
||||
|
||||
import { test, expect } from "../helpers/fixtures";
|
||||
import { EMPLOYEE, login, logout } from "../helpers/auth";
|
||||
import { openNavLink } from "../helpers/navigation";
|
||||
import { openInvoicePool, uploadLiveInvoiceAndWait } from "../helpers/liveInvoiceOcr";
|
||||
|
||||
test("手工 live 腾讯 OCR 辅助复核:真实 VAT 发票上传后进入可处理状态", async ({ page }) => {
|
||||
test.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行");
|
||||
@@ -12,23 +12,11 @@ test("手工 live 腾讯 OCR 辅助复核:真实 VAT 发票上传后进入可
|
||||
test.skip(!invoiceFilePath || !existsSync(invoiceFilePath), "需要提供存在的 PLAYWRIGHT_OCR_LIVE_FILE");
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "票据中心")).click();
|
||||
await expect(page.getByTestId("invoice-pool-table")).toBeVisible();
|
||||
await openInvoicePool(page);
|
||||
|
||||
await page.getByTestId("invoice-upload-file").setInputFiles(invoiceFilePath);
|
||||
await page.getByTestId("invoice-upload-submit").click();
|
||||
|
||||
await expect.poll(async () => {
|
||||
const text = await page.getByTestId("invoice-detail-panel").innerText().catch(() => "");
|
||||
if (text.includes("状态:available")) return "available";
|
||||
if (text.includes("状态:ocr_failed")) return "ocr_failed";
|
||||
return "pending_ocr";
|
||||
}, { timeout: 180_000 }).not.toBe("pending_ocr");
|
||||
|
||||
const detailText = await page.getByTestId("invoice-detail-panel").innerText();
|
||||
if (detailText.includes("状态:ocr_failed")) {
|
||||
const bannerText = await page.getByTestId("invoice-ocr-status-banner").innerText().catch(() => "OCR 失败");
|
||||
throw new Error(`live 腾讯 OCR 失败:${bannerText}`);
|
||||
const result = await uploadLiveInvoiceAndWait(page, invoiceFilePath);
|
||||
if (result.status === "ocr_failed") {
|
||||
throw new Error(`live 腾讯 OCR 失败:${result.bannerText || result.detailText}`);
|
||||
}
|
||||
|
||||
await expect(page.getByTestId("invoice-detail-panel")).not.toContainText("发票号:---");
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { ADMIN, EMPLOYEE, apiPostData, createAuthenticatedApiClient, login, logout } from "../helpers/auth";
|
||||
import { openNavLink } from "../helpers/navigation";
|
||||
import { TEST_IDS } from "../helpers/testIds";
|
||||
|
||||
type StepStatus = "PASS" | "FAIL";
|
||||
|
||||
type TrialStep = {
|
||||
title: string;
|
||||
status: StepStatus;
|
||||
detail: string;
|
||||
screenshot: string;
|
||||
};
|
||||
|
||||
const rootDir = path.resolve(process.cwd(), "..");
|
||||
const reportPath = path.join(rootDir, "artifacts/r70/auto-trialrun-deep-report.md");
|
||||
const issueCandidatesPath = path.join(rootDir, "artifacts/r70/auto-trialrun-deep-issues.csv");
|
||||
const screenshotDir = path.join(rootDir, "deploy/trialrun/screenshots/r70-auto-trialrun-deep");
|
||||
|
||||
function formatWeekStart(offsetDays: number) {
|
||||
const value = new Date();
|
||||
value.setUTCDate(value.getUTCDate() + offsetDays);
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function csvCell(value: string) {
|
||||
return `"${value.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
function safeName(value: string) {
|
||||
return value.replace(/[^a-z0-9\u4e00-\u9fa5]+/gi, "-").replace(/^-|-$/g, "").slice(0, 80);
|
||||
}
|
||||
|
||||
function writeReports(steps: TrialStep[]) {
|
||||
mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
const failures = steps.filter((item) => item.status === "FAIL");
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const lines = [
|
||||
"# R70 深层自动辅助试运行报告",
|
||||
"",
|
||||
`生成时间:${now}`,
|
||||
"",
|
||||
"## 结论",
|
||||
"",
|
||||
failures.length
|
||||
? `深层自动辅助试运行发现 ${failures.length} 个失败步骤,需继续人工确认。`
|
||||
: "深层自动辅助试运行未发现阻断步骤;仍需人工判断整体体验是否顺手。",
|
||||
"",
|
||||
"## 步骤结果",
|
||||
"",
|
||||
"| Step | Status | Screenshot | Detail |",
|
||||
"| --- | --- | --- | --- |",
|
||||
...steps.map((item) => `| ${item.title} | ${item.status} | ${item.screenshot || "N/A"} | ${item.detail.replaceAll("|", "\\|")} |`),
|
||||
];
|
||||
|
||||
writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf-8");
|
||||
|
||||
const issueHeader = [
|
||||
"ID",
|
||||
"Role",
|
||||
"Severity",
|
||||
"Module",
|
||||
"Title",
|
||||
"Steps",
|
||||
"Expected",
|
||||
"Actual",
|
||||
"Screenshot",
|
||||
"Owner",
|
||||
"Disposition",
|
||||
"Status",
|
||||
"Notes",
|
||||
];
|
||||
const issueRows = failures.map((item, index) =>
|
||||
[
|
||||
`R70-DEEP-${String(index + 1).padStart(3, "0")}`,
|
||||
"employee",
|
||||
"P1",
|
||||
item.title,
|
||||
`深层自动辅助试运行失败:${item.title}`,
|
||||
item.title,
|
||||
"真实环境业务链路可执行且关键断言通过",
|
||||
item.detail,
|
||||
item.screenshot || "N/A",
|
||||
"TBD",
|
||||
"needs review",
|
||||
"OPEN",
|
||||
"由 R70 深层自动辅助试运行生成,需人工复核严重级别。",
|
||||
].map(csvCell).join(","),
|
||||
);
|
||||
writeFileSync(issueCandidatesPath, `${issueHeader.map(csvCell).join(",")}\n${issueRows.join("\n")}${issueRows.length ? "\n" : ""}`, "utf-8");
|
||||
}
|
||||
|
||||
test("R70 深层自动辅助试运行:周报待办、公告团队状态、真实搜索", async ({ page }) => {
|
||||
test.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行");
|
||||
test.skip(process.env.R70_TRIALRUN_DEEP_ASSIST !== "1", "仅在显式 deep assist 模式下执行");
|
||||
test.setTimeout(240_000);
|
||||
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
const steps: TrialStep[] = [];
|
||||
let stepIndex = 0;
|
||||
|
||||
const suffix = Date.now();
|
||||
const uniqueToken = `Playwright 深层联动 ${suffix}`;
|
||||
const weekStart = formatWeekStart(280 + (suffix % 70));
|
||||
const announcementTitle = `Playwright 深层公告 ${suffix}`;
|
||||
const presenceNote = `深层辅助复核 ${suffix}`;
|
||||
const adminClient = await createAuthenticatedApiClient(ADMIN);
|
||||
|
||||
async function runStep(title: string, action: () => Promise<string | void>) {
|
||||
stepIndex += 1;
|
||||
const screenshot = path.join(screenshotDir, `${String(stepIndex).padStart(2, "0")}-${safeName(title)}.png`);
|
||||
const relativeScreenshot = path.relative(rootDir, screenshot);
|
||||
try {
|
||||
const detail = (await action()) ?? "ok";
|
||||
await page.screenshot({ path: screenshot, fullPage: true });
|
||||
steps.push({ title, status: "PASS", detail, screenshot: relativeScreenshot });
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
await page.screenshot({ path: screenshot, fullPage: true }).catch(() => undefined);
|
||||
steps.push({ title, status: "FAIL", detail, screenshot: relativeScreenshot });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await apiPostData<{ item: { id: string } }>(adminClient.api, "/api/announcements", {
|
||||
title: announcementTitle,
|
||||
summary: "用于深层自动辅助试运行验证真实公告、Portal 与搜索链路。",
|
||||
enabled: true,
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await runStep("周报保存-生成待办-提交-完成闭环", async () => {
|
||||
await (await openNavLink(page, "周报")).click();
|
||||
await expect(page.getByRole("heading", { name: "周报", exact: true })).toBeVisible();
|
||||
|
||||
const weeklyLoadResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname === "/api/weekly-reports/me" && url.searchParams.get("weekStart") === weekStart && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByLabel("周报归档周").fill(weekStart);
|
||||
await weeklyLoadResponse;
|
||||
await page.getByLabel("本周完成").fill(`${uniqueToken} 已完成`);
|
||||
await page.getByLabel("当前阻塞").fill(`${uniqueToken} 阻塞`);
|
||||
await page.getByLabel("下周计划").fill(`${uniqueToken} 下周计划`);
|
||||
|
||||
const saveResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "PUT" && url.pathname === "/api/weekly-reports/me" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.save).click();
|
||||
await saveResponse;
|
||||
await expect(page.getByText("周报已保存")).toBeVisible();
|
||||
|
||||
const createTodoResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/todos" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.createTodo).click();
|
||||
await createTodoResponse;
|
||||
await expect(page.getByText("已从周报生成待办")).toBeVisible();
|
||||
|
||||
const submitResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/weekly-reports/me/submit" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByTestId(TEST_IDS.weeklyReport.submit).click();
|
||||
await submitResponse;
|
||||
await expect(page.getByText("周报已提交")).toBeVisible();
|
||||
|
||||
await (await openNavLink(page, "待办")).click();
|
||||
const todoRow = page.getByTestId(TEST_IDS.todo.row).filter({ hasText: uniqueToken }).first();
|
||||
await expect(todoRow).toBeVisible({ timeout: 30_000 });
|
||||
await todoRow.getByTestId(TEST_IDS.todo.complete).click();
|
||||
await expect(todoRow).toContainText("done", { timeout: 30_000 });
|
||||
return `weekStart=${weekStart}; token=${uniqueToken}`;
|
||||
});
|
||||
|
||||
await runStep("公告-团队状态-Portal 汇总联动", async () => {
|
||||
await page.goto("/announcements");
|
||||
await expect(page.getByRole("heading", { name: "公告" })).toBeVisible();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto("/team");
|
||||
await expect(page.getByRole("heading", { name: "团队状态" })).toBeVisible();
|
||||
await page.locator("select.select-std").first().selectOption("remote");
|
||||
await page.getByPlaceholder("可选备注,如下午外出办事").fill(presenceNote);
|
||||
await page.getByRole("button", { name: "保存状态" }).click();
|
||||
await expect(page.getByText("今日状态已更新")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto("/portal");
|
||||
await page.getByRole("tab", { name: "公告" }).click();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.locator('[aria-label="portal-card-today_presence"]')).toBeVisible();
|
||||
return announcementTitle;
|
||||
});
|
||||
|
||||
await runStep("真实全局搜索命中公告并跳转", async () => {
|
||||
await page.goto("/portal");
|
||||
await page.getByTestId("global-search-trigger").click();
|
||||
await expect(page.getByRole("heading", { name: "全局搜索" })).toBeVisible();
|
||||
|
||||
const searchResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname === "/api/search" && response.ok();
|
||||
}, { timeout: 30_000 });
|
||||
await page.getByPlaceholder("搜索人员、项目、合同、公告、资产、请假、报销、周报、待办").fill(String(suffix));
|
||||
await searchResponse;
|
||||
|
||||
const resultButton = page.getByRole("button", { name: new RegExp(announcementTitle) }).first();
|
||||
await expect(resultButton).toBeVisible({ timeout: 30_000 });
|
||||
await resultButton.click();
|
||||
await expect(page).toHaveURL(/\/announcements$/);
|
||||
await expect(page.getByRole("heading", { name: "公告" })).toBeVisible();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
return announcementTitle;
|
||||
});
|
||||
} finally {
|
||||
writeReports(steps);
|
||||
await adminClient.dispose();
|
||||
await logout(page, EMPLOYEE.name).catch(() => undefined);
|
||||
}
|
||||
|
||||
const failures = steps.filter((item) => item.status === "FAIL");
|
||||
expect(failures, JSON.stringify(failures, null, 2)).toHaveLength(0);
|
||||
});
|
||||
@@ -261,6 +261,35 @@ docker logs -f hyb-oa-trialrun-server
|
||||
- 建议保留备用:`PAYMENT_BATCH_ENABLED=false`、`CUTOVER_CONSOLE_ENABLED=false`
|
||||
- OCR 回归环境建议:`OCR_PROVIDER=mock`
|
||||
|
||||
### 7.2A 本地 Playwright 辅助复核门禁
|
||||
1. 本地 HTTP 辅助复核先执行:
|
||||
```bash
|
||||
cd server
|
||||
TRIALRUN_RUNTIME_MODE=local node src/scripts/smoke-trialrun-runtime.js
|
||||
```
|
||||
2. 该命令会同时检查 trialrun 所需 env、feature flag、workspace setting 与导航基线。
|
||||
3. `local` 模式允许 `APP_ORIGIN` 为空、`COOKIE_SECURE` 为空或 `false`;HTTPS / 真实部署仍按 `deploy` 模式要求 `APP_ORIGIN=https://...` 且 `COOKIE_SECURE=true`。
|
||||
|
||||
### 7.2B 本地深层辅助回归命令
|
||||
```bash
|
||||
cd client
|
||||
PLAYWRIGHT_MANUAL=1 \
|
||||
R70_TRIALRUN_DEEP_ASSIST=1 \
|
||||
E2E_BASE_URL=http://127.0.0.1:3001 \
|
||||
npm run test:e2e -- tests/e2e/manual/r70-trialrun-deep-assist.spec.ts
|
||||
```
|
||||
|
||||
### 7.2C 批量真实腾讯 OCR 辅助回归命令
|
||||
```bash
|
||||
cd client
|
||||
PLAYWRIGHT_MANUAL=1 \
|
||||
PLAYWRIGHT_OCR_LIVE=1 \
|
||||
PLAYWRIGHT_OCR_LIVE_BATCH=1 \
|
||||
PLAYWRIGHT_OCR_LIVE_DIR=/abs/path/to/invoices \
|
||||
E2E_BASE_URL=http://127.0.0.1:3001 \
|
||||
npm run test:e2e -- tests/e2e/manual/live-invoice-ocr-batch.spec.ts
|
||||
```
|
||||
|
||||
### 7.3 上线前整理事项
|
||||
1. 补齐员工账号、部门、直属上级与员工档案。
|
||||
2. 核对资产卡片与实际领用关系。
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
|
||||
"A1","R74 执行合同建档与范围冻结","创建并校验 R74 合同,仅承接“trialrun 本地门禁 + 深层真实业务回归 + live OCR 批量辅助回归”;不承接 CI 接入、像素级视觉 diff、企业微信真实端到端。","issues/2026-04-28_22-40-00-r74-trialrun-automation-squeeze.csv 列完整且通过校验;A2-A4 依赖关系清晰;Notes 明确本轮不做像素 diff。","命令:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-04-28_22-40-00-r74-trialrun-automation-squeeze.csv。","manual","DONE","DONE","DONE","issues/2026-04-28_22-40-00-r74-trialrun-automation-squeeze.csv","[]","回滚路径:删除本 CSV;不影响业务运行。2026-04-28:合同已写入并通过 validate_issues_csv.py 校验。"
|
||||
"A2","本地试运行环境门禁自动化","调整 trialrun runtime smoke,使其在本地 HTTP 环境下可判定通过,并在跑 Playwright 前检查 trialrun 所需 env、feature flag、workspace setting,提前拦截配置缺口。","存在一条可复用的本地门禁命令;缺少关键 env 时明确失败;本地 HTTP 模式不会因 COOKIE_SECURE 误判失败;runbook 与 checklist 回填启动方式。","命令:cd server && TRIALRUN_RUNTIME_MODE=local node src/scripts/smoke-trialrun-runtime.js。业务:缺少 PORTAL_HOME/WEEKLY_REPORT/PRESENCE 等关键开关时先失败,不进入 Playwright。","node|manual","DONE","DONE","DONE","server/src/scripts/smoke-trialrun-runtime.js|deploy/trialrun/runbook.md|artifacts/r70/manual-trialrun-checklist.md","[""A1""]","本地模式允许 HTTP cookie 配置差异;HTTPS/正式试运行仍保留严格检查。2026-04-28:已补齐 GLOBAL_SEARCH/ATTENDANCE/PROJECT_LINKING 门禁、本地/部署模式分流、工作区显隐与导航基线校验,并在 trialrun DB 上通过 local smoke。"
|
||||
"A3","真实业务深层 Playwright 辅助回归","在现有 r70 辅助巡检之外,增加真实环境下的周报->待办、公告->团队状态->Portal、全局搜索 smoke,并输出 trialrun 辅助报告。优先复用已有 real-env spec 逻辑,不重造重复断言。","本地显式开关下可一键跑通 3 类真实业务链路;失败时输出到 artifacts/r70 辅助报告;不依赖 mock search 数据。","命令:cd client && PLAYWRIGHT_MANUAL=1 R70_TRIALRUN_DEEP_ASSIST=1 E2E_BASE_URL=http://127.0.0.1:3001 npx playwright test tests/e2e/manual/r70-trialrun-deep-assist.spec.ts --workers=1 --reporter=list。业务:周报生成待办并完成、公告发布后员工与 Portal 可见、真实数据下搜索可打开并返回结果。","playwright|manual","DONE","DONE","DONE","client/package.json|client/playwright.config.ts|client/tests/e2e/manual/r70-trialrun-deep-assist.spec.ts|client/tests/e2e/weekly-todo-flow.spec.ts|client/tests/e2e/announcements-presence.spec.ts|client/tests/e2e/global-search.spec.ts|artifacts/r70/auto-trialrun-deep-report.md","[""A1"",""A2""]","优先串联真实环境链路;mock spec 保留做组件级/交互级稳定回归。2026-04-28:已通过 deep assist,覆盖周报待办闭环、公告与团队状态 Portal 联动、真实全局搜索命中公告并跳转。"
|
||||
"A4","真实腾讯 OCR 批量辅助回归","把 live OCR 单票辅助复核扩成批量模式,支持目录或显式文件列表输入,并生成每张票的识别状态、耗时、缺失字段与可绑定状态报告。","显式开关下可对多张真实发票批量执行 live OCR;每张票都有结构化结果;失败不会中断整批执行;报告可直接用于人工复核。","命令:cd client && PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_LIVE_BATCH=1 PLAYWRIGHT_OCR_LIVE_DIR=<dir> E2E_BASE_URL=http://127.0.0.1:3001 npx playwright test tests/e2e/manual/live-invoice-ocr-batch.spec.ts --workers=1 --reporter=list。业务:多票样上传后分别得到 available/ocr_failed 与原因。","playwright|manual","DONE","TODO","TODO","client/package.json|client/playwright.config.ts|client/tests/e2e/helpers/liveInvoiceOcr.ts|client/tests/e2e/manual/live-invoice-ocr.spec.ts|client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts|artifacts/r70/live-ocr-batch-report.md|artifacts/r70/manual-trialrun-report.md|artifacts/r70/manual-trialrun-checklist.md","[""A1"",""A2""]","不把真实腾讯密钥写入仓库;继续采用运行时注入。2026-04-28:批量框架与结构化报告已落地,单文件 batch 实跑能完整输出失败明细且不中断后续文件;当前 trialrun 3001 运行时返回 `AuthFailure.SecretIdNotFound`,尚未验证成功识别路径。"
|
||||
|
@@ -11,37 +11,144 @@ const { resolvePermissionsForRoles, hasPermission } = require('../constants/perm
|
||||
|
||||
const REQUIRED_ENV_VALUES = {
|
||||
PORTAL_HOME_ENABLED: 'true',
|
||||
GLOBAL_SEARCH_ENABLED: 'true',
|
||||
ANNOUNCEMENTS_ENABLED: 'true',
|
||||
PRESENCE_BOARD_ENABLED: 'true',
|
||||
ATTENDANCE_ENABLED: 'true',
|
||||
WEEKLY_REPORT_ENABLED: 'true',
|
||||
EMPLOYEE_LIFECYCLE_ENABLED: 'true',
|
||||
TODO_BOARD_ENABLED: 'true',
|
||||
PROJECT_DOMAIN_ENABLED: 'true',
|
||||
PROJECT_LINKING_ENABLED: 'true',
|
||||
PROJECT_COMMUNICATION_LOG_ENABLED: 'true',
|
||||
PROJECT_CLOSURE_CHECKLIST_ENABLED: 'true',
|
||||
COOKIE_SECURE: 'true',
|
||||
};
|
||||
|
||||
const REQUIRED_NON_EMPTY_ENV_KEYS = ['APP_ORIGIN'];
|
||||
|
||||
const REQUIRED_FLAGS = [
|
||||
FEATURE_FLAG_KEYS.portalHome,
|
||||
FEATURE_FLAG_KEYS.globalSearch,
|
||||
FEATURE_FLAG_KEYS.announcements,
|
||||
FEATURE_FLAG_KEYS.presenceBoard,
|
||||
FEATURE_FLAG_KEYS.attendance,
|
||||
FEATURE_FLAG_KEYS.weeklyReport,
|
||||
FEATURE_FLAG_KEYS.employeeLifecycle,
|
||||
FEATURE_FLAG_KEYS.todoBoard,
|
||||
FEATURE_FLAG_KEYS.projectDomain,
|
||||
FEATURE_FLAG_KEYS.projectLinking,
|
||||
FEATURE_FLAG_KEYS.projectCommunicationLog,
|
||||
FEATURE_FLAG_KEYS.projectClosureChecklist,
|
||||
];
|
||||
|
||||
const REQUIRED_MAIN_NAV_KEYS = [
|
||||
'portal',
|
||||
'team',
|
||||
'leaveRequests',
|
||||
'reimbursements',
|
||||
'assets',
|
||||
'weeklyReports',
|
||||
'todos',
|
||||
'announcements',
|
||||
];
|
||||
|
||||
const REQUIRED_ADMIN_NAV_KEYS = [
|
||||
'workspace',
|
||||
'people',
|
||||
'systemSettings',
|
||||
'featureFlags',
|
||||
'auditLogs',
|
||||
];
|
||||
|
||||
const REQUIRED_WORKSPACE_VISIBILITY = {
|
||||
portal: 'main',
|
||||
team: 'main',
|
||||
leaveRequests: 'main',
|
||||
reimbursements: 'main',
|
||||
assets: 'main',
|
||||
weeklyReports: 'main',
|
||||
todos: 'main',
|
||||
announcements: 'main',
|
||||
people: 'admin',
|
||||
workspace: 'admin',
|
||||
systemSettings: 'admin',
|
||||
featureFlags: 'admin',
|
||||
auditLogs: 'admin',
|
||||
};
|
||||
|
||||
function assertCondition(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRuntimeMode() {
|
||||
const explicit = String(process.env.TRIALRUN_RUNTIME_MODE ?? '').trim().toLowerCase();
|
||||
if (explicit === 'local' || explicit === 'deploy') {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const appOrigin = String(process.env.APP_ORIGIN ?? '').trim();
|
||||
if (appOrigin.startsWith('https://') || process.env.COOKIE_SECURE === 'true') {
|
||||
return 'deploy';
|
||||
}
|
||||
return 'local';
|
||||
}
|
||||
|
||||
function assertEnvValues(runtimeMode) {
|
||||
for (const [key, expected] of Object.entries(REQUIRED_ENV_VALUES)) {
|
||||
const actual = process.env[key];
|
||||
assertCondition(actual === expected, `${key} 期望为 ${expected},实际为 ${actual ?? '<empty>'}`);
|
||||
console.log(`[PASS] ${key}=${actual}`);
|
||||
}
|
||||
|
||||
const appOrigin = String(process.env.APP_ORIGIN ?? '').trim();
|
||||
if (runtimeMode === 'deploy') {
|
||||
assertCondition(appOrigin.length > 0, 'APP_ORIGIN 不能为空');
|
||||
assertCondition(appOrigin.startsWith('https://'), `deploy 模式下 APP_ORIGIN 必须为 https,实际为 ${appOrigin}`);
|
||||
assertCondition(process.env.COOKIE_SECURE === 'true', `deploy 模式下 COOKIE_SECURE 期望为 true,实际为 ${process.env.COOKIE_SECURE ?? '<empty>'}`);
|
||||
console.log(`[PASS] APP_ORIGIN=${appOrigin}`);
|
||||
console.log('[PASS] COOKIE_SECURE=true');
|
||||
return;
|
||||
}
|
||||
|
||||
if (appOrigin) {
|
||||
assertCondition(/^https?:\/\//.test(appOrigin), `local 模式下 APP_ORIGIN 如提供,必须为 http/https,实际为 ${appOrigin}`);
|
||||
console.log(`[PASS] APP_ORIGIN=${appOrigin}`);
|
||||
} else {
|
||||
console.log('[PASS] APP_ORIGIN=<empty>(local 模式允许)');
|
||||
}
|
||||
|
||||
const cookieSecure = process.env.COOKIE_SECURE;
|
||||
assertCondition(cookieSecure == null || cookieSecure === '' || cookieSecure === 'false', `local 模式下 COOKIE_SECURE 应为空或 false,实际为 ${cookieSecure ?? '<empty>'}`);
|
||||
console.log(`[PASS] COOKIE_SECURE=${cookieSecure ?? '<empty>'}(local 模式允许)`);
|
||||
}
|
||||
|
||||
function assertWorkspaceSetting(workspace) {
|
||||
assertCondition(Boolean(workspace), 'WorkspaceSetting 读取失败');
|
||||
console.log(`[PASS] 工作区设置可读,profile=${workspace.profile}`);
|
||||
|
||||
for (const [key, expected] of Object.entries(REQUIRED_WORKSPACE_VISIBILITY)) {
|
||||
const actual = workspace.moduleVisibility?.[key];
|
||||
assertCondition(actual === expected, `workspace.moduleVisibility.${key} 期望为 ${expected},实际为 ${actual ?? '<empty>'}`);
|
||||
}
|
||||
console.log(`[PASS] 工作区模块显隐符合 trialrun 基线,共校验 ${Object.keys(REQUIRED_WORKSPACE_VISIBILITY).length} 项`);
|
||||
}
|
||||
|
||||
function assertNavigation(navigation) {
|
||||
assertCondition(Array.isArray(navigation.sections?.main), '系统导航主区块生成失败');
|
||||
assertCondition(Array.isArray(navigation.sections?.admin), '系统导航管理区块生成失败');
|
||||
|
||||
const mainKeys = new Set((navigation.sections.main ?? []).map((item) => item.key));
|
||||
const adminKeys = new Set((navigation.sections.admin ?? []).map((item) => item.key));
|
||||
|
||||
for (const key of REQUIRED_MAIN_NAV_KEYS) {
|
||||
assertCondition(mainKeys.has(key), `系统导航 main 缺少 ${key} 入口`);
|
||||
}
|
||||
for (const key of REQUIRED_ADMIN_NAV_KEYS) {
|
||||
assertCondition(adminKeys.has(key), `系统导航 admin 缺少 ${key} 入口`);
|
||||
}
|
||||
console.log(`[PASS] 导航构建成功,main=${navigation.sections.main.length} admin=${navigation.sections.admin.length}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = getConfig();
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
@@ -49,21 +156,13 @@ async function main() {
|
||||
|
||||
try {
|
||||
console.log('[R26] 开始 trialrun runtime smoke');
|
||||
const runtimeMode = resolveRuntimeMode();
|
||||
console.log(`[R26] runtimeMode=${runtimeMode}`);
|
||||
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
console.log('[PASS] Prisma 连接正常');
|
||||
|
||||
for (const [key, expected] of Object.entries(REQUIRED_ENV_VALUES)) {
|
||||
const actual = process.env[key];
|
||||
assertCondition(actual === expected, `${key} 期望为 ${expected},实际为 ${actual ?? '<empty>'}`);
|
||||
console.log(`[PASS] ${key}=${actual}`);
|
||||
}
|
||||
|
||||
for (const key of REQUIRED_NON_EMPTY_ENV_KEYS) {
|
||||
const actual = process.env[key];
|
||||
assertCondition(typeof actual === 'string' && actual.trim().length > 0, `${key} 不能为空`);
|
||||
console.log(`[PASS] ${key} 已配置`);
|
||||
}
|
||||
assertEnvValues(runtimeMode);
|
||||
|
||||
const adminEmail = (process.env.TRIALRUN_ADMIN_EMAIL || '[email protected]').trim();
|
||||
const adminEmployee = await prisma.employee.findFirst({
|
||||
@@ -83,8 +182,7 @@ async function main() {
|
||||
console.log(`[PASS] 已找到 active admin 员工:${adminEmployee.email}`);
|
||||
|
||||
const workspace = await getWorkspaceSetting(prisma);
|
||||
assertCondition(Boolean(workspace), 'WorkspaceSetting 读取失败');
|
||||
console.log(`[PASS] 工作区设置可读,profile=${workspace.profile}`);
|
||||
assertWorkspaceSetting(workspace);
|
||||
|
||||
const employeeForGate = {
|
||||
...adminEmployee,
|
||||
@@ -115,12 +213,7 @@ async function main() {
|
||||
hasPermission: (employee, permissionCode) => hasPermission(employee.permissionCodes ?? [], permissionCode),
|
||||
});
|
||||
|
||||
assertCondition(Array.isArray(navigation.sections?.main), '系统导航主区块生成失败');
|
||||
assertCondition(
|
||||
navigation.sections.main.some((item) => item.key === 'portal'),
|
||||
'系统导航未生成 portal 入口',
|
||||
);
|
||||
console.log(`[PASS] 导航构建成功,main=${navigation.sections.main.length} admin=${navigation.sections.admin.length}`);
|
||||
assertNavigation(navigation);
|
||||
|
||||
console.log('[R26][PASS] trialrun runtime smoke 通过');
|
||||
} finally {
|
||||
|
||||
在新工单中引用
屏蔽一个用户