From c4adadde949c785fd0b2fb460f4f0c35fd435621 Mon Sep 17 00:00:00 2001 From: Som Tripathi Date: Sat, 19 Sep 2026 04:34:19 -0500 Subject: [PATCH 1/2] test(services): isolate test app-data state Signed-off-by: Som Tripathi --- services/lmstudio-proxy/e2e_test.go | 57 +++++++++++- services/nvpair-ui-broker/main_test.go | 103 +++++++++++++++++++++ services/tests/main_test.go | 120 ++++++++++++++++++++++--- 3 files changed, 266 insertions(+), 14 deletions(-) create mode 100644 services/nvpair-ui-broker/main_test.go diff --git a/services/lmstudio-proxy/e2e_test.go b/services/lmstudio-proxy/e2e_test.go index 5412ab7c..c867a642 100644 --- a/services/lmstudio-proxy/e2e_test.go +++ b/services/lmstudio-proxy/e2e_test.go @@ -23,11 +23,52 @@ import ( // proxyBin is the real lmstudio-proxy binary, built once in TestMain so the // e2e test exercises the shipped artifact (not just in-process handlers). -var proxyBin string +var ( + proxyBin string + e2eConfigBase string +) + +func isolateE2EConfigBase() (string, func()) { + base, err := os.MkdirTemp("", "nvpair-lmproxy-config-*") + if err != nil { + panic(err) + } + keys := []string{"XDG_CONFIG_HOME", "HOME", "APPDATA", "LOCALAPPDATA"} + type savedEnv struct { + value string + set bool + } + previous := make(map[string]savedEnv, len(keys)) + for _, key := range keys { + value, set := os.LookupEnv(key) + previous[key] = savedEnv{value: value, set: set} + } + cleanup := func() { + for _, key := range keys { + prior := previous[key] + if prior.set { + _ = os.Setenv(key, prior.value) + } else { + _ = os.Unsetenv(key) + } + } + _ = os.RemoveAll(base) + } + for _, key := range keys { + if err := os.Setenv(key, base); err != nil { + cleanup() + panic(err) + } + } + return base, cleanup +} func TestMain(m *testing.M) { + var cleanupConfig func() + e2eConfigBase, cleanupConfig = isolateE2EConfigBase() tmp, err := os.MkdirTemp("", "nvpair-lmproxy-e2e-*") if err != nil { + cleanupConfig() panic(err) } suffix := "" @@ -36,9 +77,12 @@ func TestMain(m *testing.M) { } proxyBin = filepath.Join(tmp, "lmstudio-proxy"+suffix) if out, err := exec.Command("go", "build", "-o", proxyBin, ".").CombinedOutput(); err != nil { + cleanupConfig() + _ = os.RemoveAll(tmp) panic("build lmstudio-proxy: " + err.Error() + "\n" + string(out)) } code := m.Run() + cleanupConfig() _ = os.RemoveAll(tmp) os.Exit(code) } @@ -215,3 +259,14 @@ func TestE2EFailoverOverRealBinary(t *testing.T) { e2eSend(t, stdin, 9, "shutdown", nil) e2eWaitResult(t, frames, "9", 5*time.Second) } + +func TestE2EConfigBaseIsPrivate(t *testing.T) { + path, err := proxyPortPath() + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(e2eConfigBase, path) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + t.Fatalf("proxy port path %q is outside test config base %q", path, e2eConfigBase) + } +} diff --git a/services/nvpair-ui-broker/main_test.go b/services/nvpair-ui-broker/main_test.go new file mode 100644 index 00000000..19fc5d62 --- /dev/null +++ b/services/nvpair-ui-broker/main_test.go @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "nvpair-shared/appdir" + "nvpair-ui-broker/workloadstore" +) + +var uiBrokerTestConfigBase string + +func TestMain(m *testing.M) { + base, err := os.MkdirTemp("", "nvpair-ui-broker-config-*") + if err != nil { + panic(err) + } + uiBrokerTestConfigBase = base + keys := []string{"XDG_CONFIG_HOME", "HOME", "APPDATA", "LOCALAPPDATA"} + type savedEnv struct { + value string + set bool + } + previous := make(map[string]savedEnv, len(keys)) + for _, key := range keys { + value, set := os.LookupEnv(key) + previous[key] = savedEnv{value: value, set: set} + } + cleanup := func() { + for _, key := range keys { + prior := previous[key] + if prior.set { + _ = os.Setenv(key, prior.value) + } else { + _ = os.Unsetenv(key) + } + } + _ = os.RemoveAll(base) + } + for _, key := range keys { + if err := os.Setenv(key, base); err != nil { + cleanup() + panic(err) + } + } + + code := m.Run() + cleanup() + os.Exit(code) +} + +func TestWorkloadHistoryPersistsUnderPrivateBase(t *testing.T) { + path, err := appdir.Path("workloads-history.json") + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + b := &Broker{workloads: workloadstore.New()} + b.workloads.WithPersistence(path) + if err := b.workloads.Load(); err != nil { + t.Fatal(err) + } + now := time.Now().UnixMilli() + info, err := json.Marshal(map[string]any{ + "id": "test-history", + "originatedFrom": "test-node", + "state": "completed", + "scheduledOn": "test-node", + "createdAt": now - 1, + "completedAt": now, + "model": "test-model", + "engine": "ollama", + }) + if err != nil { + t.Fatal(err) + } + params, err := json.Marshal(map[string]json.RawMessage{"workloadInfo": info}) + if err != nil { + t.Fatal(err) + } + if !b.applyWorkloadEvent("workloads:upsert", params, false, time.Time{}) { + t.Fatal("terminal workload event was not applied") + } + if err := b.workloads.Flush(); err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(uiBrokerTestConfigBase, path) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + t.Fatalf("workload history path %q is outside test config base %q", path, uiBrokerTestConfigBase) + } + if _, err := os.Stat(path); err != nil { + t.Fatalf("persisted workload history: %v", err) + } +} diff --git a/services/tests/main_test.go b/services/tests/main_test.go index c5d63c9c..48e6912e 100644 --- a/services/tests/main_test.go +++ b/services/tests/main_test.go @@ -6,15 +6,18 @@ package tests import ( "bufio" "encoding/json" + "fmt" "io" "log" "os" "os/exec" "path/filepath" "runtime" + "strings" "testing" "time" + "nvpair-shared/appdir" "nvpair-shared/jsonrpc" ) @@ -31,13 +34,51 @@ var ( manualNodesBin string clusterMgrBin string schedulerBin string + testsConfigBase string ) func TestMain(m *testing.M) { + configBase, err := os.MkdirTemp("", "nvpair-services-tests-config-*") + if err != nil { + log.Fatalf("failed to create test config directory: %v", err) + } + testsConfigBase = configBase + keys := []string{"XDG_CONFIG_HOME", "HOME", "APPDATA", "LOCALAPPDATA"} + type savedEnv struct { + value string + set bool + } + previous := make(map[string]savedEnv, len(keys)) + for _, key := range keys { + value, set := os.LookupEnv(key) + previous[key] = savedEnv{value: value, set: set} + } + cleanupConfig := func() { + for _, key := range keys { + prior := previous[key] + if prior.set { + _ = os.Setenv(key, prior.value) + } else { + _ = os.Unsetenv(key) + } + } + _ = os.RemoveAll(configBase) + } + for _, key := range keys { + if err := os.Setenv(key, configBase); err != nil { + cleanupConfig() + log.Fatalf("set %s: %v", key, err) + } + } tmpDir, err := os.MkdirTemp("", "nvpair-tests-*") if err != nil { + cleanupConfig() log.Fatalf("failed to create temp dir: %v", err) } + cleanup := func() { + cleanupConfig() + _ = os.RemoveAll(tmpDir) + } ext := "" if runtime.GOOS == "windows" { @@ -59,7 +100,7 @@ func TestMain(m *testing.M) { log.Println("building ollama-proxy...") if err := goBuild(filepath.Join("..", "ollama-proxy"), proxyBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build ollama-proxy: %v", err) } @@ -67,19 +108,19 @@ func TestMain(m *testing.M) { // needs its binary (pointed at via --lmstudio-proxy-path). log.Println("building lmstudio-proxy...") if err := goBuild(filepath.Join("..", "lmstudio-proxy"), lmstudioProxyBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build lmstudio-proxy: %v", err) } log.Println("building nvpair-errors...") if err := goBuild(filepath.Join("..", "nvpair-errors"), errorsBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-errors: %v", err) } log.Println("building nvpair-node-info...") if err := goBuild(filepath.Join("..", "nvpair-node-info"), nodeInfoBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-node-info: %v", err) } @@ -88,19 +129,19 @@ func TestMain(m *testing.M) { // --scanner-path so it doesn't depend on a sibling-file layout. log.Println("building nvpair-node-scanner...") if err := goBuild(filepath.Join("..", "nvpair-node-scanner"), scannerBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-node-scanner: %v", err) } log.Println("building nvpair-ui-broker...") if err := goBuild(filepath.Join("..", "nvpair-ui-broker"), brokerBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-ui-broker: %v", err) } log.Println("building nvpair-node-settings...") if err := goBuild(filepath.Join("..", "nvpair-node-settings"), nodeSettingsBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-node-settings: %v", err) } @@ -109,7 +150,7 @@ func TestMain(m *testing.M) { // via --workload-manager-path. log.Println("building nvpair-workload-manager...") if err := goBuild(filepath.Join("..", "nvpair-workload-manager"), workloadMgrBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-workload-manager: %v", err) } @@ -119,19 +160,19 @@ func TestMain(m *testing.M) { // --cluster-manager-path). log.Println("building nvpair-engine-manager...") if err := goBuild(filepath.Join("..", "nvpair-engine-manager"), engineMgrBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-engine-manager: %v", err) } log.Println("building nvpair-manual-nodes...") if err := goBuild(filepath.Join("..", "nvpair-manual-nodes"), manualNodesBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-manual-nodes: %v", err) } log.Println("building nvpair-cluster-manager...") if err := goBuild(filepath.Join("..", "nvpair-cluster-manager"), clusterMgrBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-cluster-manager: %v", err) } @@ -139,12 +180,12 @@ func TestMain(m *testing.M) { // the scheduler-interop test also drives its binary directly. log.Println("building nvpair-job-scheduler...") if err := goBuild(filepath.Join("..", "nvpair-job-scheduler"), schedulerBin); err != nil { - os.RemoveAll(tmpDir) + cleanup() log.Fatalf("build nvpair-job-scheduler: %v", err) } code := m.Run() - os.RemoveAll(tmpDir) + cleanup() os.Exit(code) } @@ -217,3 +258,56 @@ func waitForResponse(t *testing.T, ch <-chan jsonrpc.Message, timeout time.Durat } return jsonrpc.Message{} } + +func TestLMStudioProxyChildPersistsUnderPrivateBase(t *testing.T) { + startPort := freePort(t) + cmd := exec.Command(lmstudioProxyBin, "--port", fmt.Sprint(startPort)) + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer func() { + _ = stdin.Close() + if cmd.ProcessState == nil { + _ = cmd.Process.Kill() + } + _ = cmd.Wait() + }() + + msgs := startMsgReader(stdout) + waitForMethod(t, msgs, "ready", 10*time.Second) + persistedPort := freePort(t) + if _, err := fmt.Fprintf(stdin, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"set-port\",\"params\":{\"port\":%d}}\n", persistedPort); err != nil { + t.Fatal(err) + } + waitForResponse(t, msgs, 5*time.Second) + + path, err := appdir.Path("lmstudio-proxy-port.json") + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(testsConfigBase, path) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + t.Fatalf("lmstudio proxy port path %q is outside test config base %q", path, testsConfigBase) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read persisted lmstudio proxy port: %v", err) + } + var saved struct { + Port int `json:"port"` + } + if err := json.Unmarshal(data, &saved); err != nil { + t.Fatalf("parse persisted lmstudio proxy port: %v", err) + } + if saved.Port != persistedPort { + t.Fatalf("persisted lmstudio proxy port = %d, want %d", saved.Port, persistedPort) + } +} From 24a6fca521ac9807b9fdf4edd0e081e4890fb037 Mon Sep 17 00:00:00 2001 From: Som Tripathi Date: Wed, 23 Sep 2026 15:29:55 -0500 Subject: [PATCH 2/2] test(services): port app-data isolation to nvpair-proxy Signed-off-by: Som Tripathi --- services/nvpair-proxy/e2e_test.go | 5 ++ services/tests/main_test.go | 88 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/services/nvpair-proxy/e2e_test.go b/services/nvpair-proxy/e2e_test.go index 3251e133..d3ea5186 100644 --- a/services/nvpair-proxy/e2e_test.go +++ b/services/nvpair-proxy/e2e_test.go @@ -279,6 +279,11 @@ func e2eSplitHostPort(t *testing.T, serverURL string) (string, int) { func TestE2EFailoverOverRealBinary(t *testing.T) { forEachEngine(t, func(t *testing.T, tc engineCase) { bin := proxyBinary(t) + // The child inherits this environment, so a persisted port it loads on + // facade/enable comes from a temp dir, never the developer's config. + // It runs after the build, because go derives its module and build + // caches from these variables. + redirectConfigDir(t) var gotBody string busy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/services/tests/main_test.go b/services/tests/main_test.go index a30b1198..81f79cea 100644 --- a/services/tests/main_test.go +++ b/services/tests/main_test.go @@ -12,9 +12,12 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "testing" "time" + "nvpair-shared/appdir" + "nvpair-shared/engines" "nvpair-shared/jsonrpc" ) @@ -30,6 +33,7 @@ var ( manualNodesBin string clusterMgrBin string schedulerBin string + testsConfigBase string ) func TestMain(m *testing.M) { @@ -135,6 +139,19 @@ func TestMain(m *testing.M) { log.Fatalf("build nvpair-job-scheduler: %v", err) } + // Every child binary inherits this environment, so its persisted state + // (ports, workloads, settings) lands under tmpDir instead of the + // developer's real config. These are the variables appdir resolves + // through on each platform. They are set after the builds, because go + // derives its module and build caches from these variables. + testsConfigBase = filepath.Join(tmpDir, "config") + for _, key := range []string{"XDG_CONFIG_HOME", "HOME", "APPDATA", "LOCALAPPDATA"} { + if err := os.Setenv(key, testsConfigBase); err != nil { + os.RemoveAll(tmpDir) + log.Fatalf("set %s: %v", key, err) + } + } + code := m.Run() os.RemoveAll(tmpDir) os.Exit(code) @@ -209,3 +226,74 @@ func waitForResponse(t *testing.T, ch <-chan jsonrpc.Message, timeout time.Durat } return jsonrpc.Message{} } + +// TestLMStudioFacadeChildPersistsUnderPrivateBase proves a real proxy child +// writes its persisted LM Studio port under the TestMain config base, so no +// cross-process test can clobber the developer's saved port. +func TestLMStudioFacadeChildPersistsUnderPrivateBase(t *testing.T) { + cmd := exec.Command(proxyBin) + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer func() { + _ = stdin.Close() + _ = cmd.Process.Kill() + _ = cmd.Wait() + }() + msgs := startMsgReader(stdout) + + sendLine(t, stdin, map[string]any{ + "jsonrpc": "2.0", + "id": 1, + "method": "facade/enable", + "params": map[string]any{ + "engine": "lmstudio", + "port": freePort(t), + "ignorePersistedPort": true, + }, + }) + if resp := waitForResponse(t, msgs, 10*time.Second); resp.Error != nil { + t.Fatalf("facade/enable failed: %v", resp.Error) + } + + persistedPort := freePort(t) + sendLine(t, stdin, map[string]any{ + "jsonrpc": "2.0", + "id": 2, + "method": engines.AddressMethod("lmstudio", "set-port"), + "params": map[string]any{"port": persistedPort}, + }) + if resp := waitForResponse(t, msgs, 5*time.Second); resp.Error != nil { + t.Fatalf("lmstudio set-port failed: %v", resp.Error) + } + + path, err := appdir.Path("lmstudio-proxy-port.json") + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(testsConfigBase, path) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + t.Fatalf("lmstudio port path %q is outside test config base %q", path, testsConfigBase) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read persisted lmstudio port: %v", err) + } + var saved struct { + Port int `json:"port"` + } + if err := json.Unmarshal(data, &saved); err != nil { + t.Fatalf("parse persisted lmstudio port: %v", err) + } + if saved.Port != persistedPort { + t.Fatalf("persisted lmstudio port = %d, want %d", saved.Port, persistedPort) + } +}