|
- import assert from 'node:assert/strict'
- import { readFile } from 'node:fs/promises'
- import test from 'node:test'
-
- const webSource = new URL('../../unreal_tran_web/src/features/', import.meta.url)
- const editorSource = await readFile(new URL('guide-legacy/demo-src/training-editor.js', webSource), 'utf8')
- const stepCreationMethods = editorSource.slice(editorSource.indexOf(' addStep()'), editorSource.indexOf(' removeStep()'))
- const storageFixture = () => {
- const values = new Map()
- return { getItem: key => values.get(key) ?? null, setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key) }
- }
-
- for (const runtime of ['guide-legacy/demo-src', 'virtual-training-legacy/upstream']) {
- const { createTrainingProject, normalizeTrainingProject, validateTrainingProject, saveTrainingProject, loadTrainingProject } = await import(new URL(`${runtime}/training-project-io.js`, webSource))
-
- test(`${runtime}: physical authored configuration survives save and reopen`, () => {
- const project = createTrainingProject({ trainingMode: 'physical' })
- project.modeConfig = {
- stationName: 'Fox inspection station', equipmentCode: 'FOX-PHY-001', gatewayProtocol: 'MQTT',
- sensorChannels: 'pressure,temperature', safetyInterlocks: 'emergency stop,door lock',
- }
- const storage = storageFixture()
- const saved = saveTrainingProject(storage, project, { status: 'draft' })
- assert.deepEqual(loadTrainingProject(storage, saved.id).modeConfig, project.modeConfig)
- })
-
- test(`${runtime}: clearing optional physical text does not restore demo values`, () => {
- const project = createTrainingProject({ trainingMode: 'physical' })
- project.modeConfig = { stationName: '', equipmentCode: '', gatewayProtocol: '', sensorChannels: '', safetyInterlocks: '' }
- const storage = storageFixture()
- const saved = saveTrainingProject(storage, project, { status: 'draft' })
- assert.deepEqual(loadTrainingProject(storage, saved.id).modeConfig, project.modeConfig)
- const report = validateTrainingProject(saved)
- assert.ok(report.warnings.some(item => item.path === 'modeConfig.stationName'))
- assert.ok(report.warnings.some(item => item.path === 'modeConfig.equipmentCode'))
- })
-
- test(`${runtime}: confrontation configuration keeps a supported 50-member team and all authored text`, () => {
- const project = createTrainingProject({ trainingMode: 'confrontation' })
- project.modeConfig = {
- redTeamName: 'Red maintenance', blueTeamName: 'Blue inspection', teamSize: 50,
- roundDuration: 75, objective: 'Find fault', eventInjections: 'sensor failure', winRule: 'Score then elapsed time',
- }
- const storage = storageFixture()
- const saved = saveTrainingProject(storage, project, { status: 'draft' })
- const reloaded = loadTrainingProject(storage, saved.id)
- assert.deepEqual(reloaded.modeConfig, project.modeConfig)
- assert.equal(validateTrainingProject(reloaded).errors.some(item => item.path.startsWith('modeConfig.')), false)
- })
-
- test(`${runtime}: explicit empty confrontation text stays empty and empty win rule warns`, () => {
- const project = createTrainingProject({ trainingMode: 'confrontation' })
- for (const key of ['redTeamName', 'blueTeamName', 'objective', 'eventInjections', 'winRule']) project.modeConfig[key] = ''
- const normalized = normalizeTrainingProject(project)
- for (const key of ['redTeamName', 'blueTeamName', 'objective', 'eventInjections', 'winRule']) assert.equal(normalized.modeConfig[key], '')
- assert.ok(validateTrainingProject(normalized).warnings.some(item => item.path === 'modeConfig.winRule'))
- })
-
- test(`${runtime}: invalid team sizes remain visible and fail local validation instead of silently clamping`, () => {
- for (const teamSize of [0, -1, 2.5, 201]) {
- const project = createTrainingProject({ trainingMode: 'confrontation' })
- project.modeConfig.teamSize = teamSize
- const normalized = normalizeTrainingProject(project)
- assert.equal(normalized.modeConfig.teamSize, teamSize)
- assert.ok(validateTrainingProject(normalized).errors.some(item => item.path === 'modeConfig.teamSize'))
- }
- })
-
- test(`${runtime}: invalid duration fails but existing sub-minute and long durations are preserved`, () => {
- for (const roundDuration of [0, -1]) {
- const project = createTrainingProject({ trainingMode: 'confrontation' })
- project.modeConfig.roundDuration = roundDuration
- assert.ok(validateTrainingProject(project).errors.some(item => item.path === 'modeConfig.roundDuration'))
- }
- for (const roundDuration of [0.5, 1.5, 720]) {
- const project = createTrainingProject({ trainingMode: 'confrontation' })
- project.modeConfig.roundDuration = roundDuration
- assert.equal(normalizeTrainingProject(project).modeConfig.roundDuration, roundDuration)
- assert.equal(validateTrainingProject(project).errors.some(item => item.path === 'modeConfig.roundDuration'), false)
- }
- })
-
- test(`${runtime}: existing device event and team position contracts survive normalization and reorder`, () => {
- const project = createTrainingProject({ trainingMode: 'physical' })
- const contract = {
- stepCode: 'PHY-FOX-INSPECT', actionCode: 'FOX_INSPECT',
- physicalEventRule: { source: 'VISION', eventType: 'fox.inspection.done', progressPercent: 50, manualConfirmAllowed: false },
- allowedPositions: ['observer'], positionActions: { observer: ['FOX_INSPECT'] },
- }
- Object.assign(project.steps[0], structuredClone(contract))
- const stepId = project.steps[0].id
- const storage = storageFixture()
- const saved = saveTrainingProject(storage, project, { status: 'draft' })
- const reopened = loadTrainingProject(storage, saved.id)
- reopened.steps.reverse()
- const normalized = normalizeTrainingProject(reopened)
- const retained = normalized.steps.find(step => step.id === stepId)
- for (const key of Object.keys(contract)) assert.deepEqual(retained[key], contract[key])
- retained.physicalEventRule.source = 'MANUAL'
- assert.equal(project.steps[0].physicalEventRule.source, 'VISION')
- })
-
- test(`${runtime}: add and duplicate allocate a new step without copying its source runtime codes`, () => {
- const Harness = new Function('clone', 'normalizeTrainingProject', `return class { ${stepCreationMethods} }`)(structuredClone, normalizeTrainingProject)
- for (const method of ['addStep', 'duplicateStep']) {
- const editor = new Harness()
- editor.project = createTrainingProject({ trainingMode: 'physical' })
- editor.selectedStep = editor.project.steps[0]
- editor.selectedStep.stepCode = 'EXISTING-STEP'
- editor.selectedStep.actionCode = 'EXISTING_ACTION'
- const sourceId = editor.selectedStep.id
- for (const name of ['renumberSteps', 'markDirty', 'renderProjectHeader', 'renderScene', 'renderSteps', 'renderInspector', 'toast']) editor[name] = () => {}
- editor[method]()
- const created = editor.project.steps.find(step => step.id === editor.selectedStepId)
- const original = editor.project.steps.find(step => step.id === sourceId)
- assert.notEqual(created.id, original.id)
- assert.equal(created.stepCode, undefined)
- assert.equal(created.actionCode, undefined)
- assert.equal(original.stepCode, 'EXISTING-STEP')
- assert.equal(original.actionCode, 'EXISTING_ACTION')
- }
- })
- }
|