import assert from 'node:assert/strict' import { createPinia, setActivePinia } from 'pinia' import { useDetailStore } from '../../../src/modules/cockpit/stores/detailStore.js' import { metricRows, metricsSourceText, sectionHasContent, sectionPlaceholder } from '../../../src/modules/cockpit/domain/equipmentDetailView.js' import { TIME_LABELS } from '../../../src/modules/cockpit/domain/fields.js' // Literal API-contract fixture, not a reimplementation of the backend selection. const readings = [ { metricCode: 'ENV_COD', metricName: 'COD', value: 32.5, unit: 'mg/L', sampledAt: '2026-10-29T10:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'STALE' }, { metricCode: 'ENV_PH', metricName: 'pH', value: 0, unit: '', sampledAt: '2026-10-28T09:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'CURRENT' }, { metricCode: 'ENV_TEMPERATURE', metricName: '温度', value: null, unit: '℃', sampledAt: '2026-10-29T10:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'STALE' }, ] function aggregate(code, metrics, state = 'STALE') { return { deviceCode: code, aggregateState: 'READY', archive: { state: 'SUCCESS', data: { deviceCode: code, deviceName: '仁科环境监测器', deviceType: '15', deviceTypeName: '仁科环境监测器' } }, metrics: { state, data: { deviceCode: code, state, stateReason: state === 'STALE' ? 'SAMPLE_OUTDATED' : 'READY', metrics } }, warnings: { state: 'SUCCESS', data: [] }, alarms: { state: 'SUCCESS', data: [] }, workOrders: { state: 'EMPTY', data: null }, timelines: { state: 'EMPTY', data: null }, alarmWarningRelations: { state: 'EMPTY', data: null }, latestImage: { state: 'EMPTY', data: null }, } } const paths = [] setActivePinia(createPinia()) const store = useDetailStore() store.configure({ config: { dataSource: 'real', api: { baseUrl: 'http://fixture.invalid/api/cockpit/v1' } }, fetchImpl: async (url) => { paths.push(url) assert.ok(url.endsWith('/equipments/21119740/detail-aggregate')) return Response.json({ code: 200, data: aggregate('21119740', readings) }) }, }) await store.openDevice('21119740') const metrics = store.sections.equipmentAggregate.data.sections.metrics assert.ok(sectionHasContent(metrics), sectionPlaceholder(metrics)) assert.equal(metrics.state, 'stale') assert.equal(metrics.retained, true) assert.equal(paths.length, 1, 'Populated typed aggregate must not fan out to a guessed trend endpoint') const rows = metricRows(metrics.data) assert.deepEqual(rows.map(({ name, value, unit, sampledAt }) => ({ name, value, unit, sampledAt })), [ { name: 'COD', value: '32.5', unit: 'mg/L', sampledAt: '2026-10-29T10:00:00+08:00' }, { name: 'pH', value: '0', unit: '', sampledAt: '2026-10-28T09:00:00+08:00' }, { name: '温度', value: '—', unit: '℃', sampledAt: '2026-10-29T10:00:00+08:00' }, ]) assert.equal(rows[0].sampledAtLabel, TIME_LABELS.ingested, 'A source create_time must not be labeled as physical sampling time') assert.equal(metricsSourceText(metrics.data), '指标记录已超过配置周期') assert.deepEqual(rows.map((row) => row.freshnessLabel), ['已过期', '', '已过期'], 'Mixed fresh/stale factors need their own stale labels') // A slow previous device response must never overwrite the newly selected device. let releaseA const lateA = new Promise((resolve) => { releaseA = resolve }) store.closeDetail() store.configure({ fetchImpl: async (url) => { if (url.endsWith('/equipments/A/detail-aggregate')) return lateA assert.ok(url.endsWith('/equipments/B/detail-aggregate')) return Response.json({ code: 200, data: aggregate('B', [ { metricCode: 'PRESSURE', metricName: '压力', value: 0.6, unit: 'MPa', sampledAt: '2026-11-02T10:00:00+08:00', timeSource: 'REPORTED_AT' }, ], 'SUCCESS') }) } }) const openingA = store.openDevice('A') await store.openDevice('B') releaseA(Response.json({ code: 200, data: aggregate('A', readings) })) await openingA assert.equal(store.selection.deviceCode, 'B') assert.equal(store.sections.equipmentAggregate.data.deviceCode, 'B') const bRows = metricRows(store.sections.equipmentAggregate.data.sections.metrics.data) assert.equal(bRows[0].value, '0.6') assert.equal(bRows[0].sampledAtLabel, TIME_LABELS.sampled) store.configure({ fetchImpl: async () => Response.json({ code: 200, data: aggregate('D-failed', [], 'FAILED') }) }) await store.openDevice('D-failed') const failed = store.sections.equipmentAggregate.data.sections assert.equal(failed.metrics.state, 'failed') assert.equal(failed.archive.state, 'success') assert.equal(failed.warnings.state, 'success') assert.equal(failed.alarms.state, 'success') console.log('PASS real store -> typed metric rows: per-factor time, stale values, zero/null, time source, device race and independent sections')