Files
larksuite--cli/cmd/event/status_orphan_test.go
wehub-resource-sync bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

243 lines
6.6 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package event
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/larksuite/cli/internal/event/busdiscover"
"github.com/larksuite/cli/internal/event/protocol"
)
type fakeScanner struct {
procs []busdiscover.Process
err error
}
func (f *fakeScanner) ScanBusProcesses() ([]busdiscover.Process, error) {
return f.procs, f.err
}
type fakeBusQuerier struct {
respByAppID map[string]*protocol.StatusResponse
}
func (f *fakeBusQuerier) QueryBusStatus(appID string) (*protocol.StatusResponse, error) {
if r, ok := f.respByAppID[appID]; ok {
return r, nil
}
return nil, errors.New("dial failed")
}
func TestDeriveStatuses_RunningBus(t *testing.T) {
q := &fakeBusQuerier{
respByAppID: map[string]*protocol.StatusResponse{
"cli_a": protocol.NewStatusResponse(12345, 150, 1, nil),
},
}
sc := &fakeScanner{procs: nil}
statuses := deriveStatuses([]string{"cli_a"}, sc, q, time.Now())
if len(statuses) != 1 {
t.Fatalf("expected 1 status, got %d", len(statuses))
}
s := statuses[0]
if s.State != stateRunning {
t.Errorf("State = %v, want stateRunning", s.State)
}
if s.PID != 12345 {
t.Errorf("PID = %d, want 12345", s.PID)
}
if s.UptimeSec != 150 {
t.Errorf("UptimeSec = %d, want 150", s.UptimeSec)
}
}
func TestDeriveStatuses_OrphanBus(t *testing.T) {
q := &fakeBusQuerier{respByAppID: map[string]*protocol.StatusResponse{}}
sc := &fakeScanner{procs: []busdiscover.Process{
{PID: 70926, AppID: "cli_a", StartTime: time.Now().Add(-19 * time.Hour)},
}}
now := time.Now()
statuses := deriveStatuses([]string{"cli_a"}, sc, q, now)
if len(statuses) != 1 {
t.Fatalf("expected 1 status, got %d", len(statuses))
}
s := statuses[0]
if s.State != stateOrphan {
t.Errorf("State = %v, want stateOrphan", s.State)
}
if s.PID != 70926 {
t.Errorf("PID = %d, want 70926", s.PID)
}
wantUptime := int((19 * time.Hour).Seconds())
if s.UptimeSec < wantUptime-60 || s.UptimeSec > wantUptime+60 {
t.Errorf("UptimeSec = %d, want ~%d", s.UptimeSec, wantUptime)
}
}
func TestDeriveStatuses_NotRunning(t *testing.T) {
q := &fakeBusQuerier{respByAppID: map[string]*protocol.StatusResponse{}}
sc := &fakeScanner{procs: nil}
statuses := deriveStatuses([]string{"cli_a"}, sc, q, time.Now())
if len(statuses) != 1 {
t.Fatalf("expected 1 status, got %d", len(statuses))
}
s := statuses[0]
if s.State != stateNotRunning {
t.Errorf("State = %v, want stateNotRunning", s.State)
}
}
func TestDeriveStatuses_DiscoversOrphanAppIDsFromProcessScan(t *testing.T) {
q := &fakeBusQuerier{respByAppID: map[string]*protocol.StatusResponse{}}
sc := &fakeScanner{procs: []busdiscover.Process{
{PID: 70926, AppID: "cli_orphan", StartTime: time.Now().Add(-1 * time.Hour)},
}}
statuses := deriveStatuses([]string{"cli_known"}, sc, q, time.Now())
if len(statuses) != 2 {
t.Fatalf("expected 2 statuses, got %d: %+v", len(statuses), statuses)
}
byID := map[string]appStatus{}
for _, s := range statuses {
byID[s.AppID] = s
}
if byID["cli_known"].State != stateNotRunning {
t.Errorf("cli_known state = %v, want stateNotRunning", byID["cli_known"].State)
}
if byID["cli_orphan"].State != stateOrphan {
t.Errorf("cli_orphan state = %v, want stateOrphan", byID["cli_orphan"].State)
}
}
func TestDeriveStatuses_ScannerErrorIsNotFatal(t *testing.T) {
q := &fakeBusQuerier{
respByAppID: map[string]*protocol.StatusResponse{
"cli_a": protocol.NewStatusResponse(12345, 150, 1, nil),
},
}
sc := &fakeScanner{err: errors.New("ps failed")}
statuses := deriveStatuses([]string{"cli_a"}, sc, q, time.Now())
if len(statuses) != 1 {
t.Fatalf("expected 1 status, got %d", len(statuses))
}
if statuses[0].State != stateRunning {
t.Errorf("State = %v, want stateRunning (scanner error must not break running detection)", statuses[0].State)
}
}
func TestWriteStatusText_OrphanBlock(t *testing.T) {
var buf bytes.Buffer
statuses := []appStatus{{
AppID: "cli_XXXXXXXXXXXXXXXX",
State: stateOrphan,
PID: 70926,
UptimeSec: 68400,
}}
writeStatusText(&buf, statuses)
out := buf.String()
for _, want := range []string{
"── cli_XXXXXXXXXXXXXXXX ──",
"Bus: orphan (PID 70926, started 19h ago)",
"Issue: socket file missing — consumers cannot connect",
"Action: kill 70926",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q\nfull output:\n%s", want, out)
}
}
if strings.Contains(out, "running (PID") {
t.Errorf("orphan block must not contain 'running' text; got:\n%s", out)
}
}
func TestWriteStatusJSON_OrphanFields(t *testing.T) {
var buf bytes.Buffer
statuses := []appStatus{{
AppID: "cli_XXXXXXXXXXXXXXXX",
State: stateOrphan,
PID: 70926,
UptimeSec: 68400,
}}
if err := writeStatusJSON(&buf, statuses); err != nil {
t.Fatalf("writeStatusJSON: %v", err)
}
var payload struct {
Apps []map[string]interface{} `json:"apps"`
}
if err := json.Unmarshal(buf.Bytes(), &payload); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(payload.Apps) != 1 {
t.Fatalf("apps len = %d, want 1", len(payload.Apps))
}
a := payload.Apps[0]
if a["status"] != "orphan" {
t.Errorf("status = %v, want \"orphan\"", a["status"])
}
if a["running"] != false {
t.Errorf("running = %v, want false", a["running"])
}
if a["issue"] != "socket file missing" {
t.Errorf("issue = %v, want \"socket file missing\"", a["issue"])
}
if a["suggested_action"] != "kill 70926" {
t.Errorf("suggested_action = %v, want \"kill 70926\"", a["suggested_action"])
}
if pid, ok := a["pid"].(float64); !ok || int(pid) != 70926 {
t.Errorf("pid = %v, want 70926", a["pid"])
}
}
func TestWriteStatusJSON_RunningOmitsOrphanFields(t *testing.T) {
var buf bytes.Buffer
statuses := []appStatus{{
AppID: "cli_running",
State: stateRunning,
PID: 11111,
UptimeSec: 60,
Active: 0,
}}
if err := writeStatusJSON(&buf, statuses); err != nil {
t.Fatalf("writeStatusJSON: %v", err)
}
out := buf.String()
if strings.Contains(out, `"issue"`) {
t.Errorf("running status must not include 'issue' field; got:\n%s", out)
}
if strings.Contains(out, `"suggested_action"`) {
t.Errorf("running status must not include 'suggested_action' field; got:\n%s", out)
}
}
func TestHumanizeDuration(t *testing.T) {
for _, tt := range []struct {
d time.Duration
want string
}{
{30 * time.Second, "30s ago"},
{90 * time.Second, "1m ago"},
{45 * time.Minute, "45m ago"},
{90 * time.Minute, "1h ago"},
{5 * time.Hour, "5h ago"},
{30 * time.Hour, "1d ago"},
{80 * time.Hour, "3d ago"},
} {
got := humanizeDuration(tt.d)
if got != tt.want {
t.Errorf("humanizeDuration(%v) = %q, want %q", tt.d, got, tt.want)
}
}
}