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 @@ -19,7 +19,7 @@ require (
github.com/jfrog/gofrog v1.7.6
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260527043943-fdf755c4f4c2
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260527100708-78f8364e88e1
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194
github.com/jfrog/jfrog-cli-evidence v0.9.4
github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260430094150-ce7d9b371c6f
github.com/jfrog/jfrog-cli-security v1.29.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,8 @@ github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64 h1:b
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64/go.mod h1:cKqb/JgN+XuD4RhOxvSZnyGyXw3cJsTZfQT3rk9MCho=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260527043943-fdf755c4f4c2 h1:acwlLYjjglecqjXXgj2JoM1bUhH/dDMpLpJXrBfvyqU=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260527043943-fdf755c4f4c2/go.mod h1:T5HDtDxHlUZWF4LQnmF2kiyFyd8yLOf3K618BsG0CWk=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260527100708-78f8364e88e1 h1:GRKvXZ0aLrQbKuMnaWrks9xCJoiRaSfVEZ57k2rd+b0=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260527100708-78f8364e88e1/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194 h1:cwppCKLitT0XBqYGQimW00qyx1ej88sY+rIjXAWNvAU=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE=
github.com/jfrog/jfrog-cli-evidence v0.9.4 h1:RAqZYaH2RrzmhW+bGA7dx/yTqa4X1fZ4/5V7VVMSJtc=
github.com/jfrog/jfrog-cli-evidence v0.9.4/go.mod h1:nLSLqLIhQz1Hi2n+KjHZTyK1mcmPLevv41LjItLswmE=
github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260430094150-ce7d9b371c6f h1:M1cesbKYSznwPA76dNctjCELxGx34TSSjwoYnJm9/6Y=
Expand Down
134 changes: 134 additions & 0 deletions metrics_visibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

corecommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
)

Expand Down Expand Up @@ -86,6 +87,139 @@ func startVisMockServer(t *testing.T) (*httptest.Server, chan visReq) {
return srv, ch
}

// TestVisibility_AgentContext_E2E verifies that agent execution context
// (is_agent / agent / is_interactive) flows end-to-end from env vars through
// the metrics pipeline into the visibility wire payload.
//
// ExecutionContext memoizes via sync.Once for the process lifetime, and other
// tests in this binary trigger the metrics path first under `go test`, so we
// must reset the cache before re-evaluating env vars and reset again on
// cleanup so this test cannot leak agent state into anything that follows.
func TestVisibility_AgentContext_E2E(t *testing.T) {
corecommands.ResetExecutionContextForTest()
t.Cleanup(corecommands.ResetExecutionContextForTest)
t.Setenv("CURSOR_AGENT", "1")

srv, ch := startMockServer(t)
defer srv.Close()

home := t.TempDir()
t.Setenv("JFROG_CLI_HOME_DIR", home)
t.Setenv("JFROG_CLI_REPORT_USAGE", "true")

jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials()
platformURL := srv.URL + "/"
artURL := srv.URL + "/artifactory/"
if err := jf.Exec("c", "add", "agent-mock",
"--url", platformURL,
"--artifactory-url", artURL,
"--access-token", "dummy",
"--interactive=false",
"--enc-password=false",
); err != nil {
t.Fatalf("config add failed: %v", err)
}
if err := jf.Exec("c", "use", "agent-mock"); err != nil {
t.Fatalf("config use failed: %v", err)
}

if err := jf.Exec("rt", "ping", "--server-id", "agent-mock"); err != nil {
t.Fatalf("jf exec failed: %v", err)
}

select {
case req := <-ch:
if req.Path != "/jfconnect/api/v1/backoffice/metrics/log" {
t.Fatalf("unexpected path: %s", req.Path)
}
var payload struct {
Labels struct {
IsAgent string `json:"is_agent"`
Agent string `json:"agent"`
IsInteractive string `json:"is_interactive"`
} `json:"labels"`
}
if err := json.Unmarshal(req.Body, &payload); err != nil {
t.Fatalf("bad JSON: %v\nbody: %s", err, string(req.Body))
}
if payload.Labels.IsAgent != "true" {
t.Errorf("is_agent: got %q want %q (body: %s)", payload.Labels.IsAgent, "true", string(req.Body))
}
if payload.Labels.Agent != "cursor" {
t.Errorf("agent: got %q want %q (body: %s)", payload.Labels.Agent, "cursor", string(req.Body))
}
// Stdout is not a TTY under `go test`, so is_interactive should be "false".
if payload.Labels.IsInteractive != "false" {
t.Errorf("is_interactive: got %q want %q (body: %s)", payload.Labels.IsInteractive, "false", string(req.Body))
}
case <-time.After(5 * time.Second):
t.Fatalf("timeout waiting for metrics POST")
}
}

// TestVisibility_NoAgent_E2E is the negative counterpart to
// TestVisibility_AgentContext_E2E: with no agent env var set, the wire payload
// must carry is_agent="false" and an empty agent name. ExecutionContext is
// reset because the positive test above memoizes IsAgent=true earlier in this
// binary.
func TestVisibility_NoAgent_E2E(t *testing.T) {
corecommands.ResetExecutionContextForTest()
t.Cleanup(corecommands.ResetExecutionContextForTest)
t.Setenv("CURSOR_AGENT", "")
t.Setenv("CLAUDECODE", "")
t.Setenv("AGENT", "")

srv, ch := startMockServer(t)
defer srv.Close()

home := t.TempDir()
t.Setenv("JFROG_CLI_HOME_DIR", home)
t.Setenv("JFROG_CLI_REPORT_USAGE", "true")

jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials()
platformURL := srv.URL + "/"
artURL := srv.URL + "/artifactory/"
if err := jf.Exec("c", "add", "noagent-mock",
"--url", platformURL,
"--artifactory-url", artURL,
"--access-token", "dummy",
"--interactive=false",
"--enc-password=false",
); err != nil {
t.Fatalf("config add failed: %v", err)
}
if err := jf.Exec("c", "use", "noagent-mock"); err != nil {
t.Fatalf("config use failed: %v", err)
}
if err := jf.Exec("rt", "ping", "--server-id", "noagent-mock"); err != nil {
t.Fatalf("jf exec failed: %v", err)
}

select {
case req := <-ch:
if req.Path != "/jfconnect/api/v1/backoffice/metrics/log" {
t.Fatalf("unexpected path: %s", req.Path)
}
var payload struct {
Labels struct {
IsAgent string `json:"is_agent"`
Agent string `json:"agent"`
} `json:"labels"`
}
if err := json.Unmarshal(req.Body, &payload); err != nil {
t.Fatalf("bad JSON: %v\nbody: %s", err, string(req.Body))
}
if payload.Labels.IsAgent != "false" {
t.Errorf("is_agent: got %q want %q (body: %s)", payload.Labels.IsAgent, "false", string(req.Body))
}
if payload.Labels.Agent != "" {
t.Errorf("agent: got %q want %q (body: %s)", payload.Labels.Agent, "", string(req.Body))
}
case <-time.After(5 * time.Second):
t.Fatalf("timeout waiting for metrics POST")
}
}

func TestVisibilitySendUsage_RtCurl_E2E(t *testing.T) {
srv, ch := startMockServer(t)
defer srv.Close()
Expand Down
Loading