mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
GET /api/skill-gaps aggregates skill_usage rows with found=false by skill, agent, and task category. Hits are returned for the same key so a miss rate needs no second query; hit-only skills are excluded. The source rows already existed: a miss is written when the model names a skill the platform does not provide. This is the evidence-backed capability backlog, not an auto-fixer: it creates no skill and changes no prompt. Motivated by the harness and skill-evolution sections of awesome-rsi (lobehub).
108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func TestSkillGapsReport(t *testing.T) {
|
|
m, err := NewManager(t.TempDir(), "")
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
defer m.Close()
|
|
task, err := m.CreateTask("skill gaps", "goal", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
taskID, _ := strconv.ParseInt(task.ID, 10, 64)
|
|
defer func() { _ = m.pg.DeleteTask(taskID) }()
|
|
defer func() { _, _ = m.pg.Exec(`DELETE FROM skill_usage WHERE task_id=$1`, taskID) }()
|
|
|
|
if _, err := m.pg.Exec(`
|
|
INSERT INTO skill_usage(skill,agent_key,task_id,found) VALUES
|
|
('rsi-test-graphql','worker',$1,false),
|
|
('rsi-test-graphql','worker',$1,false),
|
|
('rsi-test-graphql','worker',$1,true),
|
|
('rsi-test-cloud','worker',$1,false),
|
|
('rsi-test-known','worker',$1,true)`, taskID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
|
token, err := signJWT(s.jwtKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
get := func(path string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
rec := get("/api/skill-gaps")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var out struct {
|
|
Gaps []struct {
|
|
Skill string `json:"skill"`
|
|
AgentKey string `json:"agent_key"`
|
|
Misses int `json:"misses"`
|
|
Hits int `json:"hits"`
|
|
} `json:"gaps"`
|
|
Count int `json:"count"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bySkill := map[string]struct {
|
|
misses, hits int
|
|
agent string
|
|
}{}
|
|
for _, g := range out.Gaps {
|
|
bySkill[g.Skill] = struct {
|
|
misses, hits int
|
|
agent string
|
|
}{g.Misses, g.Hits, g.AgentKey}
|
|
}
|
|
if got := bySkill["rsi-test-graphql"]; got.misses != 2 || got.hits != 1 || got.agent != "worker" {
|
|
t.Fatalf("graphql gap=%+v want misses=2 hits=1 agent=worker", got)
|
|
}
|
|
if got := bySkill["rsi-test-cloud"]; got.misses != 1 {
|
|
t.Fatalf("cloud gap=%+v want misses=1", got)
|
|
}
|
|
if _, ok := bySkill["rsi-test-known"]; ok {
|
|
t.Fatal("a hit-only skill must not appear in the miss report")
|
|
}
|
|
first := out.Gaps[0].Skill
|
|
if first != "rsi-test-graphql" {
|
|
t.Fatalf("ordering: first=%q want rsi-test-graphql (highest misses)", first)
|
|
}
|
|
|
|
rec = get("/api/skill-gaps?limit=1")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("limit status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var limited struct {
|
|
Gaps []struct {
|
|
Skill string `json:"skill"`
|
|
} `json:"gaps"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &limited); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(limited.Gaps) != 1 || limited.Gaps[0].Skill != "rsi-test-graphql" {
|
|
t.Fatalf("limit=1 returned %+v", limited.Gaps)
|
|
}
|
|
if rec := get("/api/skill-gaps?limit=abc"); rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("bad limit status=%d want 400", rec.Code)
|
|
}
|
|
}
|