From eb02e33a51ceb129eaa9e8a7ba72ca65854f9053 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 08 九月 2026 19:45:39 +0800
Subject: [PATCH] 集成最终回归脚本与现行业务契约对齐

---
 tests/e2e/collector-sampling.spec.ts |  202 +++++++++++++++++++++-----------------------------
 1 files changed, 84 insertions(+), 118 deletions(-)

diff --git a/tests/e2e/collector-sampling.spec.ts b/tests/e2e/collector-sampling.spec.ts
index 7e10158..c042578 100644
--- a/tests/e2e/collector-sampling.spec.ts
+++ b/tests/e2e/collector-sampling.spec.ts
@@ -2,20 +2,17 @@
 import { Ticket04Stack } from './support/ticket04-stack'
 
 const stack = new Ticket04Stack()
+const readonlyCommands = ['discover', 'observe', 'cabinet-realtime', 'collector-realtime', 'getdevinfo', 'inquire']
+interface Measurement { channelCode: string; metricCode: string; value: number; unit: string; sequenceId: number; deviceTimeRaw: string }
+interface CollectorSnapshot { online: boolean; measurements: Measurement[] }
+const configuredMetrics = [
+  { metricCode: 'temperature', value: 28.6, unit: '℃' },
+  { metricCode: 'positive_potential', value: 3.812, unit: 'V' },
+  { metricCode: 'negative_potential', value: -0.116, unit: 'V' },
+  { metricCode: 'dynamic_pressure', value: 101.3, unit: 'kPa' },
+]
 
-interface StoredCollectorState {
-  deviceCode: string
-  nextSequence: number
-  lastSampledAt: string
-  historyRecordCount?: number
-  latestMeasurements?: Record<string, unknown>[]
-  history?: unknown[]
-}
-
-interface StoredSimulatorState {
-  collectors: StoredCollectorState[]
-}
-
+test.use({ actionTimeout: 25_000 })
 test.beforeAll(async () => stack.start())
 test.afterAll(async () => stack.stop())
 
@@ -26,91 +23,86 @@
   await page.getByRole('button', { name: '登录', exact: true }).click()
   await expect(page).toHaveURL(/\/dashboard$/)
 }
+async function realtime(page: Page): Promise<CollectorSnapshot> {
+  const response = await page.request.get(stack.backendUrl + '/dev/bts-simulator/collectors/1/realtime')
+  expect(response.ok()).toBe(true)
+  return response.json()
+}
+async function collectorState(page: Page) {
+  const response = await page.request.get(stack.backendUrl + '/dev/bts-simulator/state')
+  expect(response.ok()).toBe(true)
+  const state = await response.json()
+  expect(state.recentRequests.every((item: { command: string }) => readonlyCommands.includes(item.command))).toBe(true)
+  return state.collectors.find((item: { deviceCode: string }) => item.deviceCode === 'dev-sim-collector-1')
+}
 
