summaryrefslogtreecommitdiff
path: root/tests/FundLab.Api.Tests/PersistenceTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 03:03:59 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 03:03:59 +0800
commitea3a5026ad76028562ee4aa6b70c79c94fb17d4b (patch)
tree2a164631740656aa5935881e33d344392db4f36a /tests/FundLab.Api.Tests/PersistenceTests.fs
parentf55ed58d2c75b53774ef7256fddb973ae3e73323 (diff)
downloadfund-lab-ea3a5026ad76028562ee4aa6b70c79c94fb17d4b.tar.gz
fix(api): 加固行情采集子进程超时、取消与进程组清理并脱敏错误
- MarketDataService: setsid 独立会话启动采集器,硬超时覆盖子进程退出与管道 EOF 两个阶段;超时、取消与异常路径按进程组杀灭(含持有管道的后台子进程),不遗留孤儿进程 - App.ts/Search/RefreshNav 传入 ctx.RequestAborted,客户端断开即取消采集 - 对外错误仅返回统一短消息(503 MARKET_DATA_UNAVAILABLE 等),不泄露路径、stderr 或密钥;stderr 仅记录服务端日志 - 新增 9 项子进程回归(成功/stderr 洪泛/超时杀灭/取消杀灭/孤儿管道恢复/畸形载荷/非零退出/启动失败/缺脚本)与 HTTP 脱敏回归;全文套件 66 通过(Domain 19 / Web 17 / API 30) - 实机验证:挂起采集器 503 2.4s 无进程残留;真实 akshare 1.18.96 搜索 000001 返回 200,净值刷新持久化 6010 条观测 - README 与 qa/README 补充环境变量、回归命令、G1 过滤断言口径与真实建档标签辨析
Diffstat (limited to 'tests/FundLab.Api.Tests/PersistenceTests.fs')
-rw-r--r--tests/FundLab.Api.Tests/PersistenceTests.fs51
1 files changed, 49 insertions, 2 deletions
diff --git a/tests/FundLab.Api.Tests/PersistenceTests.fs b/tests/FundLab.Api.Tests/PersistenceTests.fs
index 5157a9c..64e857c 100644
--- a/tests/FundLab.Api.Tests/PersistenceTests.fs
+++ b/tests/FundLab.Api.Tests/PersistenceTests.fs
@@ -695,8 +695,8 @@ type PersistenceTests(fixture: PostgresFixture) =
let marketData =
{ new IMarketDataService with
- member _.Search _ = Ok searchPayload
- member _.RefreshNav _ = Ok [ observation ]
+ member _.Search(_, _) = Ok searchPayload
+ member _.RefreshNav(_, _) = Ok [ observation ]
member _.GetNav(_, _, _) = Ok [ observation ] }
let app = App.createApplicationWithMarketData (repository ()) marketData
@@ -726,6 +726,53 @@ type PersistenceTests(fixture: PostgresFixture) =
Assert.Contains("\"source\":\"akshare\"", navBody)
[<Fact>]
+ member _.``market data API keeps collector failures and process tree out of HTTP responses``() =
+ let dir = Path.Combine(Path.GetTempPath(), "fund-lab-collector-" + Guid.NewGuid().ToString("N"))
+
+ Directory.CreateDirectory(dir) |> ignore
+
+ try
+ let scriptPath = Path.Combine(dir, "hang.sh")
+
+ File.WriteAllText(
+ scriptPath,
+ "printf 'secret=%s\\n' 'fund-lab-HTTP-SENTINEL' >&2\nsleep 60 & child=$!\necho $child > pidfile\nwait $child\n"
+ )
+
+ let collector =
+ ProcessMarketDataCollector("/bin/sh", scriptPath, None, 1) :> IMarketDataCollector
+
+ let marketData = MarketDataService(repository (), collector)
+ let app = App.createApplicationWithMarketData (repository ()) marketData
+
+ let status, responseBody =
+ PersistenceTestHelpers.invoke
+ app
+ "GET"
+ "/api/instruments/search?q=000001"
+ [ "Authorization", "Bearer test-token" ]
+ ""
+
+ Assert.Equal(503, status)
+ Assert.Contains("MARKET_DATA_UNAVAILABLE", responseBody)
+ Assert.Contains("did not finish within 1 seconds", responseBody)
+ Assert.DoesNotContain(dir, responseBody)
+ Assert.DoesNotContain("fund-lab-HTTP-SENTINEL", responseBody)
+
+ let recordedPid = Int32.Parse(File.ReadAllText(Path.Combine(dir, "pidfile")).Trim())
+ let mutable dead = not (Directory.Exists(sprintf "/proc/%d" recordedPid))
+ let mutable waited = 0
+
+ while not dead && waited < 10000 do
+ Thread.Sleep(100)
+ waited <- waited + 100
+ dead <- not (Directory.Exists(sprintf "/proc/%d" recordedPid))
+
+ Assert.True(dead, "backgrounded subprocess survived the HTTP timeout")
+ finally
+ Directory.Delete(dir, true)
+
+ [<Fact>]
member _.``fund API rejects non-object JSON bodies``() =
let app = App.createApplication (repository ())