Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/cmd/agentic/agentic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3358,6 +3358,8 @@ owned by the server. JSON is an array; empty is []. Filter flags are optional;
explicit empty is Usage2. Loc is an exact verbatim loc, never shape-classified
as nodeId. No --since or total is available. Stderr labels the page offset and
limit; a full page says more may exist. No automatic drain or cost aggregation.
A malformed null event row fails with Error1 before any successful rows or page
note; JSON uses the normal error envelope. Valid empty results remain [].
`costMicroCents` is a nullable base-10 integer STRING, units1e-8USD; keep every
digit, never convert to float. Null is unpriced (human: unpriced), not zero.
Token nulls are unknown. Action arguments/caller payloads are not selected.
Expand Down
3 changes: 2 additions & 1 deletion internal/cmd/usage/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ is no --since, automatic paging, cost sum or billing balance.
costMicroCents stays an arbitrary-size base-10 integer string (units: 1e-8 USD),
not a floating point value. Null means unpriced and prints unpriced, never 0.
Token nulls remain unknown. JSON is an array of metadata objects; empty is [].
A malformed null event row fails before any rows or page note are printed.
Action arguments and caller identity payloads are not selected. No accounting
record is written and no model/provider call is made.`,
Args: cobra.NoArgs,
Expand Down Expand Up @@ -98,7 +99,7 @@ record is written and no model/provider call is made.`,
rows := []eventDTO{}
for _, e := range resp.UsageEvents {
if e == nil {
continue
return exitcode.Newf(exitcode.Error, "the server returned an invalid usage-event row")
}
rows = append(rows, eventDTO{ID: e.Id, Type: e.Type, CreatedAt: e.CreatedAt, MemoryID: e.MemoryId, NodeLoc: e.NodeLoc, Provider: e.Provider, Model: e.Model, KeyOwnerType: e.KeyOwnerType, TokensIn: e.TokensIn, TokensOut: e.TokensOut, CostMicroCents: e.CostMicroCents})
}
Expand Down
105 changes: 105 additions & 0 deletions internal/cmd/usage_null_1016_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cmd

import (
"encoding/json"
"fmt"
"strings"
"testing"
)

// #1016 reports a synthetic full page with a null entry; no original provider
// payload was retained. These fixtures reconstruct that documented shape.
func TestUsage1016MalformedPageIsNotSuccess(t *testing.T) {
for _, payload := range []struct{ name, rows string }{
{"full-null-last", usage65Event + `,null`},
{"full-null-first", `null,` + usage65Event},
{"partial-null", `null`},
} {
for _, asJSON := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/json=%v", payload.name, asJSON), func(t *testing.T) {
args := []string{"usage", "events", "--limit", "2", "--offset", "4"}
if asJSON {
args = append(args, "--json")
}
out, stderr, code, _ := usage65Run(t, `{"data":{"usageEvents":[`+payload.rows+`]}}`, args...)
if code != 1 {
t.Fatalf("exit=%d want1; stdout=%q stderr=%q", code, out, stderr)
}
if asJSON {
var envelope struct {
Error struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal([]byte(out), &envelope); err != nil || envelope.Error.Code != 1 || envelope.Error.Message != "the server returned an invalid usage-event row" {
t.Fatalf("expected error envelope, got %q (%v)", out, err)
}
} else if out != "" {
t.Fatalf("malformed page emitted success: %q", out)
}
if strings.Contains(stderr, "no total available") || strings.Contains(stderr, "more may exist") {
t.Fatalf("malformed page emitted page note: %q", stderr)
}
if !asJSON && !strings.Contains(stderr, "invalid usage-event row") {
t.Fatalf("missing fixed malformed-response diagnostic: %q", stderr)
}
})
}
}
}

func TestUsage1016ValidPagesKeepSemantics(t *testing.T) {
for _, tc := range []struct {
name, rows string
count int
full bool
}{
{"empty", "", 0, false}, {"partial", usage65Event, 1, false}, {"full", usage65Event + `,` + usage65Event, 2, true},
} {
for _, asJSON := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/json=%v", tc.name, asJSON), func(t *testing.T) {
args := []string{"usage", "events", "--limit", "2", "--offset", "4", "--loc", "a:b:c:d", "--memory", "acme.com:kb", "--type", "read"}
if asJSON {
args = append(args, "--json")
}
out, stderr, code, calls := usage65Run(t, `{"data":{"usageEvents":[`+tc.rows+`]}}`, args...)
if code != 0 {
t.Fatalf("exit%d %s %s", code, out, stderr)
}
if !strings.Contains(stderr, "page at offset 4 (limit 2); no total available") || strings.Contains(stderr, "more may exist") != tc.full {
t.Fatalf("page semantics: %q", stderr)
}
if tc.full && !strings.Contains(stderr, "--offset 6") {
t.Fatal(stderr)
}
var vars map[string]any
if err := json.Unmarshal(calls["UsageEvents"], &vars); err != nil {
t.Fatal(err)
}
if len(vars) != 5 || vars["limit"] != float64(2) || vars["offset"] != float64(4) || vars["loc"] != "a:b:c:d" || vars["memoryId"] != "hrn:mem:acme.com:kb" || vars["eventType"] != "read" {
t.Fatal(vars)
}
if asJSON {
if tc.count == 0 && strings.TrimSpace(out) != "[]" {
t.Fatalf("empty array: %q", out)
}
var rows []map[string]any
if err := json.Unmarshal([]byte(out), &rows); err != nil {
t.Fatal(err)
}
if rows == nil || len(rows) != tc.count {
t.Fatalf("array: %s", out)
}
for _, row := range rows {
if len(row) != 11 || row["costMicroCents"] != nil || row["tokensIn"] != nil {
t.Fatalf("projection: %s", out)
}
}
} else if tc.count > 0 && (!strings.Contains(out, "unpriced") || !strings.Contains(out, "unknown")) {
t.Fatal(out)
}
})
}
}
}
Loading