- 神识 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 标记完成、能力矩阵神识行升已验证
106 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|