|
|
@@ -5,14 +5,66 @@ import { metricRows, sectionHasContent, sectionPlaceholder } from '../../../src/
|
|
|
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: (url, options) => fetch(url, { ...options, signal: AbortSignal.timeout(15000) }),
|
|
|
+ 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 }, null, 2))
|
|
|
+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')
|
|
|
-console.log('PASS real detail API -> latest metric rows')
|
|
|
+
|
|
|
+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')
|