Files
hunjing/shenshi/internal/store/store_test.go
T
chenxin 144415247e M4 收口:神识用户面与计量闭环——BYOK/配额三段式/usage 落库/Anthropic+Gemini 适配器/神识阁
- 神识 Go:store 双实现(PG 自动建表+60s 就绪重试 / 内存回退)承载 usage_logs·byok_keys·quota_balances;BYOK 密钥库(AES-GCM 加密、API 脱敏、撤回即失效、主密钥未配置 fail-closed);配额三段式(预扣 max_tokens 钳位 1M→结算实际 token→全败幂等退款,HJ-2004);计量归属(IR user_id→usage_logs,/me/usage 用户维度聚合+配额总览,/admin/usage/sample 对账抽样);Anthropic/Gemini 出站适配器(SSE 解析、system 提升、用量映射);stub 端点补名修复计量空 endpoint
- IR 契约:user_id/byok_key_id 双端同步(Go types.go + Py types.py)
- 灵台:runs.owner_id(迁移 005,server_default 兼容存量)经 kernel 全链透传为 IR user_id;shenshi_client 扩展用户面方法(X-Hunjing-User-Id 代理,Echo 桩带内存实现);/me/byok-keys 登记·列表·撤回与 /me/usage 端点(4 用例)
- 前端神识阁(/models 重建):偏好(生成参数表单)/自带密钥(登记加密保存、脱敏列表、撤回二次确认)/用量(聚合表+配额进度条)三区;me API 库+mock+5 用例;侧栏恢复入口
- 配置与部署:.env.example 增 SHENSHI_DATABASE_URL/SHENSHI_BYOK_MASTER_KEY/SHENSHI_USER_BUDGET_TOKENS;compose 神识容器接自有库(init-shenshi.sql 预建 hunjing_shenshi,与灵台不共库共表)
- 证据:Go 34 用例(BYOK 独占/配额拒绝/结算退款/归属聚合)、后端 532、前端 210、BFF 4 全绿;跨进程 E2E 四场景(灵台 HttpShenshiClient→真 Go 神识:登记脱敏/BYOK 独占不可达整体失败/用量归属聚合/撤回即失效)通过;rewrite-plan M4 标记完成、能力矩阵神识行升已验证
2026-08-27 18:39:17 +08:00

106 lines
2.9 KiB
Go

// store 语义契约测试(内存实现;PG 实现同接口同语义)。
package store
import (
"testing"
"time"
)
func newUsage(id, user, model, endpoint, status string, p, c int) UsageLog {
return UsageLog{ID: id, RunID: "run_t", UserID: user, Model: model, Slot: "chat",
Endpoint: endpoint, Status: status, PromptTokens: p, CompletionTokens: c,
At: time.Now().UTC()}
}
func TestUsageAggregateFiltersByUserAndCountsOK(t *testing.T) {
st := NewMem()
_ = st.InsertUsage(newUsage("1", "u1", "m", "e1", "ok", 10, 20))
_ = st.InsertUsage(newUsage("2", "u1", "m", "e1", "error", 0, 0))
_ = st.InsertUsage(newUsage("3", "u2", "m", "e2", "ok", 1, 2))
rows, err := st.AggregateUsage("u1")
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 {
t.Fatalf("u1 聚合行数 = %d, 期望 1", len(rows))
}
r := rows[0]
if r.Requests != 2 || r.OK != 1 || r.PromptTokens != 10 || r.CompletionTokens != 20 {
t.Fatalf("聚合字段错误: %+v", r)
}
all, _ := st.AggregateUsage("")
if len(all) != 2 {
t.Fatalf("全体聚合行数 = %d, 期望 2", len(all))
}
}
func TestUsageSampleReturnsLatest(t *testing.T) {
st := NewMem()
for i := 0; i < 5; i++ {
_ = st.InsertUsage(newUsage(string(rune('a'+i)), "u1", "m", "e", "ok", i, 0))
}
rows, _ := st.SampleUsage(2)
if len(rows) != 2 || rows[1].ID != "e" {
t.Fatalf("抽样应返回最新 2 条(d,e),实际 %+v", rows)
}
}
func TestByokLifecycle(t *testing.T) {
st := NewMem()
key := ByokKey{ID: "byok_x", UserID: "u1", Name: "主力", Type: "openai_compatible",
BaseURL: "https://api.example.com", APIKey: "enc", Model: "m1", CreatedAt: time.Now().UTC()}
if err := st.UpsertByok(key); err != nil {
t.Fatal(err)
}
got, err := st.GetByok("u1", "byok_x")
if err != nil || got == nil || got.Model != "m1" {
t.Fatalf("GetByok 失败: %v %+v", err, got)
}
// 跨用户隔离
if other, _ := st.GetByok("u2", "byok_x"); other != nil {
t.Fatal("u2 不应读到 u1 的密钥")
}
if err := st.RevokeByok("u1", "byok_x"); err != nil {
t.Fatal(err)
}
got2, _ := st.GetByok("u1", "byok_x")
if got2 != nil {
t.Fatal("撤回后不应可读")
}
if err := st.RevokeByok("u1", "byok_x"); err != ErrByokNotFound {
t.Fatalf("重复撤回应返回 ErrByokNotFound,实际 %v", err)
}
}
func TestQuotaSaturatingRefund(t *testing.T) {
st := NewMem()
if err := st.Reserve("u1", 100); err != nil {
t.Fatal(err)
}
// 幂等退款:重复退款不产生负余额
if err := st.Refund("u1", 100); err != nil {
t.Fatal(err)
}
if err := st.Refund("u1", 100); err != nil {
t.Fatal(err)
}
b, _ := st.Balance("u1")
if b.Reserved < 0 || b.Used != 0 {
t.Fatalf("重复退款后余额为负: %+v", b)
}
}
func TestQuotaSettleReleasesDifference(t *testing.T) {
st := NewMem()
_ = st.Reserve("u1", 100)
if err := st.Settle("u1", 100, 37); err != nil {
t.Fatal(err)
}
b, _ := st.Balance("u1")
if b.Reserved != 0 || b.Used != 37 {
t.Fatalf("结算后应释放全部预扣并记实际用量: %+v", b)
}
}