feat: 场景跑完生成报告,loop.ps1 reports
- scenarios/_lib.ps1:Start-LoopRun / Add-LoopStep / Set-LoopTicket / Save-LoopReport,请观察台把这次运行存成 run\reports\ 下的单文件 HTML - S1、S8 每个断言都记成一步,通过和失败都出报告;-NoReport 只断言不出报告 - S1 增加「Graph 应答 200」断言:之前 Graph 返回 400 时 IG 侧也记了出站,要到 send-txt 502 才判失败,原因不直观 - loop.ps1:观察台传 --reports-dir;新增 reports 命令列出最近的报告 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
e0bfe6ed3a
commit
d3c6dd07a9
|
|
@ -45,7 +45,11 @@
|
|||
|
||||
## 场景
|
||||
|
||||
`scenarios\` 下每个脚本独立可跑,通过退出 0、失败退出 1 并打印两侧记录。前提是 `up` 后五个角色都健康。
|
||||
`scenarios\` 下每个脚本独立可跑,通过退出 0、失败退出 1 并打印两侧记录。前提是 `up` 后各角色都健康。
|
||||
观察台在跑时,每次跑完(通过或失败)都会生成一份**场景报告**:这次的时序图、iXserver 内部步骤、逐步断言、部署版本,
|
||||
存成 `run\reports\<开始时间>-<场景>-<PASS|FAIL>.html`(单文件,离线可开,不进 git)。
|
||||
`.\loop.ps1 reports` 列出最近的,观察台 <http://127.0.0.1:5090/reports/> 里也能点开。加 `-NoReport` 只断言不出报告(省几秒)。
|
||||
新写场景照 S1 / S8 点源 `scenarios\_lib.ps1`:`Start-LoopRun` → 每个断言 `Add-LoopStep` → `Save-LoopReport`。
|
||||
|
||||
```powershell
|
||||
.\scenarios\s1-ig-text-echo.ps1 # IG 文本 → iXserver → 第三方 chatbot echo → IG 收到
|
||||
|
|
|
|||
16
loop.ps1
16
loop.ps1
|
|
@ -12,6 +12,7 @@
|
|||
reset-abp iCONAbpFX 还原到留底备份;-Init 首次打补丁并留底
|
||||
status 各角色进程、端口、健康
|
||||
logs 看某个角色的日志尾巴
|
||||
reports 列出最近的场景报告(run\reports\,观察台 /reports/ 里也能看)
|
||||
|
||||
.EXAMPLE
|
||||
.\loop.ps1 deploy
|
||||
|
|
@ -21,10 +22,11 @@
|
|||
.\loop.ps1 up
|
||||
.\loop.ps1 status
|
||||
.\loop.ps1 logs -Only ixserver
|
||||
.\loop.ps1 reports
|
||||
#>
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[ValidateSet('deploy', 'up', 'down', 'restart', 'reset', 'reset-abp', 'status', 'logs')]
|
||||
[ValidateSet('deploy', 'up', 'down', 'restart', 'reset', 'reset-abp', 'status', 'logs', 'reports')]
|
||||
[string]$Command = 'status',
|
||||
|
||||
# 只操作这些角色:abp, ixserver, chatbot, igmock, scrm
|
||||
|
|
@ -51,6 +53,7 @@ $Dirs = @{
|
|||
Logs = Join-Path $Run 'logs'
|
||||
Pids = Join-Path $Run 'pids'
|
||||
Tools = Join-Path $Run 'tools'
|
||||
Reports = Join-Path $Run 'reports'
|
||||
}
|
||||
foreach ($d in $Dirs.Values) { if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d | Out-Null } }
|
||||
|
||||
|
|
@ -243,7 +246,7 @@ function Get-RoleSpec([string]$role) {
|
|||
$cmd = "`"$(Get-NodeExe)`" `"$script`" --port $($p.Console)" +
|
||||
" --ig http://127.0.0.1:$($p.IgMock) --chatbot http://127.0.0.1:$($p.Chatbot) --scrm http://127.0.0.1:$($p.ScrmMock)" +
|
||||
" --ixserver http://localhost:$($p.Ixserver) --abp https://localhost:$($p.Abp)" +
|
||||
" --abp-log-dir `"$(Join-Path $Dirs.Abp 'Logs\Information')`" --ix-log-dir `"$(Join-Path $Dirs.Ixserver 'Log')`" --deployed `"$(Join-Path $Root 'deployed.json')`"" +
|
||||
" --abp-log-dir `"$(Join-Path $Dirs.Abp 'Logs\Information')`" --ix-log-dir `"$(Join-Path $Dirs.Ixserver 'Log')`" --deployed `"$(Join-Path $Root 'deployed.json')`" --reports-dir `"$($Dirs.Reports)`"" +
|
||||
" --sql-server `"$($s.Server)`" --sql-user $($s.User) --db $($s.IxserverDb)" +
|
||||
" --company-code $($Cfg.CompanyCode) --service-code $($Cfg.ServiceCode) --quota-name $($Cfg.QuotaName)"
|
||||
return @{
|
||||
|
|
@ -550,4 +553,13 @@ switch ($Command) {
|
|||
Get-Content (Join-Path $Dirs.Logs "$role.log") -Tail $Tail -ErrorAction SilentlyContinue | Out-Host
|
||||
}
|
||||
}
|
||||
'reports' {
|
||||
$files = @(Get-ChildItem $Dirs.Reports -Filter '*.html' -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First $Tail)
|
||||
if (-not $files.Count) { Write-Ok '还没有报告:跑一个 scenarios\ 下的脚本(观察台要在跑)'; return }
|
||||
$files | ForEach-Object {
|
||||
$result = if ($_.Name -like '*-PASS.html') { '通过' } else { '失败' }
|
||||
[pscustomobject]@{ Time = $_.LastWriteTime.ToString('MM-dd HH:mm:ss'); Result = $result; File = $_.Name }
|
||||
} | Format-Table -AutoSize | Out-Host
|
||||
Write-Ok "观察台里打开:http://127.0.0.1:$($Cfg.Ports.Console)/reports/"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<#
|
||||
场景脚本公用:计时、记下每一步断言,结束时(通过或失败)请闭环观察台生成场景报告。
|
||||
报告是观察台截取这段时间窗里的每一跳、状态变化、iXserver 日志,连同断言与部署版本存成的单文件 HTML,
|
||||
放在 run\reports\(不进 git),列表在 http://127.0.0.1:<Ports.Console>/reports/。
|
||||
|
||||
用法(在场景脚本里点源):
|
||||
. (Join-Path $PSScriptRoot '_lib.ps1')
|
||||
Start-LoopRun 'S1' 'IG 文本 → 第三方 echo → IG'
|
||||
Add-LoopStep 'iXserver 收 webhook' $true 'HTTP 200'
|
||||
Set-LoopTicket 1973133303
|
||||
Save-LoopReport -Passed $true # 或 Save-LoopReport -Passed $false -Reason '…'
|
||||
脚本带 -NoReport 时设 $LoopNoReport = $true 就只断言不出报告(省掉等日志落盘的几秒)。
|
||||
#>
|
||||
|
||||
$LoopCfg = Import-PowerShellDataFile (Join-Path $PSScriptRoot '..\config\loop.psd1')
|
||||
$script:LoopRun = $null
|
||||
|
||||
function Start-LoopRun([string]$Scenario, [string]$Title = '') {
|
||||
$script:LoopRun = [ordered]@{
|
||||
scenario = $Scenario
|
||||
title = $Title
|
||||
startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
steps = New-Object System.Collections.ArrayList
|
||||
ticketId = ''
|
||||
}
|
||||
}
|
||||
|
||||
function Add-LoopStep([string]$Name, [bool]$Ok, [string]$Detail = '') {
|
||||
if (-not $script:LoopRun) { return }
|
||||
[void]$script:LoopRun.steps.Add([ordered]@{ name = $Name; ok = $Ok; detail = $Detail })
|
||||
}
|
||||
|
||||
function Set-LoopTicket($TicketId) {
|
||||
if ($script:LoopRun -and $TicketId) { $script:LoopRun.ticketId = [string]$TicketId }
|
||||
}
|
||||
|
||||
function Save-LoopReport([bool]$Passed, [string]$Reason = '') {
|
||||
$run = $script:LoopRun
|
||||
if (-not $run -or $LoopNoReport) { return }
|
||||
$run.endedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
$run.passed = $Passed
|
||||
$run.reason = $Reason
|
||||
$console = "http://127.0.0.1:$($LoopCfg.Ports.Console)"
|
||||
try {
|
||||
$body = [Text.Encoding]::UTF8.GetBytes(($run | ConvertTo-Json -Depth 6 -Compress))
|
||||
# 观察台要等 iXserver 日志落盘、库轮询跟上才截取,这一步会停几秒
|
||||
$r = Invoke-RestMethod "$console/_api/report" -Method Post -ContentType 'application/json; charset=utf-8' -Body $body -TimeoutSec 60
|
||||
Write-Host "报告:$($r.file)($($r.hops) 跳 / $($r.logs) 条日志;观察台里打开 $console$($r.url))"
|
||||
}
|
||||
catch {
|
||||
# 报告是附带的:观察台没在跑不影响场景本身的结果
|
||||
Write-Host "没有生成报告(观察台没在跑?.\loop.ps1 up -Only console):$($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
<#
|
||||
S1:IG 用户发一条文本 → iXserver → 第三方 chatbot(echo 自动回复)→ iXserver → IG 用户收到 echo。
|
||||
前提:.\loop.ps1 up 四个角色都健康,chatbot 以 --auto-reply echo 运行(config\loop.psd1 的 ChatbotAutoReply)。
|
||||
通过退出 0,失败退出 1 并打印两侧的记录。用法:.\scenarios\s1-ig-text-echo.ps1 [-TimeoutSec 60]
|
||||
前提:.\loop.ps1 up 各角色都健康,chatbot 以 --auto-reply echo 运行(config\loop.psd1 的 ChatbotAutoReply)。
|
||||
通过退出 0,失败退出 1 并打印两侧的记录;两种情况都在 run\reports\ 生成场景报告(观察台在跑时)。
|
||||
用法:.\scenarios\s1-ig-text-echo.ps1 [-TimeoutSec 60] [-NoReport]
|
||||
#>
|
||||
param([int]$TimeoutSec = 60)
|
||||
param([int]$TimeoutSec = 60, [switch]$NoReport)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$Cfg = Import-PowerShellDataFile (Join-Path $PSScriptRoot '..\config\loop.psd1')
|
||||
$LoopNoReport = [bool]$NoReport
|
||||
. (Join-Path $PSScriptRoot '_lib.ps1')
|
||||
$Cfg = $LoopCfg
|
||||
$ig = "http://127.0.0.1:$($Cfg.Ports.IgMock)"
|
||||
$cb = "http://127.0.0.1:$($Cfg.Ports.Chatbot)"
|
||||
|
||||
|
|
@ -17,13 +20,17 @@ function Fail([string]$why) {
|
|||
Write-Host '--- chatbot 记录(最近 6 条)'
|
||||
# 记录是新的在前
|
||||
(Invoke-RestMethod "$cb/_api/state").events | Select-Object -First 6 | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Out-Host
|
||||
Add-LoopStep $why $false
|
||||
Save-LoopReport -Passed $false -Reason $why
|
||||
exit 1
|
||||
}
|
||||
|
||||
Start-LoopRun 'S1' 'IG 文本 → 第三方 echo → IG'
|
||||
Invoke-RestMethod "$ig/_api/clear" -Method Post | Out-Null
|
||||
$text = 'loop-s1 ' + [guid]::NewGuid().ToString('N').Substring(0, 8)
|
||||
$sent = Invoke-RestMethod "$ig/_api/send" -Method Post -ContentType 'application/json; charset=utf-8' -Body (@{ kind = 'text'; text = $text } | ConvertTo-Json)
|
||||
if ($sent.event.response.status -ne 200) { Fail "iXserver 收 webhook 返回 $($sent.event.response.status)" }
|
||||
Add-LoopStep 'iXserver 收下 webhook' $true "「$text」HTTP 200"
|
||||
|
||||
# 出站是异步的:轮询 IG 模拟的 outbound,带上限
|
||||
$expected = "echo: $text"
|
||||
|
|
@ -33,9 +40,13 @@ while (-not $hit -and (Get-Date) -lt $deadline) {
|
|||
$hit = @((Invoke-RestMethod "$ig/_api/state").events | Where-Object { $_.kind -eq 'outbound' -and $_.summary -eq $expected }) | Select-Object -First 1
|
||||
if (-not $hit) { Start-Sleep -Milliseconds 500 }
|
||||
}
|
||||
if (-not $hit) { Fail "$TimeoutSec 秒内 IG 用户没有收到「$expected」" }
|
||||
if (-not $hit) { Fail "$TimeoutSec 秒内 iXserver 没有向 IG 用户发出「$expected」" }
|
||||
Add-LoopStep 'iXserver 向 IG 用户发出 echo' $true $expected
|
||||
if (@($hit.issues).Count) { Fail "出站请求有问题:$($hit.issues -join ';')" }
|
||||
if (-not $hit.tokenPresent) { Fail '出站请求没带 access_token' }
|
||||
Add-LoopStep '出站请求带 token、报文无问题' $true
|
||||
if ($hit.response.status -ne 200) { Fail "Graph 应答 $($hit.response.status)(IG 模拟档位 $($hit.response.mode)),用户实际没收到" }
|
||||
Add-LoopStep 'Graph 应答 200,用户收到' $true $hit.response.messageId
|
||||
|
||||
# 也要等:iXserver 在 send-txt 应答之前就把消息发到 IG 了,客户端拿到应答才记 reply,会晚上百毫秒
|
||||
$reply = $null
|
||||
|
|
@ -44,7 +55,10 @@ while (-not $reply -and (Get-Date) -lt $deadline.AddSeconds(5)) {
|
|||
if (-not $reply) { Start-Sleep -Milliseconds 200 }
|
||||
}
|
||||
if (-not $reply) { Fail 'chatbot 侧没有对应的回复记录' }
|
||||
Set-LoopTicket $reply.ticketId
|
||||
if ($reply.response.status -ne 201) { Fail "send-txt 返回 $($reply.response.status)" }
|
||||
Add-LoopStep '第三方 send-txt 201' $true "ticket $($reply.ticketId)"
|
||||
|
||||
Write-Host "S1 通过:「$text」→ ticket $($reply.ticketId) → IG 收到「$expected」" -ForegroundColor Green
|
||||
Save-LoopReport -Passed $true
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
SCRM 开工(SetSetting IsEnable=false + UpdateCSAvailable 3)→ 客服上线(UserReg + 额度池)
|
||||
→ 新 IG 用户发消息 → 人工工单,SCRM 在 WebSocket 上收到推送 → 指派 → 打开(验证信息 + 已读)
|
||||
→ 以客服身份回复 → IG 用户收到 → 结束工单 → SCRM 下班(切回机器人、在线 0),不影响 S1。
|
||||
前提:.\loop.ps1 up 五个角色都健康。每次用新的 IGSID,保证建出新工单。
|
||||
通过退出 0,失败退出 1 并打印记录。用法:.\scenarios\s8-human-chain.ps1 [-TimeoutSec 60] [-KeepShift]
|
||||
前提:.\loop.ps1 up 各角色都健康。每次用新的 IGSID,保证建出新工单。
|
||||
通过退出 0,失败退出 1 并打印记录;两种情况都在 run\reports\ 生成场景报告(观察台在跑时)。
|
||||
用法:.\scenarios\s8-human-chain.ps1 [-TimeoutSec 60] [-KeepShift] [-NoReport]
|
||||
#>
|
||||
param([int]$TimeoutSec = 60, [switch]$KeepShift)
|
||||
param([int]$TimeoutSec = 60, [switch]$KeepShift, [switch]$NoReport)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$Cfg = Import-PowerShellDataFile (Join-Path $PSScriptRoot '..\config\loop.psd1')
|
||||
$LoopNoReport = [bool]$NoReport
|
||||
. (Join-Path $PSScriptRoot '_lib.ps1')
|
||||
$Cfg = $LoopCfg
|
||||
$ig = "http://127.0.0.1:$($Cfg.Ports.IgMock)"
|
||||
$sc = "http://127.0.0.1:$($Cfg.Ports.ScrmMock)"
|
||||
|
||||
|
|
@ -31,13 +34,16 @@ function Fail([string]$why) {
|
|||
(Invoke-RestMethod "$sc/_api/state").events | Select-Object -First 15 | ForEach-Object { ($_ | ConvertTo-Json -Depth 8 -Compress) } | Out-Host
|
||||
Write-Host '--- IG 模拟记录'
|
||||
(Invoke-RestMethod "$ig/_api/state").events | ForEach-Object { "{0} {1} {2}" -f $_.kind, $_.igsid, $_.summary } | Out-Host
|
||||
Add-LoopStep $why $false
|
||||
if (-not $KeepShift) { Post "$sc/_api/shift" @{ on = $false } | Out-Null }
|
||||
Save-LoopReport -Passed $false -Reason $why
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Step([string]$action, $body) {
|
||||
function Step([string]$name, [string]$action, $body) {
|
||||
$r = Post "$sc/_api/$action" $body
|
||||
if (-not $r.ok) { Fail "$action 未成功:$($r.error) $((@($r.calls) | ForEach-Object { $_.api + ' → ' + ($_.response.body.msg) }) -join ';')" }
|
||||
if (-not $r.ok) { Fail "$name($action)未成功:$($r.error) $((@($r.calls) | ForEach-Object { $_.api + ' → ' + ($_.response.body.msg) }) -join ';')" }
|
||||
Add-LoopStep $name $true ((@($r.calls) | ForEach-Object { $_.api }) -join ' + ')
|
||||
return $r
|
||||
}
|
||||
|
||||
|
|
@ -51,37 +57,44 @@ function Wait-For([string]$what, [scriptblock]$probe) {
|
|||
Fail "$TimeoutSec 秒内没等到:$what"
|
||||
}
|
||||
|
||||
Start-LoopRun 'S8' 'SCRM 人工链路'
|
||||
$ws = (Invoke-RestMethod "$sc/_api/state").state.ws
|
||||
if ($ws.status -ne 'logged-in') { Fail "SCRM 模拟的 WebSocket 没登录($($ws.status) $($ws.lastError))" }
|
||||
|
||||
Invoke-RestMethod "$sc/_api/clear" -Method Post | Out-Null
|
||||
Invoke-RestMethod "$ig/_api/clear" -Method Post | Out-Null
|
||||
|
||||
Step 'shift' @{ on = $true } | Out-Null
|
||||
Step 'agent-online' @{} | Out-Null
|
||||
Step 'SCRM 开工(切人工、在线 3)' 'shift' @{ on = $true } | Out-Null
|
||||
Step '客服上线' 'agent-online' @{} | Out-Null
|
||||
|
||||
$igsid = 'loop-s8-' + [guid]::NewGuid().ToString('N').Substring(0, 8)
|
||||
$sent = Post "$ig/_api/send" @{ kind = 'text'; igsid = $igsid; text = 'S8 想找人工' }
|
||||
if ($sent.event.response.status -ne 200) { Fail "iXserver 收 webhook 返回 $($sent.event.response.status)" }
|
||||
Add-LoopStep '新 IG 用户发消息' $true "$igsid HTTP 200"
|
||||
|
||||
# 推送报文没有统一类型字段,按 enduser_openid = IGSID 认出这张新工单
|
||||
$ticketId = Wait-For 'SCRM 收到这位 IG 用户的工单推送' {
|
||||
$push = @((Invoke-RestMethod "$sc/_api/state").events | Where-Object { $_.kind -eq 'push' -and (($_.parsed | ConvertTo-Json -Depth 8 -Compress) -match [regex]::Escape($igsid)) }) | Select-Object -First 1
|
||||
if ($push) { $push.ticketIds | Select-Object -First 1 }
|
||||
}
|
||||
Set-LoopTicket $ticketId
|
||||
Add-LoopStep 'SCRM 收到工单推送' $true "ticket $ticketId"
|
||||
|
||||
Step 'assign' @{ ticket_id = $ticketId } | Out-Null
|
||||
Step 'open' @{ ticket_id = $ticketId } | Out-Null
|
||||
Step '指派给客服' 'assign' @{ ticket_id = $ticketId } | Out-Null
|
||||
Step '打开工单(验证信息 + 已读)' 'open' @{ ticket_id = $ticketId } | Out-Null
|
||||
$replyText = 'S8 客服回复 ' + [guid]::NewGuid().ToString('N').Substring(0, 6)
|
||||
Step 'reply' @{ ticket_id = $ticketId; text = $replyText } | Out-Null
|
||||
Step '客服回复' 'reply' @{ ticket_id = $ticketId; text = $replyText } | Out-Null
|
||||
|
||||
$out = Wait-For "IG 用户收到「$replyText」" {
|
||||
@((Invoke-RestMethod "$ig/_api/state").events | Where-Object { $_.kind -eq 'outbound' -and $_.igsid -eq $igsid -and $_.summary -eq $replyText }) | Select-Object -First 1
|
||||
}
|
||||
if (@($out.issues).Count) { Fail "出站请求有问题:$($out.issues -join ';')" }
|
||||
if ($out.response.status -ne 200) { Fail "Graph 应答 $($out.response.status)(IG 模拟档位 $($out.response.mode)),用户实际没收到" }
|
||||
Add-LoopStep 'IG 用户收到客服回复' $true $replyText
|
||||
|
||||
Step 'complete' @{ ticket_id = $ticketId } | Out-Null
|
||||
if (-not $KeepShift) { Step 'shift' @{ on = $false } | Out-Null }
|
||||
Step '结束工单' 'complete' @{ ticket_id = $ticketId } | Out-Null
|
||||
if (-not $KeepShift) { Step 'SCRM 下班(切回机器人、在线 0)' 'shift' @{ on = $false } | Out-Null }
|
||||
|
||||
Write-Host "S8 通过:$igsid → 人工工单 $ticketId → SCRM 收到推送、指派、回复 → IG 收到「$replyText」→ 已结束" -ForegroundColor Green
|
||||
Save-LoopReport -Passed $true
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue