From 02032429319ab2942793b93cda4e65606c42d7b9 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 29 八月 2026 14:58:29 +0800
Subject: [PATCH] 补充采集器历史存储恢复验收

---
 tests/e2e/collector-sampling.spec.ts |   52 ++++++++++++++++++++++++++
 tests/e2e/support/ticket04-stack.ts  |   34 ++++++++++++++++
 2 files changed, 85 insertions(+), 1 deletions(-)

diff --git a/tests/e2e/collector-sampling.spec.ts b/tests/e2e/collector-sampling.spec.ts
index e34a05d..df4a8dd 100644
--- a/tests/e2e/collector-sampling.spec.ts
+++ b/tests/e2e/collector-sampling.spec.ts
@@ -3,6 +3,19 @@
 
 const stack = new Ticket04Stack()
 
+interface StoredCollectorState {
+  deviceCode: string
+  nextSequence: number
+  lastSampledAt: string
+  historyRecordCount?: number
+  latestMeasurements?: Record<string, unknown>[]
+  history?: unknown[]
+}
+
+interface StoredSimulatorState {
+  collectors: StoredCollectorState[]
+}
+
 test.beforeAll(async () => stack.start())
 test.afterAll(async () => stack.stop())
 
@@ -17,13 +30,19 @@
 test('采集器登记后可观察独立实时与分页历史采样', async ({ page }) => {
   await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
   page.once('dialog', dialog => dialog.accept())
+  const simulatorReset = page.waitForResponse(response => response.request().method() === 'POST'
+    && response.url().endsWith('/dev/bts-simulator/reset') && response.ok())
   await page.getByRole('button', { name: '重置模拟器' }).click()
+  await simulatorReset
 
   let collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
   await expect(collector.getByText('S-01', { exact: true })).toBeVisible()
   await expect(collector.getByText('S-04', { exact: true })).toBeVisible()
   await collector.locator('label').filter({ hasText: '采样间隔(毫秒)' }).locator('input').fill('200')
+  const samplingSaved = page.waitForResponse(response => response.request().method() === 'PUT'
+    && response.url().endsWith('/dev/bts-simulator/collectors/1/sampling') && response.ok())
   await collector.getByRole('button', { name: '保存配置' }).click()
+  await samplingSaved
 
   collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
   await collector.getByLabel('S-01 测量项').fill([
@@ -32,7 +51,10 @@
     'negative_potential=-0.116 V',
     'dynamic_pressure=101.3 kPa',
   ].join('\n'))
+  const measurementsSaved = page.waitForResponse(response => response.request().method() === 'PUT'
+    && response.url().endsWith('/dev/bts-simulator/collectors/1/channels/S-01') && response.ok())
   await collector.getByRole('button', { name: '保存测量值' }).first().click()
+  await measurementsSaved
 
   collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
   await expect(collector.getByLabel('S-01 测量项')).toHaveValue(/dynamic_pressure=101\.3 kPa/)
@@ -44,6 +66,12 @@
   await expect(collector.locator('.collector-output')).toContainText('/ 接收 2026-')
   await collector.getByRole('button', { name: '下载历史' }).click()
   await expect(collector.locator('.collector-output')).toContainText(/历史 \d+ 条 \/ 本页 \d+ 条/)
+
+  const mainState = stack.redisCommand('GET', 'dev:bts-simulator:state:v1')
+  expect(mainState).not.toMatch(/\\?"history\\?"\s*:/)
+  expect(Number(stack.redisCommand(
+    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
+  ))).toBeGreaterThan(0)
 
   await login(page)
   await page.goto('/device-collector/new')
@@ -62,6 +90,30 @@
   await expect(collector.getByLabel('S-01 测量项')).toHaveValue(/dynamic_pressure=101\.3 kPa/)
   await collector.getByRole('button', { name: '下载历史' }).click()
   await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
+  expect(Number(stack.redisCommand(
+    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
+  ))).toBeGreaterThan(0)
+
+  const legacyState = stack.simulatorState<StoredSimulatorState>()
+  const legacyCollector = legacyState.collectors[0]
+  const legacyRecord = legacyCollector.latestMeasurements?.[0]
+  expect(legacyRecord).toBeDefined()
+  delete legacyCollector.historyRecordCount
+  delete legacyCollector.latestMeasurements
+  legacyCollector.history = [legacyRecord]
+  await stack.replaceSimulatorStateAndRestart(legacyState)
+
+  const migratedState = stack.redisCommand('GET', 'dev:bts-simulator:state:v1')
+  expect(migratedState).not.toMatch(/\\?"history\\?"\s*:/)
+  expect(Number(stack.redisCommand(
+    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
+  ))).toBeGreaterThan(1)
+
+  await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
+  collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
+  await collector.getByRole('button', { name: '下载历史' }).click()
+  await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
+  expect(stack.backendLog()).not.toContain('JSONLargeObjectException')
 
   const commands = await page.locator('#requests .request-command').allTextContents()
   expect(commands.every(command => command === 'getdevinfo' || command === 'inquire')).toBe(true)
diff --git a/tests/e2e/support/ticket04-stack.ts b/tests/e2e/support/ticket04-stack.ts
index edd0677..0da4f05 100644
--- a/tests/e2e/support/ticket04-stack.ts
+++ b/tests/e2e/support/ticket04-stack.ts
@@ -1,4 +1,4 @@
-import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
+import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
 import { createServer } from 'node:net'
 import { resolve } from 'node:path'
 import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
@@ -91,6 +91,7 @@
     await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable))
     this.docker = resolveDocker()
     mkdirSync(artifactDir, { recursive: true })
+    writeFileSync(resolve(artifactDir, 'backend.log'), '')
     try {
       this.dockerRun([
         'run', '--detach', '--rm', '--name', this.mysqlContainer,
@@ -119,6 +120,37 @@
     await this.startBackend()
   }
 
+  redisCommand(...args: string[]) {
+    const result = this.dockerRun([
+      'exec', this.redisContainer, 'redis-cli', '-a', redisPassword, '--no-auth-warning', '--raw', ...args,
+    ])
+    return result.stdout.trim()
+  }
+
+  simulatorState<T>() {
+    const stored = JSON.parse(this.redisCommand('GET', 'dev:bts-simulator:state:v1'))
+    return (typeof stored === 'string' ? JSON.parse(stored) : stored) as T
+  }
+
+  backendLog() {
+    return readFileSync(resolve(artifactDir, 'backend.log'), 'utf8')
+  }
+
+  async replaceSimulatorStateAndRestart(state: unknown) {
+    await this.stopBackend()
+    const registryKey = 'dev:bts-simulator:collector-history-keys:v1'
+    const registeredHistory = this.redisCommand('SMEMBERS', registryKey)
+      .split(/\r?\n/).filter(Boolean)
+      .map(value => {
+        const decoded = JSON.parse(value)
+        return typeof decoded === 'string' ? decoded : value
+      })
+    if (registeredHistory.length > 0) this.redisCommand('DEL', ...registeredHistory)
+    this.redisCommand('DEL', registryKey)
+    this.redisCommand('SET', 'dev:bts-simulator:state:v1', JSON.stringify(JSON.stringify(state)))
+    await this.startBackend()
+  }
+
   async stop() {
     await this.stopBackend()
     if (!this.docker) return

--
Gitblit v1.7.1