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