Files
zzet--gortex/cmd/gortex/proxy_test.go
T
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

282 lines
8.8 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
)
// TestResolveLaunchCWDFallsBackFromHome exercises the gortexhq/gortex#19
// path: Cursor launches the user-level MCP entry with cwd=$HOME. The
// resolver should prefer the editor-provided CURSOR_WORKSPACE env var
// over the ambiguous home cwd.
func TestResolveLaunchCWDFallsBackFromHome(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil || home == "" {
t.Skip("no resolvable home directory in test env")
}
// Make a real directory we can chdir into and treat as $HOME for
// the duration of the test, so isAmbiguousLaunchCWD sees the same
// path os.Getwd() reports.
fakeHome := t.TempDir()
project := filepath.Join(t.TempDir(), "myproject")
if err := os.MkdirAll(project, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("HOME", fakeHome)
t.Setenv("PWD", "") // clear so we exercise the editor-env path
t.Setenv("CURSOR_WORKSPACE", project)
t.Setenv("CLAUDE_CODE_WORKSPACE", "")
t.Setenv("WINDSURF_WORKSPACE", "")
t.Setenv("KIRO_WORKSPACE", "")
t.Setenv("CODEX_WORKSPACE", "")
t.Setenv("ANTIGRAVITY_WORKSPACE", "")
t.Setenv("VSCODE_WORKSPACE", "")
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(origDir) })
if err := os.Chdir(fakeHome); err != nil {
t.Fatal(err)
}
got, err := resolveLaunchCWD()
if err != nil {
t.Fatalf("resolveLaunchCWD: %v", err)
}
if got != project {
t.Errorf("resolveLaunchCWD = %q, want %q (the CURSOR_WORKSPACE env)", got, project)
}
}
// TestResolveLaunchCWDPrefersGetwdWhenSafe confirms the resolver
// doesn't second-guess a normal cwd. When os.Getwd() returns a
// project directory (not `/`, not $HOME), the env-var fallbacks must
// not override it — otherwise a user with a stale CURSOR_WORKSPACE
// would be pinned to the wrong project.
func TestResolveLaunchCWDPrefersGetwdWhenSafe(t *testing.T) {
project := t.TempDir()
stale := t.TempDir()
t.Setenv("CURSOR_WORKSPACE", stale)
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(origDir) })
if err := os.Chdir(project); err != nil {
t.Fatal(err)
}
got, err := resolveLaunchCWD()
if err != nil {
t.Fatalf("resolveLaunchCWD: %v", err)
}
// On macOS t.TempDir() lives under /private/var/... but Getwd may
// return /var/... after resolving the /var → /private/var symlink
// (or vice versa). Compare by resolved path.
wantResolved, _ := filepath.EvalSymlinks(project)
gotResolved, _ := filepath.EvalSymlinks(got)
if gotResolved != wantResolved {
t.Errorf("resolveLaunchCWD = %q, want %q (real cwd, not stale CURSOR_WORKSPACE)", got, project)
}
}
// TestResolveLaunchCWDFallsBackToPWDFromRoot covers the Antigravity
// `cwd=/` case the previous code special-cased: when Getwd() is `/`
// and PWD points at a real directory, use PWD.
func TestResolveLaunchCWDFallsBackToPWDFromRoot(t *testing.T) {
project := t.TempDir()
t.Setenv("PWD", project)
t.Setenv("CURSOR_WORKSPACE", "")
t.Setenv("CLAUDE_CODE_WORKSPACE", "")
t.Setenv("WINDSURF_WORKSPACE", "")
t.Setenv("KIRO_WORKSPACE", "")
t.Setenv("CODEX_WORKSPACE", "")
t.Setenv("ANTIGRAVITY_WORKSPACE", "")
t.Setenv("VSCODE_WORKSPACE", "")
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(origDir) })
if err := os.Chdir("/"); err != nil {
t.Fatal(err)
}
got, err := resolveLaunchCWD()
if err != nil {
t.Fatalf("resolveLaunchCWD: %v", err)
}
wantResolved, _ := filepath.EvalSymlinks(project)
gotResolved, _ := filepath.EvalSymlinks(got)
if gotResolved != wantResolved {
t.Errorf("resolveLaunchCWD from / = %q, want %q (the PWD fallback)", got, project)
}
}
// TestOrphanWatch_FiresOnReparent confirms the watchdog fires once when the
// parent PID changes (the parent process died and we were reparented).
func TestOrphanWatch_FiresOnReparent(t *testing.T) {
var calls atomic.Int32
getppid := func() int {
if calls.Add(1) == 1 {
return 4242 // original parent observed at arm time
}
return 1 // reparented to init after the parent exited
}
fired := make(chan struct{}, 1)
go orphanWatch(context.Background(), time.Millisecond, getppid, func() { fired <- struct{}{} })
select {
case <-fired:
case <-time.After(2 * time.Second):
t.Fatal("orphanWatch did not fire after the parent PID changed")
}
}
// TestOrphanWatch_FiresOnSubreaperReparent confirms the change-detection
// catches reparenting to a subreaper (PID != 1) — the case a bare `== 1`
// check would miss in containers / systemd user sessions.
func TestOrphanWatch_FiresOnSubreaperReparent(t *testing.T) {
var calls atomic.Int32
getppid := func() int {
if calls.Add(1) == 1 {
return 4242
}
return 99 // reparented to a subreaper, not init
}
fired := make(chan struct{}, 1)
go orphanWatch(context.Background(), time.Millisecond, getppid, func() { fired <- struct{}{} })
select {
case <-fired:
case <-time.After(2 * time.Second):
t.Fatal("orphanWatch must fire on subreaper reparenting, not just PID 1")
}
}
// TestOrphanWatch_StableParentNeverFires confirms a live, unchanging parent
// never trips the watchdog.
func TestOrphanWatch_StableParentNeverFires(t *testing.T) {
getppid := func() int { return 4242 }
fired := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
go orphanWatch(ctx, time.Millisecond, getppid, func() { fired <- struct{}{} })
time.Sleep(50 * time.Millisecond) // ~50 stable polls
cancel()
select {
case <-fired:
t.Fatal("orphanWatch fired despite a stable parent PID")
default:
}
}
// TestOrphanWatch_Disarms confirms the watchdog never arms when there is no
// meaningful parent to watch (already init / no parent) or the interval is
// non-positive — it returns immediately and never fires.
func TestOrphanWatch_Disarms(t *testing.T) {
cases := map[string]struct {
ppid int
interval time.Duration
}{
"already init": {1, time.Millisecond},
"no parent": {0, time.Millisecond},
"non-pos interval": {4242, 0},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
fired := make(chan struct{}, 1)
done := make(chan struct{})
go func() {
orphanWatch(context.Background(), tc.interval,
func() int { return tc.ppid },
func() { fired <- struct{}{} })
close(done)
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("orphanWatch should return immediately when disarmed")
}
select {
case <-fired:
t.Fatal("orphanWatch must not fire when disarmed")
default:
}
})
}
}
// TestColdStartHandshakeStaticTools proves the proxy can answer the handshake
// frames locally before the daemon connects: initialize and tools/list get a
// synthesized response (id echoed), while tools/call is left for the daemon.
func TestColdStartHandshakeStaticTools(t *testing.T) {
// initialize → a static result carrying serverInfo + instructions.
reply, ok := answerColdStart([]byte(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`), nil)
if !ok {
t.Fatal("initialize must be answerable at cold start")
}
var initResp struct {
ID json.RawMessage `json:"id"`
Result struct {
ServerInfo struct{ Name string `json:"name"` } `json:"serverInfo"`
Instructions string `json:"instructions"`
} `json:"result"`
}
if err := json.Unmarshal(reply, &initResp); err != nil {
t.Fatalf("initialize reply not JSON: %v", err)
}
if initResp.Result.ServerInfo.Name != "gortex" {
t.Errorf("serverInfo.name = %q, want gortex", initResp.Result.ServerInfo.Name)
}
if string(initResp.ID) != "1" {
t.Errorf("request id not echoed: %s", initResp.ID)
}
if initResp.Result.Instructions == "" {
t.Error("cold-start initialize must carry instructions")
}
// tools/list → the static cold-start core set.
reply, ok = answerColdStart([]byte(`{"jsonrpc":"2.0","id":2,"method":"tools/list"}`), nil)
if !ok {
t.Fatal("tools/list must be answerable at cold start")
}
var listResp struct {
Result struct {
Tools []struct {
Name string `json:"name"`
} `json:"tools"`
} `json:"result"`
}
if err := json.Unmarshal(reply, &listResp); err != nil {
t.Fatalf("tools/list reply not JSON: %v", err)
}
if len(listResp.Result.Tools) != len(coldStartTools) {
t.Errorf("cold-start tools = %d, want %d", len(listResp.Result.Tools), len(coldStartTools))
}
names := map[string]bool{}
for _, tl := range listResp.Result.Tools {
names[tl.Name] = true
}
if !names["smart_context"] || !names["search_symbols"] {
t.Errorf("cold-start list must include the hot tools; got %v", names)
}
// tools/call must NOT be answered locally — it needs the daemon (or the
// embedded fallback).
if _, ok := answerColdStart([]byte(`{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"x"}}`), nil); ok {
t.Error("tools/call must not be answered at cold start")
}
}