Files
ARTEX/db/skill_usage_test.go
T
Autumn-27 834e94535a feat(skills): 新增 Skill 调用统计(账本 + 页面总览/详情)
后端:
- 新表 skill_usage(无外键,任务/会话删除后统计保留),一次 Skill()
  调用记一行,只记维度不记 args 正文;含 found 标记未命中调用
- 用 RunInfo 经 ctx 把 task/exploration/intent/session 传到工具装配层,
  在 Skill meta-tool 外包一层做埋点(可记 args 长度与不存在的 skill)
- fsListSkills 返回附带调用次数/覆盖任务/最近调用;新增
  GET /api/skills/missing 与 GET /api/skills/{name}/usage

前端(/system/skills):
- 树节点显示调用次数 badge;目录优先排序 + 子项缩进错开一级
- 空态改为技能库总览(指标卡 + 调用排行 + 最近/未使用)
- 选中 skill 的详情面板改为响应式左右分栏(配置在左、统计在右,
  窄屏回落上下),含最近调用明细与未命中缺口清单

Claude-Session: https://claude.ai/code/session_01BNN7npEJs4X6LfTtgniMd8
2026-08-20 12:30:01 -04:00

117 lines
2.9 KiB
Go

package db
import (
"testing"
)
// TestSkillUsageLedger writes a few rows and checks the three aggregates the
// skills page relies on: per-skill totals (resolved calls only), the miss list,
// and the recent-call list. Rows are namespaced by a unique skill name so the
// shared dev DB stays usable and the test cleans up after itself.
func TestSkillUsageLedger(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
const (
skillA = "zz-test-skill-a"
missing = "zz-test-skill-missing"
)
cleanup := func() {
_, _ = d.Exec(`DELETE FROM skill_usage WHERE skill IN ($1,$2)`, skillA, missing)
}
cleanup()
defer cleanup()
rows := []*SkillUsage{
{Skill: skillA, AgentKey: "worker", TaskID: 991, ExplorationID: 5, IntentID: 7, ArgsLen: 12, Found: true},
{Skill: skillA, AgentKey: "worker", TaskID: 991, ExplorationID: 5, ArgsLen: 0, Found: true},
{Skill: skillA, AgentKey: "planner", TaskID: 992, ExplorationID: 6, Found: true},
{Skill: skillA, AgentKey: "chatbot", SessionID: "conv-1", Found: true},
{Skill: missing, AgentKey: "worker", TaskID: 991, Found: false},
{Skill: missing, AgentKey: "worker", TaskID: 991, Found: false},
}
for _, r := range rows {
if err := d.InsertSkillUsage(r); err != nil {
t.Fatalf("insert %s: %v", r.Skill, err)
}
}
stats, err := d.SkillStats()
if err != nil {
t.Fatal(err)
}
var got *SkillStat
for i := range stats {
if stats[i].Skill == skillA {
got = &stats[i]
}
if stats[i].Skill == missing {
t.Fatalf("misses must not appear in SkillStats: %+v", stats[i])
}
}
if got == nil {
t.Fatalf("skill %s absent from SkillStats", skillA)
}
if got.Calls != 4 {
t.Errorf("calls: want 4, got %d", got.Calls)
}
// 991 + 992; the chat row has a NULL task_id and COUNT(DISTINCT) skips NULLs.
if got.Tasks != 2 {
t.Errorf("tasks: want 2, got %d", got.Tasks)
}
if len(got.Agents) != 3 {
t.Errorf("agents: want 3 distinct, got %v", got.Agents)
}
if got.LastUsed == nil {
t.Error("last_used must be set")
}
miss, err := d.MissingSkillStats(10)
if err != nil {
t.Fatal(err)
}
var missCalls int
for _, m := range miss {
if m.Skill == missing {
missCalls = m.Calls
}
}
if missCalls != 2 {
t.Errorf("missing calls: want 2, got %d", missCalls)
}
calls, err := d.RecentSkillCalls(skillA, 10)
if err != nil {
t.Fatal(err)
}
if len(calls) != 4 {
t.Fatalf("recent calls: want 4, got %d", len(calls))
}
// newest first
for i := 1; i < len(calls); i++ {
if calls[i].TS.After(calls[i-1].TS) {
t.Errorf("recent calls not newest-first at %d", i)
}
}
byTask, err := d.SkillCallsByTask(991)
if err != nil {
t.Fatal(err)
}
var taskCalls int
for _, s := range byTask {
if s.Skill == skillA {
taskCalls = s.Calls
}
if s.Skill == missing {
t.Errorf("misses must not appear in SkillCallsByTask: %+v", s)
}
}
if taskCalls != 2 {
t.Errorf("task 991 calls: want 2, got %d", taskCalls)
}
}