import assert from 'node:assert/strict' import { fetchEquipmentDetailAggregate } from '../../../src/modules/cockpit/services/detailApi.js' import { metricRows, sectionHasContent, sectionPlaceholder } from '../../../src/modules/cockpit/domain/equipmentDetailView.js' const baseUrl = process.argv[2] assert.ok(baseUrl, 'Pass the existing cockpit read-only API base URL') const deviceCode = process.argv[3] || '21119740' const request = (url, options) => fetch(url, { ...options, signal: AbortSignal.timeout(15000) }) // Independent existing source API, rather than copying the cockpit adapter's query. const factors = [ { node: 1, code: 'ENV_SUSPENDED_SOLIDS', name: '悬浮物', field: 'floatValue', unit: 'mg/L' }, { node: 2, code: 'ENV_COD', name: 'COD', field: 'hum', unit: 'mg/L' }, { node: 3, code: 'ENV_AMMONIA_NITROGEN', name: '氨氮', field: 'hum', unit: 'mg/L' }, { node: 4, code: 'ENV_CONDUCTIVITY', name: '电导率', field: 'hum', unit: 'μS/cm' }, { node: 5, code: 'ENV_PH', name: 'pH', field: 'hum', unit: '' }, ] const sources = deviceCode === '21119740' ? await Promise.all(factors.map(async (factor) => { const url = new URL('/api/env/visualization/latest', baseUrl) url.searchParams.set('deviceId', deviceCode) url.searchParams.set('nodeId', String(factor.node)) const response = await request(url) assert.ok(response.ok, `Source node ${factor.node}: HTTP ${response.status}`) const body = await response.json() assert.equal(body.code, 200, `Source node ${factor.node} must succeed independently`) const record = body.data || null if (record) { assert.equal(record.deviceId, Number(deviceCode), 'Source device identity mismatch') assert.equal(record.nodeId, factor.node, 'Source node identity mismatch') } return { factor, record } })) : [] const result = await fetchEquipmentDetailAggregate(deviceCode, { config: { dataSource: 'real', api: { baseUrl } }, fetchImpl: request, }) const metrics = result.data?.sections?.metrics const rows = metricRows(metrics?.data) console.log(JSON.stringify({ deviceCode, aggregate: result.state, metrics: metrics?.state, placeholder: sectionPlaceholder(metrics), rows, sourceNodes: sources.map(({ factor, record }) => ({ node: factor.node, recordId: record?.id, value: record?.[factor.field], temperature: record?.tem, createdAt: record?.createTime })), }, null, 2)) assert.ok(sectionHasContent(metrics), `${deviceCode}: latest metrics must render content, not ${sectionPlaceholder(metrics)}`) assert.ok(rows.some((row) => row.value !== '—'), `${deviceCode}: expected at least one real reading`) assert.ok(rows.every((row) => row.name && row.sampledAt), 'Every metric must retain its name and source time') function assertReading(code, name, value, unit, sourceTime) { const row = rows.find((item) => item.code === code) assert.ok(row, `Missing real source metric ${code}`) assert.equal(row.name, name) assert.equal(row.unit, unit, `${code}: source unit mismatch`) const missing = value === null || value === undefined || String(value).trim() === '' || !Number.isFinite(Number(value)) if (missing) assert.equal(row.value, '—', `${code}: missing source value must not become zero`) else assert.equal(Number(row.value), Number(value), `${code}: source and cockpit readings differ`) assert.equal(row.sampledAtLabel, '数据入库时间', `${code}: create_time must not be called sampling time`) const sourceAt = /(?:Z|[+-]\d{2}:\d{2})$/.test(sourceTime) ? sourceTime : `${sourceTime}+08:00` const sourceMillis = Date.parse(sourceAt) const metricMillis = Date.parse(row.sampledAt) assert.ok(Number.isFinite(sourceMillis), `${code}: invalid source timestamp`) assert.ok(Number.isFinite(metricMillis), `${code}: invalid cockpit timestamp`) assert.equal(metricMillis, sourceMillis, `${code}: latest node source time differs`) } for (const { factor, record } of sources) { if (!record) { assert.ok(!rows.some((row) => row.code === factor.code), `Node ${factor.node}: no source must not produce a reading`) continue } assertReading(factor.code, factor.name, record[factor.field], factor.unit, record.createTime) assertReading(`ENV_NODE_${factor.node}_TEMPERATURE`, `${factor.name}节点温度`, record.tem, '℃', record.createTime) } console.log('PASS real detail API -> latest metric rows, independently checked against each available source node')