R74-A2 A3 A4 压榨试运行自动化空间
这个提交包含在:
@@ -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 {
|
||||
|
||||
在新工单中引用
屏蔽一个用户