import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { createServer } from 'vite'; const storage = new Map(); let failCanonicalSlotWrites = false; globalThis.window = { localStorage: { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { if ( failCanonicalSlotWrites && String(key).includes(':slot-') ) { throw new Error( 'Simulated canonical campaign save-slot failure.' ); } storage.set(key, String(value)); }, removeItem(key) { storage.delete(key); }, clear() { storage.clear(); } } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true }, appType: 'custom' }); try { const campaign = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const story = await server.ssrLoadModule( '/src/game/state/campaignStoryCheckpoint.ts' ); const campaignFlow = await server.ssrLoadModule( '/src/game/data/campaignFlow.ts' ); const scenario = await server.ssrLoadModule( '/src/game/data/scenario.ts' ); const pages = [ { id: 'sequence-new', legacyBeatIds: ['sequence-old'], chapter: '회귀 검증', background: 'story-chaos', text: '첫 장면' }, { id: 'beat-inserted', chapter: '회귀 검증', background: 'story-chaos', text: '나중에 삽입된 장면' }, { id: 'beat-final', legacyBeatIds: ['beat-old'], chapter: '회귀 검증', background: 'story-chaos', text: '이어 읽어야 할 장면' } ]; verifyPhaseContract(); verifyStableBeatResolution(); verifyLegacyCheckpointMigration(); verifyUnknownBeatIsRejected(); verifyStoryRouteResolution(); verifyStoryExplorationMutualExclusion(); verifyBeginCampaignStoryAtomicCommit(); verifyTutorialStoryAtomicCommit(); verifyHandoffRejectsLateReadingWrites(); verifyExactHandoffConsumption(); verifyLegacyFirstVictoryStoryTransition(); verifyExactClearRejectsStaleRevision(); verifyInvalidDirectWriteIsCompleteNoop(); verifyCanonicalStorageFailureIsAtomic(); verifyDestinationOwnedHandoffContract(); console.log( 'Verified campaign story checkpoints: stable-beat and legacy migration, reading/handoff phases, story/exploration exclusion, atomic story entry commits, stale-battle retirement, exact destination-owned handoff consumption, legacy victory-step advancement, stale-revision races, invalid-write no-ops, and canonical storage-failure invariance.' ); function verifyPhaseContract() { assert.deepEqual( story.campaignStoryCheckpointPhases, ['reading', 'handoff'], 'Story checkpoint phases must remain the explicit reading/handoff pair.' ); } function verifyStableBeatResolution() { const checkpoint = normalizedCheckpoint({ revision: 3, sequenceId: 'sequence-new', beatId: 'beat-final', pageIndex: 0 }); const resolution = story.resolveCampaignStoryCheckpoint( checkpoint, pages, 'prologue' ); assert.deepEqual( resolution, { pageIndex: 2, sequenceId: 'sequence-new', beatId: 'beat-final', migrated: true }, 'A stable beat ID must win over a conflicting legacy page index.' ); } function verifyLegacyCheckpointMigration() { const rawState = initialState('prologue'); rawState.updatedAt = '2026-07-28T10:00:00.000Z'; rawState.storyCheckpointRevision = 7; rawState.storyCheckpoint = { version: 0, sequenceId: 'sequence-new', legacyPageIndex: 2, savedAt: '2026-07-28T10:01:00.000Z' }; const loaded = loadRawState(rawState); assert.deepEqual( loaded.storyCheckpoint, { version: 1, revision: 7, sequenceId: 'sequence-new', phase: 'reading', pageIndex: 2, campaignStep: 'prologue', continuationIntent: 'default', savedAt: '2026-07-28T10:01:00.000Z' }, 'A legacy pageIndex-only checkpoint must normalize without losing its position.' ); assert.deepEqual( story.resolveCampaignStoryCheckpoint( loaded.storyCheckpoint, pages, loaded.step ), { pageIndex: 2, sequenceId: 'sequence-new', beatId: 'beat-final', migrated: true }, 'A legacy pageIndex-only checkpoint must migrate to the canonical beat ID.' ); const renamedCheckpoint = normalizedCheckpoint({ revision: 8, sequenceId: 'sequence-old', beatId: 'beat-old', pageIndex: 0 }); assert.deepEqual( story.resolveCampaignStoryCheckpoint( renamedCheckpoint, pages, 'prologue' ), { pageIndex: 2, sequenceId: 'sequence-new', beatId: 'beat-final', migrated: true }, 'Legacy sequence and beat aliases must migrate to canonical IDs.' ); } function verifyUnknownBeatIsRejected() { const checkpoint = normalizedCheckpoint({ revision: 9, sequenceId: 'sequence-new', beatId: 'beat-removed', pageIndex: 2 }); assert.equal( story.resolveCampaignStoryCheckpoint( checkpoint, pages, 'prologue' ), undefined, 'An unknown stable beat must not fall back to an unrelated page index.' ); } function verifyStoryRouteResolution() { const cases = [ { label: 'ordinary battle intro', pages: scenario.secondBattleIntroPages, step: 'second-battle', nextBattleId: 'second-battle-yellow-turban-pursuit' }, { label: 'Hanzhong king council', pages: scenario.hanzhongKingCouncilPages, step: 'hanzhong-king-camp' }, { label: 'Shu Han foundation', pages: scenario.shuHanFoundationPages, step: 'shu-han-foundation-camp' }, { label: 'Baidi entrustment', pages: scenario.baidiEntrustmentPages, step: 'baidi-entrustment-camp' }, { label: 'northern campaign preparation', pages: scenario.northernCampaignPrepPages, step: 'northern-campaign-prep-camp' } ]; for (const routeCase of cases) { const sequenceId = story.storySequenceIdForPages( routeCase.pages ); const resolved = campaignFlow.getSortieFlowForStorySequence( sequenceId, routeCase.step ); assert( resolved, `${routeCase.label} must resolve from its stable story sequence.` ); assert.equal( resolved.campaignStep, routeCase.step, `${routeCase.label} resolved to the wrong campaign step.` ); assert.equal( resolved.nextBattleId, routeCase.nextBattleId, `${routeCase.label} resolved to the wrong destination battle.` ); assert.equal( story.storySequenceIdForPages(resolved.pages), sequenceId, `${routeCase.label} resolved to a different story sequence.` ); } } function verifyStoryExplorationMutualExclusion() { resetTo(initialState('prologue-town')); const exploring = campaign.setCampaignExplorationCheckpoint( villageCheckpoint() ); assert(exploring.explorationCheckpoint); assert.equal(exploring.storyCheckpoint, undefined); const reading = campaign.setCampaignStoryCheckpoint( storyWrite('village-story', 'village-story', 0) ); assert(reading.storyCheckpoint); assert.equal( reading.explorationCheckpoint, undefined, 'Starting a story must retire the exploration checkpoint.' ); assertPersistedState( reading, 'story replacing exploration' ); const storyRevision = reading.storyCheckpointRevision; const exploringAgain = campaign.setCampaignExplorationCheckpoint( villageCheckpoint() ); assert(exploringAgain.explorationCheckpoint); assert.equal( exploringAgain.storyCheckpoint, undefined, 'Starting exploration must retire the story checkpoint.' ); assert.equal( exploringAgain.storyCheckpointRevision, storyRevision + 1, 'Retiring a story checkpoint must invalidate its revision.' ); assertPersistedState( exploringAgain, 'exploration replacing story' ); } function verifyBeginCampaignStoryAtomicCommit() { resetTo(initialState('prologue-town')); const exploring = campaign.setCampaignExplorationCheckpoint( villageCheckpoint() ); const previousRevision = exploring.storyCheckpointRevision; const previousBattleGeneration = exploring.battleResumeGeneration; const staleBattleSaveKey = `heros-web:battle:first-battle-zhuo-commandery:slot-${exploring.activeSaveSlot}`; storage.set( staleBattleSaveKey, '{"fixture":"superseded battle"}' ); const begun = campaign.beginCampaignStory( 'first-battle', storyWrite( 'first-battle-opening', 'first-battle-opening', 0 ), { startBattle: true } ); assert.equal(begun.step, 'first-battle'); assert.equal(begun.explorationCheckpoint, undefined); assert.equal(begun.activeCityStayId, undefined); assert.equal(begun.activeCampVisitId, undefined); assert.equal( begun.storyCheckpointRevision, previousRevision + 1 ); assert.equal( begun.battleResumeGeneration, previousBattleGeneration + 1, 'A new battle story must advance the durable battle-resume generation.' ); assert.deepEqual( storySnapshot(begun), { sequenceId: 'first-battle-opening', beatId: 'first-battle-opening', pageIndex: 0, campaignStep: 'first-battle', phase: 'reading' }, 'beginCampaignStory must save the new step and first beat together.' ); assert.equal( begun.updatedAt, begun.storyCheckpoint.savedAt ); assertPersistedState( begun, 'beginCampaignStory atomic commit' ); assert.equal( storage.has(staleBattleSaveKey), false, 'A committed story departure must retire the superseded battle resume.' ); } function verifyTutorialStoryAtomicCommit() { resetTo(initialState('prologue-town')); const exploring = campaign.setCampaignExplorationCheckpoint( villageCheckpoint() ); const previousRevision = exploring.storyCheckpointRevision; const tutorialId = campaign.prologueVillageCampaignTutorialIds.complete; const completed = campaign.completeCampaignTutorial( tutorialId, { clearExplorationCheckpointScene: 'prologue-village', storyCheckpoint: storyWrite( 'volunteers-rise', 'volunteers-rise', 0 ) } ); assert( completed.completedTutorialIds.includes(tutorialId), 'The tutorial completion must be part of the committed state.' ); assert.equal(completed.explorationCheckpoint, undefined); assert.equal( completed.storyCheckpointRevision, previousRevision + 1 ); assert.deepEqual( storySnapshot(completed), { sequenceId: 'volunteers-rise', beatId: 'volunteers-rise', pageIndex: 0, campaignStep: 'prologue-town', phase: 'reading' }, 'completeCampaignTutorial must save completion and the first story beat together.' ); assert.equal( completed.updatedAt, completed.storyCheckpoint.savedAt ); assertPersistedState( completed, 'tutorial and story atomic commit' ); } function verifyHandoffRejectsLateReadingWrites() { resetTo(initialState('prologue')); const reading = campaign.setCampaignStoryCheckpoint( storyWrite('handoff-story', 'handoff-story', 0) ); const revision = reading.storyCheckpoint.revision; const handedOff = campaign.markCampaignStoryHandoff( 'handoff-story', revision ); assert.equal( handedOff.storyCheckpoint.phase, 'handoff' ); assertPersistedState(handedOff, 'story handoff'); assertCompleteNoop( () => campaign.setCampaignStoryCheckpoint( storyWrite( 'handoff-story', 'late-reading', 1, revision ) ), 'A late revision-bound reading write after handoff' ); assertCompleteNoop( () => campaign.setCampaignStoryCheckpoint( storyWrite( 'handoff-story', 'pagehide-reading', 2 ) ), 'A late pagehide reading write without a revision after handoff' ); } function verifyExactHandoffConsumption() { resetTo(initialState('prologue')); const reading = campaign.setCampaignStoryCheckpoint( storyWrite( 'destination-owned-story', 'destination-owned-story', 0 ) ); const reference = { sequenceId: reading.storyCheckpoint.sequenceId, revision: reading.storyCheckpoint.revision }; assertCompleteNoop( () => campaign.consumeCampaignStoryHandoff( reference ), 'A destination trying to consume a reading checkpoint' ); const handedOff = campaign.markCampaignStoryHandoff( reference.sequenceId, reference.revision ); assert.equal( handedOff.storyCheckpoint.phase, 'handoff' ); assertCompleteNoop( () => campaign.consumeCampaignStoryHandoff({ ...reference, revision: reference.revision + 1 }), 'A stale destination handoff reference' ); assertCompleteNoop( () => campaign.consumeCampaignStoryHandoff({ ...reference, sequenceId: 'different-story' }), 'A mismatched destination handoff reference' ); const consumed = campaign.consumeCampaignStoryHandoff( reference ); assert.equal(consumed.storyCheckpoint, undefined); assert.equal( consumed.storyCheckpointRevision, reference.revision + 1, 'An exact destination handoff consume must invalidate its revision.' ); assertPersistedState( consumed, 'exact destination handoff consumption' ); assertCompleteNoop( () => campaign.consumeCampaignStoryHandoff( reference ), 'A repeated destination handoff consume' ); } function verifyLegacyFirstVictoryStoryTransition() { const legacyState = initialState( 'first-victory-story' ); legacyState.gold = 321; resetTo(legacyState); const reading = campaign.setCampaignStoryCheckpoint( storyWrite( 'legacy-first-victory', 'legacy-first-victory', 0 ) ); const reference = { sequenceId: reading.storyCheckpoint.sequenceId, revision: reading.storyCheckpoint.revision }; const handedOff = campaign.markCampaignStoryHandoff( reference.sequenceId, reference.revision ); assert.equal( handedOff.storyCheckpoint.phase, 'handoff' ); assertCompleteNoop( () => campaign.completeLegacyFirstVictoryStoryHandoff( { ...reference, revision: reference.revision + 1 } ), 'A stale legacy first-victory handoff reference' ); const completed = campaign.completeLegacyFirstVictoryStoryHandoff( reference ); assert.equal( completed.step, 'first-camp', 'Completing the supported legacy victory story must advance to the current first-camp flow.' ); assert.equal( completed.latestBattleId, 'first-battle-zhuo-commandery' ); assert.equal( completed.storyCheckpoint, undefined ); assert.equal( completed.storyCheckpointRevision, reference.revision + 1 ); assert.equal( completed.gold, 321, 'Legacy step migration must not settle rewards again.' ); assertPersistedState( completed, 'legacy first-victory handoff transition' ); } function verifyExactClearRejectsStaleRevision() { resetTo(initialState('prologue')); const reading = campaign.setCampaignStoryCheckpoint( storyWrite('clear-story', 'clear-story', 0) ); const revision = reading.storyCheckpoint.revision; const cleared = campaign.clearCampaignStoryCheckpoint( 'clear-story', revision ); assert.equal(cleared.storyCheckpoint, undefined); assert.equal( cleared.storyCheckpointRevision, revision + 1, 'An exact clear must invalidate the cleared checkpoint revision.' ); assertPersistedState(cleared, 'exact story clear'); assertCompleteNoop( () => campaign.setCampaignStoryCheckpoint( storyWrite( 'clear-story', 'stale-reading', 1, revision ) ), 'A stale write after an exact story clear' ); } function verifyInvalidDirectWriteIsCompleteNoop() { resetTo(initialState('prologue')); assertCompleteNoop( () => campaign.setCampaignStoryCheckpoint({ version: 1, sequenceId: 'invalid story id!', beatId: 'invalid-beat', pageIndex: 0 }), 'An invalid direct story write' ); } function verifyCanonicalStorageFailureIsAtomic() { resetTo(initialState('prologue-town')); const staleBattleSaveKey = 'heros-web:battle:first-battle-zhuo-commandery:slot-2'; storage.set( staleBattleSaveKey, '{"fixture":"must survive failed commit"}' ); assertCanonicalWriteFailure( () => campaign.beginCampaignStory( 'first-battle', storyWrite( 'failed-begin', 'failed-begin', 0 ), { startBattle: true } ), 'beginCampaignStory' ); assert.equal( storage.get(staleBattleSaveKey), '{"fixture":"must survive failed commit"}', 'A failed story commit must not retire the prior battle resume.' ); assertCanonicalWriteFailure( () => campaign.completeCampaignTutorial( campaign.prologueVillageCampaignTutorialIds .complete, { storyCheckpoint: storyWrite( 'failed-tutorial', 'failed-tutorial', 0 ) } ), 'completeCampaignTutorial' ); const priorCampaign = initialState('first-battle', 1); priorCampaign.battleResumeGeneration = 9; resetTo(priorCampaign); const priorBattleSaveKey = 'heros-web:battle:first-battle-zhuo-commandery:slot-1'; storage.set( priorBattleSaveKey, '{"fixture":"must survive failed new campaign"}' ); assertCanonicalWriteFailure( () => campaign.startNewCampaign({ storyCheckpoint: storyWrite( 'failed-new-campaign', 'failed-new-campaign', 0 ) }), 'startNewCampaign' ); assert.equal( storage.get(priorBattleSaveKey), '{"fixture":"must survive failed new campaign"}', 'A failed new-campaign commit must preserve the overwritten slot battle resume.' ); resetTo( initialState('first-victory-story') ); const legacyReading = campaign.setCampaignStoryCheckpoint( storyWrite( 'failed-legacy-handoff', 'failed-legacy-handoff', 0 ) ); const legacyReference = { sequenceId: legacyReading.storyCheckpoint .sequenceId, revision: legacyReading.storyCheckpoint .revision }; campaign.markCampaignStoryHandoff( legacyReference.sequenceId, legacyReference.revision ); assertCanonicalWriteFailure( () => campaign.completeLegacyFirstVictoryStoryHandoff( legacyReference ), 'completeLegacyFirstVictoryStoryHandoff' ); } function verifyDestinationOwnedHandoffContract() { const titleSource = sourceFile( 'src/game/scenes/TitleScene.ts' ); const titleContinue = sourceSection( titleSource, 'private async continueGameAsync(', 'private async continueStoryCheckpoint(' ); assertOrder( titleContinue, 'continueStoryCheckpoint(campaign)', 'readCampaignBattleResume(campaign', 'Title Continue must prefer a durable story checkpoint over a superseded battle snapshot.' ); assert( titleSource.includes( 'battleResume = campaign.storyCheckpoint' ), 'Title save summaries must describe the story checkpoint instead of a superseded battle snapshot.' ); assert( /create\(data\?: TitleSceneData\)[\s\S]*?this\.consumeTitleStoryHandoff\([\s\S]*?private consumeTitleStoryHandoff\([\s\S]*?completeLegacyFirstVictoryStoryHandoff\(/.test( titleSource ) && /candidate\.destinationScene === 'TitleScene'[\s\S]*?this\.consumeTitleStoryHandoff\(/.test( titleSource ), 'Both normal and restored Story-to-Title handoffs must atomically advance the supported legacy first-victory step.' ); const villageCreate = sourceSection( sourceFile( 'src/game/scenes/PrologueVillageScene.ts' ), 'create(data?: PrologueVillageSceneData)', 'getDebugState()' ); assertOrder( villageCreate, 'this.prepareCampaignProgress()', 'consumeCampaignStoryHandoff(data?.storyHandoff)', 'The village must establish a recoverable campaign step before consuming its story handoff.' ); const militiaCreate = sourceSection( sourceFile( 'src/game/scenes/PrologueMilitiaCampScene.ts' ), 'create(data?: PrologueMilitiaCampSceneData)', 'getDebugState()' ); assertOrder( militiaCreate, 'this.prepareCampaignProgress()', 'consumeCampaignStoryHandoff(data?.storyHandoff)', 'The militia camp must establish a recoverable campaign step before consuming its story handoff.' ); const campSource = sourceFile( 'src/game/scenes/CampScene.ts' ); const campInit = sourceSection( campSource, 'init(data?: CampSceneData)', 'create()' ); assert( campInit.includes( 'completeCampaignStoryHandoffAftermath(' ) && campInit.includes( 'completeCampaignAftermath(' ) && campInit.includes( 'consumeCampaignStoryHandoff(' ), 'Camp must atomically consume exact aftermath handoffs while retaining legacy independent completion and handoff paths.' ); const campInitOffset = campSource.indexOf( 'init(data?: CampSceneData)' ); assert( campSource.indexOf( 'consumeCampaignStoryHandoff(', campInitOffset ) < campSource.indexOf( 'this.campaign = getCampaignState()' ), 'Camp must consume the handoff before caching campaign state so a stale snapshot cannot resurrect it.' ); const battleSource = sourceFile( 'src/game/scenes/BattleScene.ts' ); const battleInit = sourceSection( battleSource, 'init(data?: BattleSceneData)', 'create()' ); assert( battleInit.includes( 'this.storyHandoff = data?.storyHandoff' ) && !battleInit.includes( 'consumeCampaignStoryHandoff(' ), 'Battle preload must retain, not consume, the durable handoff.' ); const battleCreate = sourceSection( battleSource, 'create()', 'getDebugState()' ); assertOrder( battleCreate, "markCampaignStep('first-battle',", 'consumeCampaignStoryHandoff(this.storyHandoff)', 'Battle must establish its recoverable campaign step before consuming the handoff.' ); } function normalizedCheckpoint({ revision, sequenceId, beatId, pageIndex }) { const checkpoint = story.normalizeCampaignStoryCheckpoint( { version: 1, revision, sequenceId, phase: 'reading', beatId, pageIndex, campaignStep: 'prologue', continuationIntent: 'default', savedAt: '2026-07-28T10:30:00.000Z' }, 'prologue', revision ); assert( checkpoint, `Expected a valid checkpoint for ${sequenceId}/${beatId}.` ); return checkpoint; } function initialState(step, slot = 2) { const state = campaign.createInitialCampaignState(); state.updatedAt = '2026-07-28T10:00:00.000Z'; state.step = step; state.activeSaveSlot = slot; return state; } function villageCheckpoint() { return { version: 1, scene: 'prologue-village', contextId: 'prologue-village', player: { x: 420.25, y: 512.5, direction: 'south' } }; } function storyWrite( sequenceId, beatId, pageIndex, revision ) { return { version: 1, ...(revision === undefined ? {} : { revision }), sequenceId, beatId, pageIndex }; } function storySnapshot(state) { const checkpoint = state.storyCheckpoint; assert(checkpoint, 'Expected a story checkpoint.'); return { sequenceId: checkpoint.sequenceId, beatId: checkpoint.beatId, pageIndex: checkpoint.pageIndex, campaignStep: checkpoint.campaignStep, phase: checkpoint.phase }; } function resetTo(state) { failCanonicalSlotWrites = false; storage.clear(); return campaign.setCampaignState(state); } function loadRawState(state) { failCanonicalSlotWrites = false; storage.clear(); const serialized = JSON.stringify(state); storage.set(campaign.campaignStorageKey, serialized); storage.set( slotKey(state.activeSaveSlot ?? 1), serialized ); return campaign.loadCampaignState(); } function assertPersistedState(expected, label) { assert.deepEqual( campaign.getCampaignState(), expected, `${label}: in-memory state must match the returned state.` ); assert.deepEqual( JSON.parse(storage.get(campaign.campaignStorageKey)), expected, `${label}: base-save state must match the returned state.` ); assert.deepEqual( JSON.parse( storage.get(slotKey(expected.activeSaveSlot)) ), expected, `${label}: canonical slot-save state must match the returned state.` ); } function assertCompleteNoop(action, label) { const before = persistentSnapshot(); const returned = action(); assert.deepEqual( returned, before.state, `${label} must return the unchanged state.` ); assert.deepEqual( campaign.getCampaignState(), before.state, `${label} must leave memory and updatedAt unchanged.` ); assert.equal( storage.get(campaign.campaignStorageKey), before.base, `${label} must leave base-save bytes unchanged.` ); assert.equal( storage.get(slotKey(before.state.activeSaveSlot)), before.slot, `${label} must leave slot-save bytes unchanged.` ); } function assertCanonicalWriteFailure(action, label) { const before = persistentSnapshot(); failCanonicalSlotWrites = true; try { assert.throws( action, /Simulated canonical campaign save-slot failure/, `${label} must surface a canonical storage failure.` ); } finally { failCanonicalSlotWrites = false; } assert.deepEqual( campaign.getCampaignState(), before.state, `${label} storage failure must leave memory unchanged.` ); assert.equal( storage.get(campaign.campaignStorageKey), before.base, `${label} storage failure must leave base-save bytes unchanged.` ); assert.equal( storage.get(slotKey(before.state.activeSaveSlot)), before.slot, `${label} storage failure must leave slot-save bytes unchanged.` ); } function persistentSnapshot() { const state = campaign.getCampaignState(); const base = storage.get(campaign.campaignStorageKey); const slot = storage.get(slotKey(state.activeSaveSlot)); assert(base, 'Expected the base campaign save fixture.'); assert(slot, 'Expected the slot campaign save fixture.'); return { state, base, slot }; } function slotKey(slot) { return `${campaign.campaignStorageKey}:slot-${slot}`; } function sourceFile(path) { return readFileSync( new URL(`../${path}`, import.meta.url), 'utf8' ); } function sourceSection(source, startMarker, endMarker) { const start = source.indexOf(startMarker); assert( start >= 0, `Missing source marker "${startMarker}".` ); const end = source.indexOf( endMarker, start + startMarker.length ); assert( end > start, `Missing source marker "${endMarker}" after "${startMarker}".` ); return source.slice(start, end); } function assertOrder(source, first, second, message) { const firstIndex = source.indexOf(first); const secondIndex = source.indexOf(second); assert( firstIndex >= 0 && secondIndex > firstIndex, message ); } } finally { await server.close(); }