feat(hyperguild): brain pass-rate subcommand
Adds 'hyperguild brain pass-rate <skill> [--window 7d] [--json]' calling the new /pass-rate endpoint. Human output: tdd: 47 / 50 = 94% (window: 7d) or 'no data (window: 7d)' when pass_rate is null. PassRateResult mirrors the response envelope; PassRate is *float64 so null is preserved across decode.
This commit is contained in:
@@ -11,15 +11,17 @@ import (
|
||||
|
||||
func runBrain(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("subcommand required (query|write)")
|
||||
return errors.New("subcommand required (query|write|pass-rate)")
|
||||
}
|
||||
switch args[0] {
|
||||
case "query":
|
||||
return runBrainQuery(ctx, args[1:], stdin, stdout, stderr)
|
||||
case "write":
|
||||
return runBrainWrite(ctx, args[1:], stdin, stdout, stderr)
|
||||
case "pass-rate":
|
||||
return runBrainPassRate(ctx, args[1:], stdin, stdout, stderr)
|
||||
default:
|
||||
return fmt.Errorf("unknown subcommand: %s (expected query|write)", args[0])
|
||||
return fmt.Errorf("unknown subcommand: %s (expected query|write|pass-rate)", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,3 +73,34 @@ func runBrainWrite(ctx context.Context, args []string, stdin io.Reader, stdout,
|
||||
fmt.Fprintln(stdout, res.Path) //nolint:errcheck
|
||||
return nil
|
||||
}
|
||||
|
||||
func runBrainPassRate(ctx context.Context, args []string, _ io.Reader, stdout, stderr io.Writer) error {
|
||||
fs := flag.NewFlagSet("brain pass-rate", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
asJSON := fs.Bool("json", false, "output JSON instead of human-readable")
|
||||
window := fs.String("window", "7d", "lookback window (e.g. 1h, 24h, 7d, 30d)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("parse flags: %w", err)
|
||||
}
|
||||
if fs.NArg() < 1 {
|
||||
return errors.New("skill required")
|
||||
}
|
||||
skill := fs.Arg(0)
|
||||
|
||||
res, err := newBrainClient().PassRate(ctx, skill, *window)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *asJSON {
|
||||
enc := json.NewEncoder(stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(res)
|
||||
}
|
||||
if res.PassRate == nil {
|
||||
fmt.Fprintf(stdout, "%s: no data (window: %s)\n", res.Skill, res.Window) //nolint:errcheck
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s: %d / %d = %.0f%% (window: %s)\n", res.Skill, res.Pass, res.Total, *res.PassRate*100, res.Window) //nolint:errcheck
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -154,3 +154,67 @@ func TestRunBrainWrite_EmptyStdin(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, gotLen, "empty stdin should produce empty content payload")
|
||||
}
|
||||
|
||||
func TestRunBrainPassRate_Human(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"skill":"tdd","window":"7d","pass":47,"fail":3,"skip":0,"total":50,"pass_rate":0.94}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("BRAIN_URL", srv.URL)
|
||||
|
||||
var out, errBuf bytes.Buffer
|
||||
err := runBrain(context.Background(), []string{"pass-rate", "tdd"}, strings.NewReader(""), &out, &errBuf)
|
||||
require.NoError(t, err)
|
||||
got := out.String()
|
||||
assert.Contains(t, got, "tdd")
|
||||
assert.Contains(t, got, "47 / 50")
|
||||
assert.Contains(t, got, "94%")
|
||||
}
|
||||
|
||||
func TestRunBrainPassRate_NoData(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"skill":"tdd","window":"7d","pass":0,"fail":0,"skip":0,"total":0,"pass_rate":null}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("BRAIN_URL", srv.URL)
|
||||
|
||||
var out, errBuf bytes.Buffer
|
||||
err := runBrain(context.Background(), []string{"pass-rate", "tdd"}, strings.NewReader(""), &out, &errBuf)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, out.String(), "no data")
|
||||
}
|
||||
|
||||
func TestRunBrainPassRate_JSON(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"skill":"tdd","window":"7d","pass":47,"fail":3,"skip":0,"total":50,"pass_rate":0.94}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("BRAIN_URL", srv.URL)
|
||||
|
||||
var out, errBuf bytes.Buffer
|
||||
err := runBrain(context.Background(), []string{"pass-rate", "--json", "tdd"}, strings.NewReader(""), &out, &errBuf)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, out.String(), `"pass_rate": 0.94`)
|
||||
}
|
||||
|
||||
func TestRunBrainPassRate_MissingSkill(t *testing.T) {
|
||||
var out, errBuf bytes.Buffer
|
||||
err := runBrain(context.Background(), []string{"pass-rate"}, strings.NewReader(""), &out, &errBuf)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "skill required")
|
||||
}
|
||||
|
||||
func TestRunBrainPassRate_WindowFlag(t *testing.T) {
|
||||
gotWindow := ""
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotWindow = r.URL.Query().Get("window")
|
||||
_, _ = w.Write([]byte(`{"skill":"tdd","window":"30d","pass":0,"fail":0,"skip":0,"total":0,"pass_rate":null}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("BRAIN_URL", srv.URL)
|
||||
|
||||
var out, errBuf bytes.Buffer
|
||||
err := runBrain(context.Background(), []string{"pass-rate", "--window", "30d", "tdd"}, strings.NewReader(""), &out, &errBuf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "30d", gotWindow)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
@@ -119,3 +120,40 @@ func (c *brainClient) Write(ctx context.Context, kind, slug string, content io.R
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// PassRateResult mirrors the /pass-rate response envelope.
|
||||
type PassRateResult struct {
|
||||
Skill string `json:"skill"`
|
||||
Window string `json:"window"`
|
||||
Pass int `json:"pass"`
|
||||
Fail int `json:"fail"`
|
||||
Skip int `json:"skip"`
|
||||
Total int `json:"total"`
|
||||
PassRate *float64 `json:"pass_rate"`
|
||||
}
|
||||
|
||||
func (c *brainClient) PassRate(ctx context.Context, skill, window string) (*PassRateResult, error) {
|
||||
q := url.Values{}
|
||||
q.Set("skill", skill)
|
||||
q.Set("window", window)
|
||||
u := c.baseURL + "/pass-rate?" + q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("brain GET /pass-rate: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("brain GET /pass-rate: status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var out PassRateResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return nil, fmt.Errorf("decode /pass-rate response: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
@@ -84,6 +84,40 @@ func TestBrainClient_Write_Success(t *testing.T) {
|
||||
assert.Equal(t, "knowledge/find-h.md", res.Path)
|
||||
}
|
||||
|
||||
func TestBrainClient_PassRate_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.Equal(t, "/pass-rate", r.URL.Path)
|
||||
assert.Equal(t, "tdd", r.URL.Query().Get("skill"))
|
||||
assert.Equal(t, "7d", r.URL.Query().Get("window"))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"skill":"tdd","window":"7d","pass":47,"fail":3,"skip":0,"total":50,"pass_rate":0.94}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := &brainClient{baseURL: srv.URL, http: srv.Client()}
|
||||
res, err := c.PassRate(context.Background(), "tdd", "7d")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "tdd", res.Skill)
|
||||
assert.Equal(t, 47, res.Pass)
|
||||
assert.Equal(t, 3, res.Fail)
|
||||
require.NotNil(t, res.PassRate)
|
||||
assert.InDelta(t, 0.94, *res.PassRate, 0.001)
|
||||
}
|
||||
|
||||
func TestBrainClient_PassRate_NullRate(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"skill":"tdd","window":"7d","pass":0,"fail":0,"skip":0,"total":0,"pass_rate":null}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := &brainClient{baseURL: srv.URL, http: srv.Client()}
|
||||
res, err := c.PassRate(context.Background(), "tdd", "7d")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, res.PassRate)
|
||||
}
|
||||
|
||||
func TestNewBrainClient_DefaultURL(t *testing.T) {
|
||||
t.Setenv("BRAIN_URL", "")
|
||||
c := newBrainClient()
|
||||
|
||||
Reference in New Issue
Block a user