@ -1,30 +1,159 @@
/ * *
* CNC 采 集 服 务 Playwright 端 到 端 测 试
* CNC 采 集 服 务 Playwright 端 到 端 测 试 ( 完 整 版 )
*
* 覆 盖 范 围 :
* 套 件 1 : 管 理 API 控 制 测 试 ( 7 个 )
* 套 件 2 : 采 集 数 据 全 链 路 验 证 ( 6 个 )
* 套 件 3 : 产 量 分 段 跟 踪 验 证 ( 3 个 )
* 套 件 4 : 异 常 处 理 与 恢 复 验 证 ( 3 个 )
* 套 件 5 : 心 跳 上 报 验 证 ( 2 个 )
*
* 前 置 条 件 :
* 1 . MariaDB 运 行 中 ( cnc_business + cnc_log 库 已 初 始 化 )
* 2 . CncSimulator 运 行 在 http : //localhost:9001/
* 2 . CncSimulator 运 行 在 http : //localhost:9001/ (网关模式)
* 3 . CncCollector 运 行 中 , 管 理 API 在 http : //localhost:5800/
*
* 安 装 : npm init - y && npm install @playwright / test
* 安 装 : npm in stall @playwright / test mysql2
* 运 行 : npx playwright test e2e - collector . spec . ts -- reporter = list
* /
import { test , expect , request } from '@playwright/test' ;
import mysql from 'mysql2/promise' ;
// ============================================================
// 常量与配置
// ============================================================
const API_BASE = 'http://localhost:5800' ;
const SIM_GATEWAY = 'http://localhost:9001' ;
const API_KEY = 'collector_api_key_2026' ;
const HEADERS = { 'X-API-Key' : API_KEY , 'Content-Type' : 'application/json' } ;
const DB_BUSINESS = {
host : 'localhost' , user : 'root' , password : 'root' ,
database : 'cnc_business' , charset : 'utf8mb4'
} ;
const DB_LOG = {
host : 'localhost' , user : 'root' , password : 'root' ,
database : 'cnc_log' , charset : 'utf8mb4'
} ;
/** 采集地址1对应的模拟端口( 启动后动态获取) */
let simulationPort = 0 ;
/** 原始URL备份( 用于恢复) */
let originalUrl = '' ;
/** 测试开始时间(用于过滤测试产生的数据) */
const testStartTime = new Date ( ) ;
// ============================================================
// 测试套件1: 采集服务管理API控制测试
// 数据库查询辅助函数
// ============================================================
test . describe ( '采集服务管理API控制测试' , ( ) = > {
test ( 'GET /api/collector/status - 获取服务状态' , async ( ) = > {
async function queryBusiness ( sql : string , params? : any [ ] ) {
const conn = await mysql . createConnection ( DB_BUSINESS ) ;
try {
const [ rows ] = await conn . query ( sql , params ) ;
return rows as any [ ] ;
} finally {
await conn . end ( ) ;
}
}
async function queryLog ( sql : string , params? : any [ ] ) {
const conn = await mysql . createConnection ( DB_LOG ) ;
try {
const [ rows ] = await conn . query ( sql , params ) ;
return rows as any [ ] ;
} finally {
await conn . end ( ) ;
}
}
async function executeBusiness ( sql : string , params? : any [ ] ) {
const conn = await mysql . createConnection ( DB_BUSINESS ) ;
try {
await conn . execute ( sql , params ) ;
} finally {
await conn . end ( ) ;
}
}
/** 辅助: 调用采集服务管理API */
async function collectorApi ( method : string , path : string , headers? : Record < string , string > ) {
const ctx = await request . newContext ( ) ;
const resp = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
const opts : any = { headers : headers || HEADERS } ;
if ( method === 'GET' ) return ctx . get ( ` ${ API_BASE } ${ path } ` , opts ) ;
return ctx . post ( ` ${ API_BASE } ${ path } ` , opts ) ;
}
/** 辅助:等待指定毫秒 */
const sleep = ( ms : number ) = > new Promise ( r = > setTimeout ( r , ms ) ) ;
// ============================================================
// 全局串行配置
// ============================================================
test . describe . configure ( { mode : 'serial' } ) ;
// ============================================================
// 全局 Setup / Teardown
// ============================================================
test . beforeAll ( async ( ) = > {
console . log ( '\n===== E2E测试前置准备 =====' ) ;
// 1. 备份并更新采集地址URL( 禁用不可达的地址2)
const addrs = await queryBusiness ( 'SELECT id, url FROM cnc_collect_address WHERE id = 1' ) ;
if ( addrs . length > 0 ) {
originalUrl = addrs [ 0 ] . url ;
}
await executeBusiness ( 'UPDATE cnc_collect_address SET is_enabled = 0 WHERE id = 2' ) ;
console . log ( ' [1/5] 已禁用不可达地址(id=2)' ) ;
// 2. 通过模拟器管理API启动地址1的模拟
const startResp = await fetch ( ` ${ SIM_GATEWAY } /admin/api/start-address ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { dbAddressId : 1 , interval : 5 } )
} ) ;
const startResult = await startResp . json ( ) as any ;
simulationPort = startResult . port ;
console . log ( ` [2/5] 模拟器启动地址1模拟 → 端口 ${ simulationPort } ` ) ;
// 3. 更新数据库中的采集地址URL
await executeBusiness (
'UPDATE cnc_collect_address SET url = ?, is_enabled = 1 WHERE id = 1' ,
[ ` http://localhost: ${ simulationPort } / ` ]
) ;
console . log ( ` [3/5] 更新采集地址URL → http://localhost: ${ simulationPort } / ` ) ;
// 4. 停止采集服务(清空状态),刷新配置,再启动
await collectorApi ( 'POST' , '/api/collector/stop' ) ;
await sleep ( 1000 ) ;
await collectorApi ( 'POST' , '/api/collector/refresh' ) ;
await sleep ( 500 ) ;
await collectorApi ( 'POST' , '/api/collector/start' ) ;
console . log ( ' [4/5] 采集服务已重启并刷新配置' ) ;
// 5. 等待2个采集周期( 间隔5秒 × 2 + 余量)
await sleep ( 14000 ) ;
console . log ( ' [5/5] 等待数据采集完成\n' ) ;
} ) ;
test . afterAll ( async ( ) = > {
// 仅在全部测试结束后恢复数据库配置
// 注意: Playwright的afterAll可能在describe块之间执行
// 所以这里不做任何清理,由最终测试负责
} ) ;
// ============================================================
// 套件1: 采集服务管理API控制测试
// ============================================================
test . describe ( '套件1: 管理API控制测试' , ( ) = > {
test ( 'GET /api/collector/status - 获取服务状态' , async ( ) = > {
const resp = await collectorApi ( 'GET' , '/api/collector/status' ) ;
expect ( resp . status ( ) ) . toBe ( 200 ) ;
const body = await resp . json ( ) ;
@ -56,9 +185,7 @@ test.describe('采集服务管理API控制测试', () => {
} ) ;
test ( 'POST /api/collector/refresh - 刷新配置' , async ( ) = > {
const ctx = await request . newContext ( ) ;
const resp = await ctx . post ( ` ${ API_BASE } /api/collector/refresh ` , { headers : HEADERS } ) ;
const resp = await collectorApi ( 'POST' , '/api/collector/refresh' ) ;
expect ( resp . status ( ) ) . toBe ( 200 ) ;
const body = await resp . json ( ) ;
expect ( body . code ) . toBe ( 0 ) ;
@ -66,120 +193,371 @@ test.describe('采集服务管理API控制测试', () => {
} ) ;
test ( 'POST /api/collector/stop - 停止采集服务' , async ( ) = > {
const ctx = await request . newContext ( ) ;
const resp = await ctx . post ( ` ${ API_BASE } /api/collector/stop ` , { headers : HEADERS } ) ;
const resp = await collectorApi ( 'POST' , '/api/collector/stop' ) ;
expect ( resp . status ( ) ) . toBe ( 200 ) ;
const body = await resp . json ( ) ;
expect ( body . code ) . toBe ( 0 ) ;
// 验证状态变为停止
const statusResp = await c tx. get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
const statusResp = await c ollectorApi( 'GET' , '/api/collector/status' ) ;
const statusBody = await statusResp . json ( ) ;
expect ( statusBody . data . isRunning ) . toBe ( false ) ;
} ) ;
test ( 'POST /api/collector/start - 启动采集服务' , async ( ) = > {
const ctx = await request . newContext ( ) ;
const resp = await ctx . post ( ` ${ API_BASE } /api/collector/start ` , { headers : HEADERS } ) ;
const resp = await collectorApi ( 'POST' , '/api/collector/start' ) ;
expect ( resp . status ( ) ) . toBe ( 200 ) ;
const body = await resp . json ( ) ;
expect ( body . code ) . toBe ( 0 ) ;
// 等待启动
await new Promise ( r = > setTimeout ( r , 2000 ) ) ;
await sleep ( 2000 ) ;
// 验证状态变为运行
const statusResp = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
const statusResp = await collectorApi ( 'GET' , '/api/collector/status' ) ;
const statusBody = await statusResp . json ( ) ;
expect ( statusBody . data . isRunning ) . toBe ( true ) ;
} ) ;
test ( '未知端点返回404' , async ( ) = > {
const ctx = await request . newContext ( ) ;
const resp = await ctx . get ( ` ${ API_BASE } /api/collector/nonexistent ` , { headers : HEADERS } ) ;
const resp = await collectorApi ( 'GET' , '/api/collector/nonexistent' ) ;
expect ( resp . status ( ) ) . toBe ( 404 ) ;
} ) ;
} ) ;
// ============================================================
// 测试套件2: 采集数据流程 验证
// 套件2: 采集数据全链路 验证
// ============================================================
test . describe ( '采集数据流程验证' , ( ) = > {
test ( '采集服务启动后工作线程数 > 0' , async ( ) = > {
const ctx = await request . newContext ( ) ;
test . describe ( '套件2: 采集数据全链路验证' , ( ) = > {
// 确保启动
await ctx . post ( ` ${ API_BASE } /api/collector/start ` , { headers : HEADERS } ) ;
await new Promise ( r = > setTimeout ( r , 3000 ) ) ;
test ( '原始JSON写入日志库 log_collect_raw' , async ( ) = > {
const rows = await queryLog (
'SELECT id, raw_json, is_success, collect_address_id FROM log_collect_raw WHERE created_at >= ? ORDER BY id DESC LIMIT 5' ,
[ testStartTime ]
) ;
const resp = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
const body = await resp . json ( ) ;
expect ( rows . length ) . toBeGreaterThanOrEqual ( 1 ) ;
const latest = rows [ 0 ] ;
// 验证raw_json非空且是有效JSON
expect ( latest . raw_json ) . toBeTruthy ( ) ;
const parsed = JSON . parse ( latest . raw_json ) ;
expect ( Array . isArray ( parsed ) ) . toBe ( true ) ;
// 验证采集成功
expect ( latest . is_success ) . toBe ( 1 ) ;
expect ( body . data . isRunning ) . toBe ( true ) ;
expect ( body . data . workerCount ) . toBeGreaterThanOrEqual ( 0 ) ;
// 验证是地址1的数据
expect ( latest. collect_address_id ) . toBe ( 1 ) ;
} ) ;
test ( '采集服务运行时间持续增长' , async ( ) = > {
const ctx = await request . newContext ( ) ;
test ( '结构化记录写入业务库 cnc_collect_record' , async ( ) = > {
const rows = await queryBusiness (
'SELECT id, machine_id, program_name, part_count, spindle_speed_actual, feed_speed_actual, spindle_load, collect_time FROM cnc_collect_record WHERE created_at >= ? ORDER BY id DESC LIMIT 10' ,
[ testStartTime ]
) ;
expect ( rows . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// 验证每条记录的关键字段
for ( const r of rows ) {
expect ( r . machine_id ) . toBeGreaterThan ( 0 ) ;
expect ( r . program_name ) . toBeTruthy ( ) ;
expect ( r . collect_time ) . toBeTruthy ( ) ;
}
} ) ;
test ( '机床实时状态已更新 cnc_machine' , async ( ) = > {
const machines = await queryBusiness (
"SELECT id, last_collect_time, last_program_name, is_online FROM cnc_machine WHERE collect_address_id = 1 AND is_enabled = 1"
) ;
expect ( machines . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// 至少有一台机床的最后采集时间是测试开始之后
const recentMachines = machines . filter ( ( m : any ) = > {
if ( ! m . last_collect_time ) return false ;
return new Date ( m . last_collect_time ) >= testStartTime ;
} ) ;
expect ( recentMachines . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// 至少有一台机床是在线的
const onlineMachines = machines . filter ( ( m : any ) = > m . is_online === 1 ) ;
expect ( onlineMachines . length ) . toBeGreaterThanOrEqual ( 1 ) ;
} ) ;
await ctx . post ( ` ${ API_BASE } /api/collector/start ` , { headers : HEADERS } ) ;
await new Promise ( r = > setTimeout ( r , 2000 ) ) ;
test ( '采集地址状态已更新 cnc_collect_address' , async ( ) = > {
const addrs = await queryBusiness (
'SELECT last_collect_time, last_collect_status, fail_count FROM cnc_collect_address WHERE id = 1'
) ;
const resp1 = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
expect ( addrs . length ) . toBe ( 1 ) ;
const addr = addrs [ 0 ] ;
expect ( addr . last_collect_status ) . toBe ( 'success' ) ;
expect ( addr . fail_count ) . toBe ( 0 ) ;
expect ( new Date ( addr . last_collect_time ) >= testStartTime ) . toBe ( true ) ;
} ) ;
test ( '字段映射解析正确性 - 数值在合理范围' , async ( ) = > {
const records = await queryBusiness (
` SELECT program_name, part_count, spindle_speed_set, feed_speed_set,
spindle_speed_actual , feed_speed_actual , spindle_load , spindle_override ,
power_on_time , run_time , cutting_time , cycle_time
FROM cnc_collect_record WHERE created_at >= ? ORDER BY id DESC LIMIT 5 ` ,
[ testStartTime ]
) ;
expect ( records . length ) . toBeGreaterThanOrEqual ( 1 ) ;
for ( const r of records ) {
// 程序名应非空
expect ( r . program_name ) . toBeTruthy ( ) ;
// 零件数应 >= 0( DECIMAL字段mysql2返回字符串)
if ( r . part_count !== null ) {
expect ( Number ( r . part_count ) ) . toBeGreaterThanOrEqual ( 0 ) ;
}
// 开机时间应 > 0
if ( r . power_on_time !== null ) {
expect ( Number ( r . power_on_time ) ) . toBeGreaterThan ( 0 ) ;
}
// 运行时间应 >= 0
if ( r . run_time !== null ) {
expect ( Number ( r . run_time ) ) . toBeGreaterThanOrEqual ( 0 ) ;
}
}
} ) ;
test ( '成功计数递增' , async ( ) = > {
const resp1 = await collectorApi ( 'GET' , '/api/collector/status' ) ;
const body1 = await resp1 . json ( ) ;
const uptime1 = body1 . data . uptimeSeconds ;
const success1 = body1 . data . totalSucces s;
await new Promise ( r = > setTimeout ( r , 3000 ) ) ;
// 等待一个采集周期
await sleep ( 8000 ) ;
const resp2 = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
const resp2 = await c ollectorApi( 'GET' , '/api/collector/status' ) ;
const body2 = await resp2 . json ( ) ;
const uptime2 = body2 . data . uptimeSeconds ;
const success2 = body2 . data . totalSucces s;
expect ( uptime2 ) . toBeGreaterThanOrEqual ( uptime1 ) ;
expect ( success2) . toBeGreaterThanOrEqual ( success 1) ;
} ) ;
} ) ;
test ( '停止后再启动,状态正确切换' , async ( ) = > {
const ctx = await request . newContext ( ) ;
// ============================================================
// 套件3: 产量分段跟踪验证
// ============================================================
// 停止
await ctx . post ( ` ${ API_BASE } /api/collector/stop ` , { headers : HEADERS } ) ;
await new Promise ( r = > setTimeout ( r , 1000 ) ) ;
test . describe ( '套件3: 产量分段跟踪验证' , ( ) = > {
let statusResp = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
let statusBody = await statusResp . json ( ) ;
expect ( statusBody . data . isRunning ) . toBe ( false ) ;
test ( '产量段自动创建 - 存在未结账活跃段' , async ( ) = > {
const segments = await queryBusiness (
` SELECT id, machine_id, program_name, start_time, start_part_count, end_part_count
FROM cnc_production_segment
WHERE is_settled = 0 AND end_time IS NULL
ORDER BY id DESC LIMIT 10 `
) ;
expect ( segments . length ) . toBeGreaterThanOrEqual ( 1 ) ;
for ( const seg of segments ) {
expect ( seg . machine_id ) . toBeGreaterThan ( 0 ) ;
expect ( seg . program_name ) . toBeTruthy ( ) ;
expect ( seg . start_time ) . toBeTruthy ( ) ;
}
} ) ;
test ( '段内零件数实时更新 - end_part_count >= start_part_count' , async ( ) = > {
const segments = await queryBusiness (
` SELECT id, start_part_count, end_part_count, program_name
FROM cnc_production_segment
WHERE is_settled = 0 AND end_time IS NULL AND end_part_count IS NOT NULL
LIMIT 10 `
) ;
// 活跃段的 end_part_count 应 >= start_part_count( 模拟器在持续生产)
if ( segments . length > 0 ) {
for ( const seg of segments ) {
expect ( Number ( seg . end_part_count ) ) . toBeGreaterThanOrEqual ( Number ( seg . start_part_count ) ) ;
}
}
} ) ;
test ( '服务停止时活跃段自动结账' , async ( ) = > {
// 记录当前活跃段数量
const beforeSegments = await queryBusiness (
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 0 AND end_time IS NULL'
) ;
const beforeCount = ( beforeSegments [ 0 ] as any ) . cnt ;
if ( beforeCount === 0 ) {
// 没有活跃段,跳过此测试(标记通过)
return ;
}
// 启动
await ctx . post ( ` ${ API_BASE } /api/collector/start ` , { headers : HEADERS } ) ;
await new Promise ( r = > setTimeout ( r , 2000 ) ) ;
// 停止采集服务
await collectorApi ( 'POST' , '/api/collector/stop' ) ;
await sleep ( 2000 ) ;
// 验证:所有段都已结账
const afterSegments = await queryBusiness (
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 0 AND end_time IS NULL'
) ;
const afterCount = ( afterSegments [ 0 ] as any ) . cnt ;
expect ( afterCount ) . toBe ( 0 ) ;
// 验证:结账的段有 close_reason
const closedSegments = await queryBusiness (
` SELECT id, close_reason, end_time, quantity
FROM cnc_production_segment
WHERE is_settled = 1 AND close_reason IS NOT NULL
ORDER BY id DESC LIMIT 5 `
) ;
expect ( closedSegments . length ) . toBeGreaterThanOrEqual ( 1 ) ;
for ( const seg of closedSegments ) {
expect ( seg . close_reason ) . toBeTruthy ( ) ;
expect ( seg . end_time ) . toBeTruthy ( ) ;
}
// 恢复:重新启动采集服务(后续测试可能需要)
await collectorApi ( 'POST' , '/api/collector/start' ) ;
await sleep ( 5000 ) ;
} ) ;
} ) ;
// ============================================================
// 套件4: 异常处理与恢复验证
// ============================================================
statusResp = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
statusBody = await statusResp . json ( ) ;
test . describe ( '套件4: 异常处理与恢复验证' , ( ) = > {
test ( '模拟器不可达时采集服务优雅处理' , async ( ) = > {
// 1. 停止采集服务
await collectorApi ( 'POST' , '/api/collector/stop' ) ;
await sleep ( 1000 ) ;
// 2. 将地址URL改为不可达地址
await executeBusiness (
'UPDATE cnc_collect_address SET url = ? WHERE id = 1' ,
[ 'http://localhost:9999/' ]
) ;
// 3. 启动采集服务( 强制重建worker)
await collectorApi ( 'POST' , '/api/collector/start' ) ;
// 4. 验证:采集服务仍然在运行(没有崩溃)
const statusResp = await collectorApi ( 'GET' , '/api/collector/status' ) ;
const statusBody = await statusResp . json ( ) ;
expect ( statusBody . data . isRunning ) . toBe ( true ) ;
// 5. 轮询等待失败记录( 重试3次× 30秒间隔≈90秒才能完成)
let isFailed = false ;
for ( let i = 0 ; i < 12 ; i ++ ) {
await sleep ( 10000 ) ;
const addr = await queryBusiness (
'SELECT last_collect_status, fail_count FROM cnc_collect_address WHERE id = 1'
) ;
const addrData = addr [ 0 ] as any ;
isFailed = addrData . last_collect_status === 'fail' || Number ( addrData . fail_count ) > 0 ;
if ( isFailed ) break ;
}
expect ( isFailed ) . toBe ( true ) ;
} ) ;
test ( '工作线程状态包含预期字段' , async ( ) = > {
const ctx = await request . newContext ( ) ;
test ( '模拟器恢复后采集恢复正常' , async ( ) = > {
// 1. 停止采集服务
await collectorApi ( 'POST' , '/api/collector/stop' ) ;
await sleep ( 1000 ) ;
await ctx . post ( ` ${ API_BASE } /api/collector/start ` , { headers : HEADERS } ) ;
await new Promise ( r = > setTimeout ( r , 3000 ) ) ;
// 2. 恢复采集地址为模拟器真实端口
await executeBusiness (
'UPDATE cnc_collect_address SET url = ?, fail_count = 0 WHERE id = 1' ,
[ ` http://localhost: ${ simulationPort } / ` ]
) ;
const resp = await ctx . get ( ` ${ API_BASE } /api/collector/status ` , { headers : HEADERS } ) ;
const body = await resp . json ( ) ;
// 3. 启动采集服务( 强制重建worker加载正确URL)
await collectorApi ( 'POST' , '/api/collector/start' ) ;
// 4. 等待2个采集周期
await sleep ( 14000 ) ;
if ( body . data . workerCount > 0 ) {
const workers = body . data . workers ;
expect ( Array . isArray ( workers ) ) . toBe ( true ) ;
if ( workers . length > 0 ) {
const w = workers [ 0 ] ;
expect ( w ) . toHaveProperty ( 'addressId' ) ;
expect ( w ) . toHaveProperty ( 'url' ) ;
expect ( w ) . toHaveProperty ( 'isRunning' ) ;
// 5. 验证:采集地址状态恢复为成功
const addr = await queryBusiness (
'SELECT last_collect_status, fail_count FROM cnc_collect_address WHERE id = 1'
) ;
const addrData = addr [ 0 ] as any ;
expect ( addrData . last_collect_status ) . toBe ( 'success' ) ;
// 6. 验证:成功计数在增长
const statusResp = await collectorApi ( 'GET' , '/api/collector/status' ) ;
const statusBody = await statusResp . json ( ) ;
expect ( statusBody . data . totalSuccess ) . toBeGreaterThan ( 0 ) ;
} ) ;
test ( '不可达期间产生失败日志记录' , async ( ) = > {
// 查看最近的失败原始记录( 由测试17产生)
const failLogs = await queryLog (
` SELECT id, is_success, error_message FROM log_collect_raw
WHERE collect_address_id = 1 AND is_success = 0
ORDER BY id DESC LIMIT 5 `
) ;
// 应该有失败记录
if ( failLogs . length > 0 ) {
for ( const log of failLogs ) {
expect ( log . is_success ) . toBe ( 0 ) ;
expect ( log . error_message ) . toBeTruthy ( ) ;
}
}
} ) ;
} ) ;
// ============================================================
// 套件5: 心跳上报验证
// ============================================================
test . describe ( '套件5: 心跳上报验证' , ( ) = > {
test ( '心跳记录定时写入 log_collector_heartbeat' , async ( ) = > {
const heartbeats = await queryLog (
` SELECT id, service_id, status, success_count, fail_count, uptime_seconds, created_at
FROM log_collector_heartbeat
WHERE created_at >= ?
ORDER BY id DESC LIMIT 10 ` ,
[ testStartTime ]
) ;
expect ( heartbeats . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// 验证有心跳状态为 running 的记录
const runningHeartbeats = heartbeats . filter ( ( h : any ) = > h . status === 'running' ) ;
expect ( runningHeartbeats . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// 验证字段完整
const latest = heartbeats [ 0 ] as any ;
expect ( latest . service_id ) . toBeTruthy ( ) ;
expect ( Number ( latest . uptime_seconds ) ) . toBeGreaterThanOrEqual ( 0 ) ;
} ) ;
test ( '停止后心跳状态变更' , async ( ) = > {
// 停止采集
await collectorApi ( 'POST' , '/api/collector/stop' ) ;
await sleep ( 3000 ) ;
// 验证有 stopped 心跳记录
const stoppedHeartbeats = await queryLog (
` SELECT id, status FROM log_collector_heartbeat
WHERE status = 'stopped' AND created_at >= ?
ORDER BY id DESC LIMIT 5 ` ,
[ testStartTime ]
) ;
expect ( stoppedHeartbeats . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// 恢复采集服务
await collectorApi ( 'POST' , '/api/collector/start' ) ;
await sleep ( 3000 ) ;
} ) ;
} ) ;