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: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
go 1.25.1

require (
github.com/flashcatcloud/go-flashduty v0.5.3-0.20260602040240-b12fb6a1ddb2
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07
github.com/mattn/go-runewidth v0.0.23
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/flashcatcloud/go-flashduty v0.5.3-0.20260602040240-b12fb6a1ddb2 h1:Tr563N4JAbclxnC9dWmwyPC39SCc/bifW0eVvCcnSyk=
github.com/flashcatcloud/go-flashduty v0.5.3-0.20260602040240-b12fb6a1ddb2/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07 h1:bi1rOjR2OY+TovBGabtVOTcEQWlgzU9RfEwlJxU+3n8=
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
Expand Down
69 changes: 58 additions & 11 deletions internal/cli/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ import (
"github.com/spf13/cobra"
)

// loadSpecPaths reads every GET/POST operation from the openapi spec shipped in
// the linked go-flashduty module — the same spec cligen generates against —
// returning operationId -> path.
func loadSpecPaths(t *testing.T) map[string]string {
// specOpMeta is the slice of an operation the coverage tests reason about.
type specOpMeta struct {
id string
path string
streaming bool // 200 body is not application/json (e.g. application/x-ndjson)
}

// loadSpecOps reads every public GET/POST operation from the openapi spec
// shipped in the linked go-flashduty module — the same spec cligen generates
// against — recording each op's id, path, and whether its 200 response is a
// non-JSON streaming body. Streaming ops are served by curated commands (the
// generated typed-response template cannot model an io.ReadCloser), so the
// generator-coverage check excludes them.
func loadSpecOps(t *testing.T) []specOpMeta {
t.Helper()
out, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/flashcatcloud/go-flashduty").Output()
if err != nil {
Expand All @@ -29,12 +39,15 @@ func loadSpecPaths(t *testing.T) map[string]string {
Paths map[string]map[string]struct {
OperationID string `json:"operationId"`
Tags []string `json:"tags"`
Responses map[string]struct {
Content map[string]json.RawMessage `json:"content"`
} `json:"responses"`
} `json:"paths"`
}
if err := json.Unmarshal(data, &spec); err != nil {
t.Fatalf("parse spec: %v", err)
}
ids := map[string]string{}
var ops []specOpMeta
for path, methods := range spec.Paths {
for verb, op := range methods {
v := strings.ToUpper(verb)
Expand All @@ -44,9 +57,25 @@ func loadSpecPaths(t *testing.T) map[string]string {
if op.OperationID == "" || len(op.Tags) == 0 {
continue
}
ids[op.OperationID] = path
streaming := false
if resp, ok := op.Responses["200"]; ok && len(resp.Content) > 0 {
if _, hasJSON := resp.Content["application/json"]; !hasJSON {
streaming = true
}
}
ops = append(ops, specOpMeta{id: op.OperationID, path: path, streaming: streaming})
}
}
return ops
}

// loadSpecPaths returns operationId -> path for every public GET/POST operation.
func loadSpecPaths(t *testing.T) map[string]string {
t.Helper()
ids := map[string]string{}
for _, op := range loadSpecOps(t) {
ids[op.id] = op.path
}
return ids
}

Expand Down Expand Up @@ -110,20 +139,38 @@ func TestEveryOperationHasPathCommand(t *testing.T) {
}

// TestGeneratorTargetsFullSpec asserts the generator emitted a command for every
// spec operation (no gaps, no phantom manifest entries from a stale run).
// non-streaming spec operation (no gaps, no phantom manifest entries from a
// stale run). Streaming ops (200 body is not application/json) are deliberately
// excluded from generation — they cannot be modeled by the typed-response
// template and are served by curated commands instead — so the manifest must NOT
// contain them and they are not required to be generated.
func TestGeneratorTargetsFullSpec(t *testing.T) {
specPaths := loadSpecPaths(t)
ops := loadSpecOps(t)
streaming := map[string]bool{}
wantGenerated := map[string]bool{}
for _, op := range ops {
if op.streaming {
streaming[op.id] = true
continue
}
wantGenerated[op.id] = true
}

gen := map[string]bool{}
for _, id := range generatedOpIDs {
gen[id] = true
if _, ok := specPaths[id]; !ok {
if streaming[id] {
t.Errorf("manifest op %q is streaming and must not be generated (curated only)", id)
}
if !wantGenerated[id] && !streaming[id] {
t.Errorf("manifest op %q is not in the current spec (regenerate cligen)", id)
}
}
for id := range specPaths {
for id := range wantGenerated {
if !gen[id] {
t.Errorf("op %q has no generated command (regenerate cligen)", id)
}
}
t.Logf("generator targets %d/%d spec operations", len(gen), len(specPaths))
t.Logf("generator targets %d/%d non-streaming spec operations (%d streaming, curated)",
len(gen), len(wantGenerated), len(streaming))
}
8 changes: 8 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,21 @@ func init() {
rootCmd.AddCommand(newWhoamiCmd())
rootCmd.AddCommand(newUpdateCmd())

// AI agent sessions (list + transcript export).
rootCmd.AddCommand(newSessionCmd())

// Diagnostics entry points (value-add over the raw API).
rootCmd.AddCommand(newMonitQueryCmd())
rootCmd.AddCommand(newMonitAgentCmd())

// Generated commands (full OpenAPI coverage). Registered AFTER curated
// commands so curated leaves win on any name conflict (see genAddLeaf).
registerGenerated(rootCmd)

// session/export is a streaming op excluded from the generated tree; attach
// its path-is-king leaf to the (now-existing) generated `safari` group so the
// operation stays reachable at safari session-export.
attachSafariSessionExport(rootCmd)
}

// Execute runs the root command.
Expand Down
Loading
Loading