-test('采集器登记后可观察独立实时与分页历史采样', async ({ page }) => {
-  await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
+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'
+  const reset = 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 reset
+  const 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([
-    'temperature=28.6 ℃',
-    'positive_potential=3.812 V',
-    'negative_potential=-0.116 V',
-    'dynamic_pressure=101.3 kPa',
-  ].join('\n'))
-  const measurementsSaved = page.waitForResponse(response => response.request().method() === 'PUT'
+  const channel = collector.locator('tbody tr').filter({ has: page.getByText('S-01', { exact: true }) })
+  await channel.getByRole('button', { name: '编辑测量值', exact: true }).click()
+  const editor = page.getByRole('dialog', { name: '采集器 1 · S-01 测量值', exact: true })
+  for (let index = 0; index < 3; index++) await editor.getByLabel('数值', { exact: true }).nth(index).fill(String(configuredMetrics[index]!.value))
+  await editor.getByRole('button', { name: '添加测量项', exact: true }).click()
+  const custom = editor.locator('tbody tr').last()
+  await custom.getByLabel('自定义名称', { exact: true }).fill('dynamic_pressure')
+  await custom.getByLabel('数值', { exact: true }).fill('101.3')
+  await custom.getByLabel('单位', { exact: true }).fill('kPa')
+  const saved = 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
+  await editor.getByRole('button', { name: '保存通道配置', exact: true }).click()
+  await saved
+  await expect(editor).not.toBeVisible()
+  await expect(channel).toContainText('dynamic_pressure 101.3 kPa')
+  const configured = await collectorState(page)
+  expect(configured.channels[0].metrics).toEqual(configuredMetrics)
+  expect(configured.latestMeasurements).toEqual([])
 
-  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/)
-  await expect(collector.getByText(/原始历史 [1-9]\d* 条/)).toBeVisible()
-  await collector.getByRole('button', { name: '查看实时' }).click()
-  await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
+  const observed = page.waitForResponse(response => response.url().endsWith('/collectors/1/realtime') && response.ok())
+  await collector.getByRole('button', { name: '查看实时', exact: true }).click()
+  const first: CollectorSnapshot = await (await observed).json()
+  expect(first.online).toBe(true)
+  expect(first.measurements).toHaveLength(13)
+  expect(first.measurements.filter(item => item.channelCode === 'S-01').map(({ metricCode, value, unit }) => ({ metricCode, value, unit }))).toEqual(configuredMetrics)
   await expect(collector.locator('.collector-output')).toContainText('S-01 dynamic_pressure=101.3 kPa')
   await expect(collector.locator('.collector-output')).toContainText('设备 2026-')
   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*:/)
-  const historyBeforePowerOff = Number(stack.redisCommand(
-    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
-  ))
-  expect(historyBeforePowerOff).toBeGreaterThan(0)
+  const second = await realtime(page)
+  expect(second.measurements[0]!.sequenceId).toBeGreaterThan(first.measurements[0]!.sequenceId)
+  expect(second.measurements.map(item => item.value)).toEqual(first.measurements.map(item => item.value))
+  const beforeIdle = await collectorState(page)
+  // ADR-0048:状态读取和请求空档不生成观测,也不积累历史。
+  await page.waitForTimeout(1_100)
+  expect((await collectorState(page)).latestMeasurements).toEqual(beforeIdle.latestMeasurements)
+  expect(stack.redisCommand('EXISTS', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1')).toBe('0')
+  expect(stack.redisCommand('GET', 'dev:bts-simulator:state:v1')).not.toMatch(/\\?"history\\?"\s*:/)
 
   page.once('dialog', dialog => dialog.accept())
-  const poweredOff = page.waitForResponse(response => response.request().method() === 'POST'
-    && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
-  await page.getByRole('button', { name: '一键模拟关机' }).click()
-  await poweredOff
-  await expect(page.locator('#device-power')).toHaveText('已关机')
-  const historyAfterPowerOff = Number(stack.redisCommand(
-    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
-  ))
-  await page.waitForTimeout(700)
-  expect(Number(stack.redisCommand(
-    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
-  ))).toBe(historyAfterPowerOff)
-
+  const off = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
+  await page.getByRole('button', { name: '全部设备离线', exact: true }).click()
+  await off
+  await expect(page.locator('#device-power')).toHaveText('全部离线')
+  expect(await realtime(page)).toMatchObject({ online: false, measurements: [] })
   await stack.restartBackend()
-  await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
-  await expect(page.locator('#device-power')).toHaveText('已关机')
-  await page.waitForTimeout(700)
-  expect(Number(stack.redisCommand(
-    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
-  ))).toBe(historyAfterPowerOff)
-
-  const poweredOn = page.waitForResponse(response => response.request().method() === 'POST'
-    && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
-  await page.getByRole('button', { name: '一键模拟开机' }).click()
-  await poweredOn
-  await expect(page.locator('#device-power')).toHaveText('已开机')
-  await expect(page.locator('#error')).not.toHaveClass(/visible/)
-  await expect.poll(() => Number(stack.redisCommand(
-    'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
-  ))).toBeGreaterThan(historyAfterPowerOff)
-  await expect.poll(() => {
-    const stored = JSON.parse(stack.redisCommand('GET', 'dev:device-discovery:candidates:v1'))
-    const candidates = typeof stored === 'string' ? JSON.parse(stored) : stored
-    return candidates.collectors.find((item: { deviceCode: string }) =>
-      item.deviceCode === 'dev-sim-collector-1')?.online
-  }, { timeout: 10_000 }).toBe(true)
+  await page.goto(stack.backendUrl + '/dev/bts-simulator.html')
+  await expect(page.locator('#device-power')).toHaveText('全部离线')
+  expect(await realtime(page)).toMatchObject({ online: false, measurements: [] })
+  expect((await collectorState(page)).channels[0].metrics).toEqual(configuredMetrics)
+  const on = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
+  await page.getByRole('button', { name: '全部设备上线', exact: true }).click()
+  await on
+  await expect(page.locator('#device-power')).toHaveText('全部在线')
+  const resumed = await realtime(page)
+  expect(resumed.measurements).toHaveLength(13)
+  expect(resumed.measurements[0]!.sequenceId).toBeGreaterThan(second.measurements[0]!.sequenceId)
+  expect(resumed.measurements.map(item => item.value)).toEqual(first.measurements.map(item => item.value))
 
   await login(page)
   await page.goto('/device-collector/new')
@@ -118,42 +110,16 @@
   const candidate = page.locator('.device-table tbody tr').filter({ hasText: 'dev-sim-collector-1' })
   await expect(candidate).toBeVisible()
   await candidate.getByRole('button', { name: '选择' }).click()
-  await page.locator('label').filter({ hasText: '采集器名称' }).locator('input').fill('Ticket06 验收采集器')
+  await page.locator('label').filter({ hasText: '采集器名称' }).locator('input').fill('实时观测验收采集器')
   await page.getByRole('button', { name: '保存', exact: true }).click()
   await expect(page.locator('tbody tr').filter({ hasText: 'dev-sim-collector-1' })).toBeVisible()
-
   await stack.restartBackend()
-  await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
-  collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
-  await expect(collector.locator('label').filter({ hasText: '采样间隔(毫秒)' }).locator('input')).toHaveValue('200')
-  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')
-
+  await page.goto(stack.backendUrl + '/dev/bts-simulator.html')
+  expect((await collectorState(page)).channels[0].metrics).toEqual(configuredMetrics)
+  expect((await realtime(page)).measurements.map(item => item.value)).toEqual(first.measurements.map(item => item.value))
+  await collector.getByRole('button', { name: '查看实时', exact: true }).click()
+  await expect(collector.locator('.collector-output')).toContainText('S-01 dynamic_pressure=101.3 kPa')
   const commands = await page.locator('#requests .request-command').allTextContents()
-  expect(commands.every(command => command === 'getdevinfo' || command === 'inquire')).toBe(true)
+  expect(commands.length).toBeGreaterThan(0)
+  expect(commands.every(command => readonlyCommands.includes(command))).toBe(true)
 })

--
Gitblit v1.7.1