|
- import assert from 'node:assert/strict'
- import fs from 'node:fs'
- import path from 'node:path'
- import crypto from 'node:crypto'
- import { fileURLToPath } from 'node:url'
- import { api, localCredentials } from './review-api.mjs'
-
- // Development integration test. Creates isolated students, tasks and runs;
- // credentials stay in memory and no hardware adapter is contacted.
- // node tools/training-modes-api-live.mjs --physical 163:215 --confrontation 164:216
- const args = process.argv.slice(2)
- const parameter = name => args[args.indexOf(name) + 1]
- function reference(name) {
- if (!args.includes(name) || !/^\d+:\d+$/.test(parameter(name) || '')) throw new Error(`Required: ${name} projectId:publishedVersionId`)
- const [projectId, versionId] = parameter(name).split(':')
- return { projectId, versionId }
- }
- const waitForConfrontation = args.includes('--wait-confrontation')
- const references = { PHYSICAL: reference('--physical'), CONFRONTATION: waitForConfrontation ? null : reference('--confrontation') }
- const reportDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../reports/training-modes-20260905')
- const readyFile = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../.codex-tmp/training-modes-confrontation-ready.json')
- fs.mkdirSync(reportDir, { recursive: true })
- const evidencePath = path.join(reportDir, 'api-evidence.json')
- const suffix = Date.now().toString(36).toUpperCase()
- const prefix = `UTE2E-MODES-${suffix}`
- const evidence = {
- title: '实装人工事件与红蓝对抗正式 API 闭环', status: 'RUNNING',
- startedAt: new Date().toISOString(), baseURL: 'http://127.0.0.1:6180',
- method: '真实 Auth / Gateway / Tran API;新建隔离学生与开发测试任务;不连接设备、视觉推理或外部通知。',
- references, checks: [], assignments: [], runs: [], actors: [], requests: [],
- limitations: [
- '实装事件均为本次自动化提交的 MANUAL 人工确认或明确的权限拒绝测试,不代表真实设备、PLC、摄像机或视觉模型联调。',
- '教学运行结果来自正式 API;默认实装离线回放和默认对抗本地工作台的显示另行记录,不混同为服务端运行闭环。',
- '对抗按四名隔离学员的独立会话依次操作,检查服务端截止时间已生效;未等待整局自然超时,也不代表多人同时操作的压力或网络同步测试。',
- '测试账号使用本次随机密码,密码与会话仅在运行内存中;报告只保留用户 ID、队伍和岗位以便追溯。',
- ],
- }
- const credentials = localCredentials()
- const ephemeralPassword = `Tm!${crypto.randomBytes(8).toString('hex')}`
- const secretValues = [credentials.username, credentials.password, ephemeralPassword]
- const sessions = []
- let admin
- const save = () => fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`)
- function safeError(error) {
- let result = error instanceof Error ? error.message : String(error)
- for (const value of secretValues) if (value) result = result.split(value).join('[redacted]')
- return result.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]').replace(/eyJ[\w-]+\.[\w-]+\.[\w-]+/g, '[redacted]')
- }
- async function call(url, actor, body, method) {
- const result = await api(url, typeof actor === 'string' ? actor : actor?.token, body, method)
- if (url.startsWith('/api/tran/v1/teaching/')) evidence.requests.push({ method: method || (body === undefined ? 'GET' : 'POST'), path: url, status: result.status })
- return result
- }
- async function request(url, actor, body, method) {
- const result = await call(url, actor, body, method)
- if (result.status !== 200) throw new Error(`${method || (body === undefined ? 'GET' : 'POST')} ${url}: HTTP ${result.status}; ${safeError(result.message || '')}`)
- return result.data
- }
- async function denied(url, actor, body, expected, method) {
- const result = await call(url, actor, body, method)
- assert.ok([expected].flat().includes(result.status), `${method || 'POST'} ${url}: expected ${expected}, received ${result.status}: ${safeError(result.message || '')}`)
- return { status: result.status, message: safeError(result.message || '') }
- }
- function passed(title, details) { evidence.checks.push({ title, status: 'PASS', details }); save() }
- async function section(title, fn) {
- try { await fn() } catch (error) {
- evidence.checks.push({ title, status: 'FAIL', details: safeError(error) }); save()
- }
- }
- const uuid = () => crypto.randomUUID()
- const teaching = relative => `/api/tran/v1/teaching${relative}`
- const codeOf = step => String(step.stepCode || step.code || step.id || '')
- const actionOf = step => String(step.actionCode || step.action?.code || '')
- const slimRun = run => ({ id: run.id, assignmentId: run.assignmentId, status: run.status, version: run.version, progressPercent: run.progressPercent, currentStepCode: run.currentStepCode, score: run.score, passed: run.passed })
- const slimFacts = facts => facts.map(fact => ({ stepCode: fact.stepCode, status: fact.statusCode, targetProgressPercent: fact.targetProgressPercent, attemptCount: fact.attemptCount, failureCount: fact.failureCount, evidenceSourceCode: fact.evidenceSourceCode }))
- const rowsOf = value => value?.records || value || []
- const runOf = (id, actor) => request(teaching(`/runs/${id}`), actor)
- const factsOf = (id, actor) => request(teaching(`/runs/${id}/steps`), actor)
- function rememberRun(run, extra = {}) {
- const entry = { ...slimRun(run), ...extra }
- const index = evidence.runs.findIndex(item => item.id === run.id)
- if (index >= 0) evidence.runs[index] = { ...evidence.runs[index], ...entry }
- else evidence.runs.push(entry)
- save()
- }
- async function loginAdmin() {
- const login = await request('/api/auth/v1/auth/login', null, { ...credentials, roleCode: 'admin', rememberMe: false })
- admin = { token: login.accessToken }
- secretValues.push(admin.token)
- sessions.push(admin)
- }
- async function createActors() {
- const roles = rowsOf(await request('/api/auth/v1/roles?size=100', admin))
- const role = roles.find(item => item.code === 'student')
- assert.ok(role?.id, 'student role must exist')
- const context = await request('/api/auth/v1/auth/login-context', admin)
- const department = context.departments?.find(item => item.children?.length)?.children?.[0] || context.departments?.[0]
- assert.ok(department?.id, 'An active department must exist')
- const actors = []
- for (const team of ['RED', 'BLUE']) for (const position of ['COMMANDER', 'OPERATOR']) {
- const username = `tm_${suffix.toLowerCase()}_${team[0].toLowerCase()}${position[0].toLowerCase()}`
- secretValues.push(username)
- const created = await request('/api/auth/v1/users', admin, {
- username, displayName: `模式实测-${suffix}-${team}-${position}`,
- departmentId: department.id, roleIds: [role.id], defaultRoleId: role.id,
- enabled: true, password: ephemeralPassword, remark: `${prefix}; isolated development API test; no hardware`,
- })
- const user = created.user || created
- assert.ok(user.id, 'New student must have an id')
- evidence.actors.push({ userId: String(user.id), team, position, departmentId: String(department.id) })
- save()
- const logged = await request('/api/auth/v1/auth/login', null, { userId: user.id, roleCode: 'student', password: ephemeralPassword, rememberMe: false })
- const actor = { id: String(user.id), token: logged.accessToken, team, position }
- secretValues.push(actor.token)
- sessions.push(actor)
- actors.push(actor)
- }
- return actors
- }
- async function contentReference(channel) {
- const reference = references[channel]
- const detail = await request(`/api/tran/v1/content/projects/${reference.projectId}`, admin)
- const project = detail.project || detail
- assert.equal(project.status, 'PUBLISHED')
- assert.equal(String(project.publishedVersionId), reference.versionId)
- const version = await request(`/api/tran/v1/content/projects/${reference.projectId}/versions/${reference.versionId}`, admin)
- const content = version.content || version.version?.content
- assert.ok(content && Array.isArray(content.steps) && content.steps.length > 1, `${channel}: published steps required`)
- return { reference, content, sceneDependencies: (version.dependencies || []).filter(dependency => dependency.relationType === 'TRAINING_SCENE') }
- }
- async function verifyFrozenSnapshots(profiles) {
- assert.equal(evidence.assignments.length, 2, 'Both isolated teaching assignments must exist')
- const snapshots = []
- for (const assignment of evidence.assignments) {
- const profile = profiles[assignment.channel]
- const current = await request(teaching(`/assignments/${assignment.id}`), admin)
- assert.equal(String(current.contentProjectId), profile.reference.projectId)
- assert.equal(String(current.contentVersionId), profile.reference.versionId)
- assert.equal(current.definitionSnapshot?.trainingMode, assignment.channel)
- assert.deepEqual(current.definitionSnapshot.steps.map(codeOf), profile.content.steps.map(codeOf))
- assert.equal(profile.sceneDependencies.length, 1)
- const scene = current.definitionSnapshot.teachingScene
- const expected = profile.sceneDependencies[0]
- assert.equal(String(scene.sourceProjectId), String(expected.targetProjectId))
- assert.equal(String(scene.sourceVersionId), String(expected.targetVersionId))
- const models = scene.objects.filter(object => object.targetProjectId).map(object => ({ projectId: object.targetProjectId, versionId: object.targetVersionId, assetCode: object.assetCode }))
- assert.ok(models.length && models.every(model => model.versionId && model.assetCode))
- const frozenScene = { projectId: scene.sourceProjectId, versionId: scene.sourceVersionId, models }
- assignment.frozenScene = frozenScene
- snapshots.push({ assignmentId: assignment.id, channel: assignment.channel, content: profile.reference, frozenScene })
- }
- passed('正式任务快照固定训练、场景与模型发布版本', { snapshots })
- }
- async function waitForConfrontationPublication() {
- evidence.waitingFor = 'CONFRONTATION published project/version reference from the UI test owner'
- save()
- console.log(JSON.stringify({ phase: 'WAITING_FOR_CONFRONTATION', physicalChecks: evidence.checks.length, readyFile }))
- const until = Date.now() + 30 * 60 * 1000
- while (Date.now() < until) {
- if (fs.existsSync(readyFile) && fs.statSync(readyFile).mtimeMs >= Date.parse(evidence.startedAt)) {
- const ready = JSON.parse(fs.readFileSync(readyFile, 'utf8').replace(/^\uFEFF/, ''))
- assert.match(String(ready.projectId), /^\d+$/)
- assert.match(String(ready.versionId), /^\d+$/)
- references.CONFRONTATION = { projectId: String(ready.projectId), versionId: String(ready.versionId) }
- delete evidence.waitingFor
- save()
- return
- }
- await new Promise(resolve => setTimeout(resolve, 1500))
- }
- throw new Error('Timed out waiting for the published CONFRONTATION UI reference; PHYSICAL evidence and all retained IDs are preserved')
- }
- async function newAssignment(channel, profile, actors) {
- const members = channel === 'PHYSICAL'
- ? [{ userId: actors[0].id, memberType: 'LEARNER', teamCode: 'NEUTRAL', positionCode: 'OPERATOR', sortOrder: 0 }]
- : actors.map((actor, index) => ({ userId: actor.id, memberType: 'LEARNER', teamCode: actor.team, positionCode: actor.position, sortOrder: index }))
- const created = await request(teaching('/assignments'), admin, {
- code: `${prefix}-${channel}`, name: `开发实测-${channel === 'PHYSICAL' ? '实装人工事件' : '红蓝岗位对抗'}-${suffix}`,
- description: '开发环境正式 API 流程验证;人工/模拟事件,不代表设备联调或实际技能评定。',
- channel, assignmentKind: 'TRAINING', executionMode: 'PRACTICE', audienceType: 'ASSIGNED',
- collaborationMode: channel === 'PHYSICAL' ? 'INDIVIDUAL' : 'TEAM', digitalHumanAllowed: false,
- contentProjectId: profile.reference.projectId, contentVersionId: profile.reference.versionId, members,
- })
- evidence.assignments.push({ id: created.id, code: created.code, channel, contentProjectId: profile.reference.projectId, contentVersionId: profile.reference.versionId, status: created.status })
- save()
- const published = await request(teaching(`/assignments/${created.id}/publish`), admin, { version: created.version })
- evidence.assignments.at(-1).status = published.status
- evidence.assignments.at(-1).version = published.version
- save()
- return published
- }
- async function accept(assignment, actor, physical = false) {
- const run = await request(teaching(`/assignments/${assignment.id}/accept`), actor, {
- assignmentVersion: assignment.version, teamCode: physical ? 'NEUTRAL' : actor.team,
- positionCode: physical ? 'OPERATOR' : actor.position,
- })
- rememberRun(run, { channel: assignment.channel, team: physical ? 'NEUTRAL' : actor.team })
- return run
- }
- async function startWithReplay(run, actor) {
- const body = { version: run.version, commandId: uuid() }
- const started = await request(teaching(`/runs/${run.id}/start`), actor, body)
- const replay = await request(teaching(`/runs/${run.id}/start`), actor, body)
- assert.equal(replay.version, started.version)
- const facts = await factsOf(run.id, actor)
- assert.equal(facts[0].attemptCount, 1)
- assert.equal(facts[0].statusCode, 'IN_PROGRESS')
- passed('开始命令重放不会重复激活或递增次数', { runId: run.id, version: started.version, attemptCount: facts[0].attemptCount })
- return started
- }
- async function submitAndReview(run, actor, channel) {
- const body = { version: run.version, commandId: uuid(), summary: '开发自动化合同测试完成;不代表真实技能训练。', evidence: { testOnly: true, source: 'development-api-contract' }, payload: {} }
- const submitted = await request(teaching(`/runs/${run.id}/submit`), actor, body)
- const replay = await request(teaching(`/runs/${run.id}/submit`), actor, body)
- assert.equal(submitted.status, 'SUBMITTED')
- assert.equal(replay.version, submitted.version)
- const reviewBody = { version: submitted.version, commandId: uuid(), score: 100, passed: true, feedback: '仅测试正式 API 评定留痕,不代表实际维修能力。', rubric: { testOnly: true } }
- const reviewed = await request(teaching(`/runs/${run.id}/review`), admin, reviewBody)
- const reviewReplay = await request(teaching(`/runs/${run.id}/review`), admin, reviewBody)
- assert.equal(reviewed.status, 'REVIEWED')
- assert.equal(reviewReplay.version, reviewed.version)
- const facts = await factsOf(run.id, admin)
- assert.ok(facts.every(fact => fact.statusCode === 'COMPLETED'))
- rememberRun(reviewed, { channel, stepFacts: slimFacts(facts) })
- passed(`${channel} 提交、教员评定与结果回读`, { run: slimRun(reviewed), submitReplay: true, reviewReplay: true, facts: slimFacts(facts) })
- return reviewed
- }
- async function physicalFlow(profile, actors) {
- const steps = profile.content.steps
- assert.ok(steps.every(step => step.physicalEventRule?.source === 'MANUAL'), 'This test only drives explicitly configured MANUAL rules')
- assert.deepEqual(steps.map(step => step.physicalEventRule.progressPercent), steps.map((_, index) => Math.round((index + 1) * 100 / steps.length)))
- const assignment = await newAssignment('PHYSICAL', profile, actors)
- const actor = actors[0]
- let run = await accept(assignment, actor, true)
- run = await startWithReplay(run, actor)
- passed('实装任务固定发布版本、分配学员并开始', { assignmentId: assignment.id, run: slimRun(run), content: profile.reference, rules: steps.map(step => ({ stepCode: codeOf(step), source: step.physicalEventRule.source, eventType: step.physicalEventRule.eventType, progressPercent: step.physicalEventRule.progressPercent })) })
- const eventBody = (step, extra = {}) => ({ eventId: `MANUAL-${uuid()}`, runVersion: run.version, source: 'MANUAL', eventType: step.physicalEventRule.eventType, stepCode: codeOf(step), manualConfirmed: true, occurredAt: Math.floor(Date.now() / 1000), publicPayload: { testOnly: true, evidenceKind: 'api-contract-simulation' }, ...extra })
- const forbiddenProgress = await denied(teaching(`/runs/${run.id}/progress`), actor, { version: run.version, commandId: uuid(), completedStepCode: codeOf(steps[0]), actionCode: actionOf(steps[0]) }, 403, 'PUT')
- const prematureSubmit = await denied(teaching(`/runs/${run.id}/submit`), actor, { version: run.version, commandId: uuid(), summary: 'negative test' }, 409)
- const skipped = await denied(teaching(`/runs/${run.id}/physical-events`), actor, eventBody(steps[1]), 409)
- const untrustedDevice = await denied(teaching(`/runs/${run.id}/physical-events`), actor, eventBody(steps[0], { source: 'DEVICE' }), 403)
- const managerManual = await denied(teaching(`/runs/${run.id}/physical-events`), admin, eventBody(steps[0]), 403)
- assert.equal((await runOf(run.id, actor)).progressPercent, 0)
- passed('实装拒绝普通进度接口、抢跑、提前提交及错误事件身份', { runId: run.id, forbiddenProgress, prematureSubmit, skipped, untrustedDevice, managerManual, progressUnchanged: 0 })
-
- const failed = await request(teaching(`/runs/${run.id}/physical-events`), actor, eventBody(steps[0], { manualConfirmed: false }))
- assert.equal(failed.outcome, 'UNMATCHED')
- assert.equal(failed.progressApplied, false)
- let facts = await factsOf(run.id, actor)
- assert.equal(facts[0].statusCode, 'FAILED')
- assert.equal(facts[0].failureCount, 1)
- run = await runOf(run.id, actor)
- const beforeRetry = await denied(teaching(`/runs/${run.id}/physical-events`), actor, eventBody(steps[0]), 409)
- run = await request(teaching(`/runs/${run.id}/start`), actor, { version: run.version, commandId: uuid() })
- facts = await factsOf(run.id, actor)
- assert.equal(facts[0].attemptCount, 2)
- passed('实装不匹配事件失败,必须重试后才可继续', { runId: run.id, outcome: failed.outcome, beforeRetry, facts: slimFacts(facts) })
-
- const progression = []
- for (const [index, step] of steps.entries()) {
- const body = eventBody(step)
- const result = await request(teaching(`/runs/${run.id}/physical-events`), actor, body)
- assert.equal(result.outcome, 'MATCHED')
- assert.equal(result.progressApplied, true)
- const after = await runOf(run.id, actor)
- assert.equal(after.progressPercent, Math.round((index + 1) * 100 / steps.length))
- const replay = await request(teaching(`/runs/${run.id}/physical-events`), actor, body)
- assert.equal(replay.runVersion, after.version)
- assert.equal((await runOf(run.id, actor)).version, after.version)
- const differentPayload = await denied(teaching(`/runs/${run.id}/physical-events`), actor, { ...body, publicPayload: { testOnly: true, altered: true } }, 409)
- progression.push({ stepCode: codeOf(step), eventId: result.eventId, progress: after.progressPercent, replayVersion: replay.runVersion, alteredEventStatus: differentPayload.status })
- run = after
- }
- facts = await factsOf(run.id, actor)
- assert.ok(facts.every(fact => fact.statusCode === 'COMPLETED' && fact.evidenceSourceCode === 'MANUAL'))
- passed('实装逐步人工确认、事件幂等与载荷冲突', { runId: run.id, progression, facts: slimFacts(facts) })
- await submitAndReview(run, actor, 'PHYSICAL')
- }
- async function confrontationFlow(profile, actors) {
- const configuration = profile.content.channelProfiles?.CONFRONTATION || profile.content.modeConfig
- assert.equal(configuration.teamSize, 2, 'Isolated test expects two learners per team')
- assert.ok(configuration.submitPositions.includes('COMMANDER'))
- assert.ok(profile.content.steps.every(step => step.allowedPositions?.includes('COMMANDER') && !step.allowedPositions?.includes('OPERATOR')), 'Each tested action must be COMMANDER-only for the negative role test')
- const assignment = await newAssignment('CONFRONTATION', profile, actors)
- const runs = new Map()
- for (const actor of actors) {
- const run = await accept(assignment, actor)
- if (runs.has(actor.team)) assert.equal(run.id, runs.get(actor.team).id)
- runs.set(actor.team, run)
- }
- assert.notEqual(runs.get('RED').id, runs.get('BLUE').id)
- const listed = rowsOf(await request(teaching(`/runs?assignmentId=${assignment.id}&size=100`), admin))
- assert.equal(listed.length, 2)
- passed('对抗红蓝两队各两人,按队伍共享且运行互相独立', { assignmentId: assignment.id, content: profile.reference, configuration: { teamSize: configuration.teamSize, roundDurationMinutes: configuration.roundDurationMinutes, submitPositions: configuration.submitPositions }, runs: listed.map(slimRun) })
- const redCommander = actors.find(actor => actor.team === 'RED' && actor.position === 'COMMANDER')
- const crossTeam = await denied(teaching(`/runs/${runs.get('BLUE').id}`), redCommander, undefined, 403, 'GET')
- passed('对抗学员不能读取另一队运行明细', { requestingTeam: 'RED', otherRunId: runs.get('BLUE').id, response: crossTeam })
- for (const team of ['RED', 'BLUE']) {
- const commander = actors.find(actor => actor.team === team && actor.position === 'COMMANDER')
- const operator = actors.find(actor => actor.team === team && actor.position === 'OPERATOR')
- let run = await startWithReplay(runs.get(team), commander)
- const execution = await request(teaching(`/runs/${run.id}/execution`), commander)
- assert.ok(Number.isFinite(execution.deadlineAt) && execution.deadlineAt > execution.serverNow, 'Server deadline must be active')
- const steps = profile.content.steps
- const progressBody = step => ({ version: run.version, commandId: uuid(), completedStepCode: codeOf(step), actionCode: actionOf(step), checkpoint: { testOnly: true }, automaticResult: {} })
- const wrongPosition = await denied(teaching(`/runs/${run.id}/progress`), operator, progressBody(steps[0]), 403, 'PUT')
- const skipped = await denied(teaching(`/runs/${run.id}/progress`), commander, progressBody(steps[1]), [400, 409], 'PUT')
- const earlySubmit = await denied(teaching(`/runs/${run.id}/submit`), commander, { version: run.version, commandId: uuid(), summary: 'negative test' }, 409)
- assert.equal((await runOf(run.id, commander)).progressPercent, 0)
- passed(`${team} 队岗位越权、跳步和提前提交均拒绝`, { runId: run.id, wrongPosition, skipped, earlySubmit, deadlineAt: execution.deadlineAt, serverNow: execution.serverNow })
- const progression = []
- for (const [index, step] of steps.entries()) {
- const body = progressBody(step)
- const advanced = await request(teaching(`/runs/${run.id}/progress`), commander, body, 'PUT')
- const replay = await request(teaching(`/runs/${run.id}/progress`), commander, body, 'PUT')
- assert.equal(replay.version, advanced.version)
- assert.equal(advanced.progressPercent, Math.round((index + 1) * 100 / steps.length))
- const conflict = await denied(teaching(`/runs/${run.id}/progress`), commander, { ...body, checkpoint: { testOnly: true, altered: true } }, 409, 'PUT')
- progression.push({ stepCode: codeOf(step), progress: advanced.progressPercent, replayVersion: replay.version, changedCommandStatus: conflict.status })
- run = advanced
- }
- const wrongSubmitPosition = await denied(teaching(`/runs/${run.id}/submit`), operator, { version: run.version, commandId: uuid(), summary: 'negative role test' }, 403)
- passed(`${team} 队授权动作逐步推进、重放幂等、提交负责人限制`, { runId: run.id, progression, wrongSubmitPosition })
- await submitAndReview(run, commander, 'CONFRONTATION')
- if (team === 'RED') {
- const blue = await runOf(runs.get('BLUE').id, admin)
- assert.equal(blue.progressPercent, 0)
- passed('红队完成不会推进蓝队进度', { redRunId: run.id, blueRun: slimRun(blue) })
- }
- }
- }
-
- save()
- try {
- await loginAdmin()
- const physical = await contentReference('PHYSICAL')
- let confrontation = waitForConfrontation ? null : await contentReference('CONFRONTATION')
- const actors = await createActors()
- await section('PHYSICAL 正式流程', () => physicalFlow(physical, actors))
- if (waitForConfrontation) {
- await waitForConfrontationPublication()
- confrontation = await contentReference('CONFRONTATION')
- }
- await section('CONFRONTATION 正式流程', () => confrontationFlow(confrontation, actors))
- await section('固定发布快照回读', () => verifyFrozenSnapshots({ PHYSICAL: physical, CONFRONTATION: confrontation }))
- } catch (error) {
- evidence.checks.push({ title: '测试准备', status: 'FAIL', details: safeError(error) })
- } finally {
- for (const actor of sessions.reverse()) await api('/api/auth/v1/auth/logout', actor.token, {}).catch(() => {})
- evidence.status = evidence.checks.some(check => check.status === 'FAIL') ? 'FAIL' : 'PASS'
- evidence.finishedAt = new Date().toISOString()
- evidence.retainedData = { assignments: evidence.assignments.map(item => item.id), runs: evidence.runs.map(item => item.id), users: evidence.actors.map(item => item.userId), note: '仅新建本次开发测试数据,未修改或删除既有账号、任务与运行。' }
- save()
- console.log(JSON.stringify({ status: evidence.status, checks: evidence.checks.length, passed: evidence.checks.filter(check => check.status === 'PASS').length, evidence: 'reports/training-modes-20260905/api-evidence.json', retainedData: evidence.retainedData }))
- if (evidence.status !== 'PASS') process.exitCode = 1
- }
|