選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

124 行
7.2 KiB

  1. import assert from 'node:assert/strict'
  2. import { readFile } from 'node:fs/promises'
  3. import test from 'node:test'
  4. const webSource = new URL('../../unreal_tran_web/src/features/', import.meta.url)
  5. const editorSource = await readFile(new URL('guide-legacy/demo-src/training-editor.js', webSource), 'utf8')
  6. const stepCreationMethods = editorSource.slice(editorSource.indexOf(' addStep()'), editorSource.indexOf(' removeStep()'))
  7. const storageFixture = () => {
  8. const values = new Map()
  9. return { getItem: key => values.get(key) ?? null, setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key) }
  10. }
  11. for (const runtime of ['guide-legacy/demo-src', 'virtual-training-legacy/upstream']) {
  12. const { createTrainingProject, normalizeTrainingProject, validateTrainingProject, saveTrainingProject, loadTrainingProject } = await import(new URL(`${runtime}/training-project-io.js`, webSource))
  13. test(`${runtime}: physical authored configuration survives save and reopen`, () => {
  14. const project = createTrainingProject({ trainingMode: 'physical' })
  15. project.modeConfig = {
  16. stationName: 'Fox inspection station', equipmentCode: 'FOX-PHY-001', gatewayProtocol: 'MQTT',
  17. sensorChannels: 'pressure,temperature', safetyInterlocks: 'emergency stop,door lock',
  18. }
  19. const storage = storageFixture()
  20. const saved = saveTrainingProject(storage, project, { status: 'draft' })
  21. assert.deepEqual(loadTrainingProject(storage, saved.id).modeConfig, project.modeConfig)
  22. })
  23. test(`${runtime}: clearing optional physical text does not restore demo values`, () => {
  24. const project = createTrainingProject({ trainingMode: 'physical' })
  25. project.modeConfig = { stationName: '', equipmentCode: '', gatewayProtocol: '', sensorChannels: '', safetyInterlocks: '' }
  26. const storage = storageFixture()
  27. const saved = saveTrainingProject(storage, project, { status: 'draft' })
  28. assert.deepEqual(loadTrainingProject(storage, saved.id).modeConfig, project.modeConfig)
  29. const report = validateTrainingProject(saved)
  30. assert.ok(report.warnings.some(item => item.path === 'modeConfig.stationName'))
  31. assert.ok(report.warnings.some(item => item.path === 'modeConfig.equipmentCode'))
  32. })
  33. test(`${runtime}: confrontation configuration keeps a supported 50-member team and all authored text`, () => {
  34. const project = createTrainingProject({ trainingMode: 'confrontation' })
  35. project.modeConfig = {
  36. redTeamName: 'Red maintenance', blueTeamName: 'Blue inspection', teamSize: 50,
  37. roundDuration: 75, objective: 'Find fault', eventInjections: 'sensor failure', winRule: 'Score then elapsed time',
  38. }
  39. const storage = storageFixture()
  40. const saved = saveTrainingProject(storage, project, { status: 'draft' })
  41. const reloaded = loadTrainingProject(storage, saved.id)
  42. assert.deepEqual(reloaded.modeConfig, project.modeConfig)
  43. assert.equal(validateTrainingProject(reloaded).errors.some(item => item.path.startsWith('modeConfig.')), false)
  44. })
  45. test(`${runtime}: explicit empty confrontation text stays empty and empty win rule warns`, () => {
  46. const project = createTrainingProject({ trainingMode: 'confrontation' })
  47. for (const key of ['redTeamName', 'blueTeamName', 'objective', 'eventInjections', 'winRule']) project.modeConfig[key] = ''
  48. const normalized = normalizeTrainingProject(project)
  49. for (const key of ['redTeamName', 'blueTeamName', 'objective', 'eventInjections', 'winRule']) assert.equal(normalized.modeConfig[key], '')
  50. assert.ok(validateTrainingProject(normalized).warnings.some(item => item.path === 'modeConfig.winRule'))
  51. })
  52. test(`${runtime}: invalid team sizes remain visible and fail local validation instead of silently clamping`, () => {
  53. for (const teamSize of [0, -1, 2.5, 201]) {
  54. const project = createTrainingProject({ trainingMode: 'confrontation' })
  55. project.modeConfig.teamSize = teamSize
  56. const normalized = normalizeTrainingProject(project)
  57. assert.equal(normalized.modeConfig.teamSize, teamSize)
  58. assert.ok(validateTrainingProject(normalized).errors.some(item => item.path === 'modeConfig.teamSize'))
  59. }
  60. })
  61. test(`${runtime}: invalid duration fails but existing sub-minute and long durations are preserved`, () => {
  62. for (const roundDuration of [0, -1]) {
  63. const project = createTrainingProject({ trainingMode: 'confrontation' })
  64. project.modeConfig.roundDuration = roundDuration
  65. assert.ok(validateTrainingProject(project).errors.some(item => item.path === 'modeConfig.roundDuration'))
  66. }
  67. for (const roundDuration of [0.5, 1.5, 720]) {
  68. const project = createTrainingProject({ trainingMode: 'confrontation' })
  69. project.modeConfig.roundDuration = roundDuration
  70. assert.equal(normalizeTrainingProject(project).modeConfig.roundDuration, roundDuration)
  71. assert.equal(validateTrainingProject(project).errors.some(item => item.path === 'modeConfig.roundDuration'), false)
  72. }
  73. })
  74. test(`${runtime}: existing device event and team position contracts survive normalization and reorder`, () => {
  75. const project = createTrainingProject({ trainingMode: 'physical' })
  76. const contract = {
  77. stepCode: 'PHY-FOX-INSPECT', actionCode: 'FOX_INSPECT',
  78. physicalEventRule: { source: 'VISION', eventType: 'fox.inspection.done', progressPercent: 50, manualConfirmAllowed: false },
  79. allowedPositions: ['observer'], positionActions: { observer: ['FOX_INSPECT'] },
  80. }
  81. Object.assign(project.steps[0], structuredClone(contract))
  82. const stepId = project.steps[0].id
  83. const storage = storageFixture()
  84. const saved = saveTrainingProject(storage, project, { status: 'draft' })
  85. const reopened = loadTrainingProject(storage, saved.id)
  86. reopened.steps.reverse()
  87. const normalized = normalizeTrainingProject(reopened)
  88. const retained = normalized.steps.find(step => step.id === stepId)
  89. for (const key of Object.keys(contract)) assert.deepEqual(retained[key], contract[key])
  90. retained.physicalEventRule.source = 'MANUAL'
  91. assert.equal(project.steps[0].physicalEventRule.source, 'VISION')
  92. })
  93. test(`${runtime}: add and duplicate allocate a new step without copying its source runtime codes`, () => {
  94. const Harness = new Function('clone', 'normalizeTrainingProject', `return class { ${stepCreationMethods} }`)(structuredClone, normalizeTrainingProject)
  95. for (const method of ['addStep', 'duplicateStep']) {
  96. const editor = new Harness()
  97. editor.project = createTrainingProject({ trainingMode: 'physical' })
  98. editor.selectedStep = editor.project.steps[0]
  99. editor.selectedStep.stepCode = 'EXISTING-STEP'
  100. editor.selectedStep.actionCode = 'EXISTING_ACTION'
  101. const sourceId = editor.selectedStep.id
  102. for (const name of ['renumberSteps', 'markDirty', 'renderProjectHeader', 'renderScene', 'renderSteps', 'renderInspector', 'toast']) editor[name] = () => {}
  103. editor[method]()
  104. const created = editor.project.steps.find(step => step.id === editor.selectedStepId)
  105. const original = editor.project.steps.find(step => step.id === sourceId)
  106. assert.notEqual(created.id, original.id)
  107. assert.equal(created.stepCode, undefined)
  108. assert.equal(created.actionCode, undefined)
  109. assert.equal(original.stepCode, 'EXISTING-STEP')
  110. assert.equal(original.actionCode, 'EXISTING_ACTION')
  111. }
  112. })
  113. }