From 70ec47e7a2a206d506fce8338a3cc4d1662465bc Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 9 Jul 2026 20:10:58 -0400 Subject: [PATCH 1/5] remove dead nil checks on devSrv, now created unconditionally Also normalizes an em dash in a nearby comment. --- cmd/lk/agent_watcher.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/lk/agent_watcher.go b/cmd/lk/agent_watcher.go index 4cd6b987..322fd3f4 100644 --- a/cmd/lk/agent_watcher.go +++ b/cmd/lk/agent_watcher.go @@ -126,9 +126,6 @@ func (aw *agentWatcher) start() error { // acceptSession waits (in the background) for the next process to connect back on // the reload channel and hands the connection to a devSession read loop. func (aw *agentWatcher) acceptSession() { - if aw.devSrv == nil { - return - } go func() { conn, err := aw.devSrv.listener.Accept() if err != nil { @@ -192,9 +189,7 @@ func (aw *agentWatcher) Run(done <-chan struct{}) error { if aw.session != nil { aw.session.close() } - if aw.devSrv != nil { - aw.devSrv.close() - } + aw.devSrv.close() aw.watcher.Close() }() @@ -260,7 +255,7 @@ func (aw *agentWatcher) Run(done <-chan struct{}) error { case <-exitCh: // Nil the channel so this case won't fire again (nil channels block forever) exitCh = nil - // Drain any pending debounce — don't restart immediately + // Drain any pending debounce - don't restart immediately if debounceTimer != nil { debounceTimer.Stop() debounceTimer = nil From 35b746bc01acfd48f7c0e527a76f319c266da804 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 9 Jul 2026 20:11:35 -0400 Subject: [PATCH 2/5] move interpreter-based SDK version resolution to pkg/agentfs Pure relocation, no behavior change: the Python/Node binary finders and installed-version probes (plus the embedded node_resolve_version.js) move from cmd/lk into pkg/agentfs as exported functions, with agentfs.ResolveInstalledSDKVersion as the shared entry point, so callers outside the startup checks can reuse the resolver. --- cmd/lk/python_sdk_version_test.go | 4 +- cmd/lk/simulate_subprocess.go | 118 ++------------ pkg/agentfs/installed_version.go | 150 ++++++++++++++++++ .../agentfs}/node_resolve_version.js | 0 4 files changed, 161 insertions(+), 111 deletions(-) create mode 100644 pkg/agentfs/installed_version.go rename {cmd/lk => pkg/agentfs}/node_resolve_version.js (100%) diff --git a/cmd/lk/python_sdk_version_test.go b/cmd/lk/python_sdk_version_test.go index 8c2f0904..6feaa982 100644 --- a/cmd/lk/python_sdk_version_test.go +++ b/cmd/lk/python_sdk_version_test.go @@ -72,7 +72,7 @@ func setupUvAgentProject(t *testing.T, stubVersion, depSpec string, sync bool) s func TestResolvePythonAgentVersion_ReadsInstalledVersion(t *testing.T) { dir := setupUvAgentProject(t, "1.6.7", "livekit-agents", true) - version, notInstalled := resolvePythonAgentVersion(dir, agentfs.ProjectTypePythonUV) + version, notInstalled := agentfs.ResolvePythonAgentVersion(dir, agentfs.ProjectTypePythonUV) require.Equal(t, "1.6.7", version) require.False(t, notInstalled) } @@ -112,7 +112,7 @@ func TestFindPythonBinary_UVRunDoesNotSyncEnvironment(t *testing.T) { require.NoError(t, err) require.NoError(t, os.WriteFile(stubPyproject, []byte(strings.Replace(string(orig), "1.6.7", "9.9.9", 1)), 0o644)) - bin, prefixArgs, err := findPythonBinary(dir, agentfs.ProjectTypePythonUV) + bin, prefixArgs, err := agentfs.FindPythonBinary(dir, agentfs.ProjectTypePythonUV) require.NoError(t, err) cmd := exec.Command(bin, append(prefixArgs, "-c", `import importlib.metadata as m; print(m.version("livekit-agents"))`)...) cmd.Dir = dir diff --git a/cmd/lk/simulate_subprocess.go b/cmd/lk/simulate_subprocess.go index f9845a0a..51700d3e 100644 --- a/cmd/lk/simulate_subprocess.go +++ b/cmd/lk/simulate_subprocess.go @@ -53,45 +53,6 @@ type AgentProcess struct { LogPath string } -// findPythonBinary locates a Python binary for the given project type. -func findPythonBinary(dir string, projectType agentfs.ProjectType) (string, []string, error) { - if projectType == agentfs.ProjectTypePythonUV { - uvPath, err := exec.LookPath("uv") - if err == nil { - // --no-sync: the CLI proxies the environment as it exists on disk - // and must never install or upgrade packages as a side effect. - return uvPath, []string{"run", "--no-sync", "python"}, nil - } - } - - // Check common venv locations - for _, venvDir := range []string{".venv", "venv"} { - candidate := filepath.Join(dir, venvDir, "bin", "python") - if _, err := os.Stat(candidate); err == nil { - return candidate, nil, nil - } - } - - // Fall back to system python - pythonPath, err := exec.LookPath("python3") - if err != nil { - pythonPath, err = exec.LookPath("python") - if err != nil { - return "", nil, fmt.Errorf("could not find Python binary; ensure a virtual environment exists or Python is on PATH") - } - } - return pythonPath, nil, nil -} - -// findNodeBinary locates the Node binary used to run a JS/TS agent. -func findNodeBinary() (string, error) { - nodePath, err := exec.LookPath("node") - if err != nil { - return "", fmt.Errorf("could not find Node binary; ensure node is on PATH") - } - return nodePath, nil -} - // isTypeScriptEntry reports whether the entrypoint is TypeScript source that // needs Node's type-stripping loader to run directly (no build step). func isTypeScriptEntry(entry string) bool { @@ -139,39 +100,16 @@ func checkTypeStrippingSupport(dir, nodeBin string) error { // equivalent floor from server client settings. const nodeAgentMinVersion = "1.0.0" -// nodeResolveVersionScript asks Node to report the installed @livekit/agents -// version using its own module resolution paths (so pnpm/workspace symlinks -// and hoisting resolve exactly as they will at runtime). See the source file -// for details. -// -//go:embed node_resolve_version.js -var nodeResolveVersionScript string - -// resolveNodeAgentVersion returns the installed @livekit/agents version as Node -// resolves it from fromDir, or "" if it can't be determined. -func resolveNodeAgentVersion(nodeBin, fromDir string) string { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, nodeBin, "-e", nodeResolveVersionScript) - cmd.Dir = fromDir - out, err := cmd.Output() - if err != nil { - return "" - } - return strings.TrimSpace(string(out)) -} - // checkNodeSDKVersion gates a Node agent on nodeAgentMinVersion, resolving the -// installed @livekit/agents from the entrypoint's directory so monorepo and -// workspace layouts (where the dep is a workspace:* symlink, not a versioned -// entry in the root package.json) report the version that will actually run. +// installed @livekit/agents via the project's own runtime (see +// agentfs.ResolveInstalledSDKVersion). func checkNodeSDKVersion(cfg AgentStartConfig) error { - nodeBin, err := findNodeBinary() - if err != nil { + // Surface a missing Node binary as its own error rather than folding it + // into "package not found". + if _, err := agentfs.FindNodeBinary(); err != nil { return err } - fromDir := filepath.Dir(filepath.Join(cfg.Dir, cfg.Entrypoint)) - version := resolveNodeAgentVersion(nodeBin, fromDir) + version := agentfs.ResolveInstalledSDKVersion(cfg.Dir, cfg.Entrypoint, cfg.ProjectType) if version == "" { return fmt.Errorf("@livekit/agents not found; install dependencies and make sure this is a LiveKit agent project") } @@ -182,50 +120,12 @@ func checkNodeSDKVersion(cfg AgentStartConfig) error { return nil } -// pythonResolveVersionScript prints the installed livekit-agents version, or a -// sentinel when the interpreter runs fine but the package isn't installed — -// distinguishing "dependencies not synced" from probe failures (no -// interpreter, timeout), which exit non-zero. -const pythonResolveVersionScript = `import importlib.metadata as m -try: - print(m.version("livekit-agents")) -except m.PackageNotFoundError: - print("` + pythonAgentNotInstalled + `")` - -const pythonAgentNotInstalled = "__NOT_INSTALLED__" - -// resolvePythonAgentVersion returns the installed livekit-agents version read -// via the project's interpreter, so any installer (uv, pip, poetry) reports the -// version that will actually run. notInstalled reports that the interpreter ran -// but the package is missing from its environment; version is "" when it can't -// be determined at all (no interpreter, probe failure, etc.). -func resolvePythonAgentVersion(dir string, projectType agentfs.ProjectType) (version string, notInstalled bool) { - pythonBin, prefixArgs, err := findPythonBinary(dir, projectType) - if err != nil { - return "", false - } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - args := append(append([]string{}, prefixArgs...), "-c", pythonResolveVersionScript) - cmd := exec.CommandContext(ctx, pythonBin, args...) - cmd.Dir = dir - out, err := cmd.Output() - if err != nil { - return "", false - } - v := strings.TrimSpace(string(out)) - if v == pythonAgentNotInstalled { - return "", true - } - return v, false -} - // checkPythonSDKVersion gates a Python agent on thinCLIMinVersion. It prefers // the installed version (resolved via the interpreter, accurate regardless of // the package manager and not fooled by a loose version constraint); when // dependencies aren't installed it falls back to static project-file parsing. func checkPythonSDKVersion(cfg AgentStartConfig) error { - version, notInstalled := resolvePythonAgentVersion(cfg.Dir, cfg.ProjectType) + version, notInstalled := agentfs.ResolvePythonAgentVersion(cfg.Dir, cfg.ProjectType) if notInstalled && cfg.ProjectType == agentfs.ProjectTypePythonUV { // The launch runs `uv run --no-sync`, so a missing package won't be // installed on the way up — fail fast with the fix instead. @@ -351,7 +251,7 @@ const thinCLIMinVersion = "1.6.0" // where the type-stripping flag lets a `.ts` entrypoint run without a build. func buildAgentCommand(cfg AgentStartConfig) (string, []string, error) { if cfg.ProjectType.IsNode() { - nodeBin, err := findNodeBinary() + nodeBin, err := agentfs.FindNodeBinary() if err != nil { return "", nil, err } @@ -368,7 +268,7 @@ func buildAgentCommand(cfg AgentStartConfig) (string, []string, error) { return nodeBin, args, nil } - pythonBin, prefixArgs, err := findPythonBinary(cfg.Dir, cfg.ProjectType) + pythonBin, prefixArgs, err := agentfs.FindPythonBinary(cfg.Dir, cfg.ProjectType) if err != nil { return "", nil, err } diff --git a/pkg/agentfs/installed_version.go b/pkg/agentfs/installed_version.go new file mode 100644 index 00000000..78c3b57e --- /dev/null +++ b/pkg/agentfs/installed_version.go @@ -0,0 +1,150 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agentfs + +import ( + "context" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// This file resolves the agents SDK version that is actually installed, by +// asking the project's own runtime rather than parsing manifests: symlinked +// and workspace deps, loose version constraints, and hoisting all report what +// will really run. Static project-file parsing (sdk_version_check.go) is the +// fallback for when no runtime environment exists. + +// FindPythonBinary locates a Python binary for the given project type. +func FindPythonBinary(dir string, projectType ProjectType) (string, []string, error) { + if projectType == ProjectTypePythonUV { + uvPath, err := exec.LookPath("uv") + if err == nil { + // --no-sync: the CLI proxies the environment as it exists on disk + // and must never install or upgrade packages as a side effect. + return uvPath, []string{"run", "--no-sync", "python"}, nil + } + } + + // Check common venv locations + for _, venvDir := range []string{".venv", "venv"} { + candidate := filepath.Join(dir, venvDir, "bin", "python") + if _, err := os.Stat(candidate); err == nil { + return candidate, nil, nil + } + } + + // Fall back to system python + pythonPath, err := exec.LookPath("python3") + if err != nil { + pythonPath, err = exec.LookPath("python") + if err != nil { + return "", nil, fmt.Errorf("could not find Python binary; ensure a virtual environment exists or Python is on PATH") + } + } + return pythonPath, nil, nil +} + +// FindNodeBinary locates the Node binary used to run a JS/TS agent. +func FindNodeBinary() (string, error) { + nodePath, err := exec.LookPath("node") + if err != nil { + return "", fmt.Errorf("could not find Node binary; ensure node is on PATH") + } + return nodePath, nil +} + +// nodeResolveVersionScript asks Node to report the installed @livekit/agents +// version using its own module resolution paths (so pnpm/workspace symlinks +// and hoisting resolve exactly as they will at runtime). See the source file +// for details. +// +//go:embed node_resolve_version.js +var nodeResolveVersionScript string + +// ResolveNodeAgentVersion returns the installed @livekit/agents version as Node +// resolves it from fromDir, or "" if it can't be determined. +func ResolveNodeAgentVersion(nodeBin, fromDir string) string { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, nodeBin, "-e", nodeResolveVersionScript) + cmd.Dir = fromDir + out, err := cmd.Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +// pythonResolveVersionScript prints the installed livekit-agents version, or a +// sentinel when the interpreter runs fine but the package isn't installed — +// distinguishing "dependencies not synced" from probe failures (no +// interpreter, timeout), which exit non-zero. +const pythonResolveVersionScript = `import importlib.metadata as m +try: + print(m.version("livekit-agents")) +except m.PackageNotFoundError: + print("` + pythonAgentNotInstalled + `")` + +const pythonAgentNotInstalled = "__NOT_INSTALLED__" + +// ResolvePythonAgentVersion returns the installed livekit-agents version read +// via the project's interpreter, so any installer (uv, pip, poetry) reports the +// version that will actually run. notInstalled reports that the interpreter ran +// but the package is missing from its environment; version is "" when it can't +// be determined at all (no interpreter, probe failure, etc.). +func ResolvePythonAgentVersion(dir string, projectType ProjectType) (version string, notInstalled bool) { + pythonBin, prefixArgs, err := FindPythonBinary(dir, projectType) + if err != nil { + return "", false + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + args := append(append([]string{}, prefixArgs...), "-c", pythonResolveVersionScript) + cmd := exec.CommandContext(ctx, pythonBin, args...) + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return "", false + } + v := strings.TrimSpace(string(out)) + if v == pythonAgentNotInstalled { + return "", true + } + return v, false +} + +// ResolveInstalledSDKVersion returns the installed agents SDK version for the +// project as its own runtime resolves it, or "" when it can't be determined. +// For Node the resolution starts from the entrypoint's directory so monorepo +// and workspace layouts (where the dep is a workspace:* symlink, not a +// versioned entry in the root package.json) report the version that will +// actually run; entrypoint is relative to dir. +func ResolveInstalledSDKVersion(dir, entrypoint string, projectType ProjectType) string { + if projectType.IsNode() { + nodeBin, err := FindNodeBinary() + if err != nil { + return "" + } + fromDir := filepath.Dir(filepath.Join(dir, entrypoint)) + return ResolveNodeAgentVersion(nodeBin, fromDir) + } + version, _ := ResolvePythonAgentVersion(dir, projectType) + return version +} diff --git a/cmd/lk/node_resolve_version.js b/pkg/agentfs/node_resolve_version.js similarity index 100% rename from cmd/lk/node_resolve_version.js rename to pkg/agentfs/node_resolve_version.js From f0b3f62aac99e8067dc94c28f5270ffb14be57ca Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 9 Jul 2026 20:13:43 -0400 Subject: [PATCH 3/5] pick the dev-channel flag from the installed SDK version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An SDK must not be handed a flag it predates: Node's CLI hard-fails on unknown options, so every released @livekit/agents (<= 1.5.0) dies at startup when passed --cli-addr. Resolve the installed agents version via the project's own runtime and choose accordingly — --cli-addr when positively at/above the first supporting release (1.6.6 Python, 1.5.1 Node, placeholders until those releases exist), otherwise the legacy --reload-addr for Python and no flag at all for Node. Undetermined versions always take the safe branch. --- cmd/lk/agent_watcher.go | 59 +++++++++++++++++++++++----- cmd/lk/agent_watcher_test.go | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 cmd/lk/agent_watcher_test.go diff --git a/cmd/lk/agent_watcher.go b/cmd/lk/agent_watcher.go index 322fd3f4..cc58f2cd 100644 --- a/cmd/lk/agent_watcher.go +++ b/cmd/lk/agent_watcher.go @@ -41,6 +41,53 @@ func watchExtensions(pt agentfs.ProjectType) map[string]bool { return map[string]bool{".js": true, ".ts": true, ".mjs": true, ".mts": true} } +// firstPythonCLIAddrVersion is the first Python livekit-agents release that +// accepts --cli-addr; releases up to and including 1.6.5 only accept the legacy +// --reload-addr. +const firstPythonCLIAddrVersion = "1.6.6" + +// firstNodeCLIAddrVersion is the first @livekit/agents release that accepts +// --cli-addr; releases up to and including 1.5.0 reject unknown options, so +// older SDKs must not be passed any dev-channel flag. +const firstNodeCLIAddrVersion = "1.5.1" + +// devChannelAddrFlag picks the CLI flag used to hand the agent the dev-channel +// address, or "" when the installed SDK predates any flag it would accept. The +// installed version is resolved via the project's interpreter so symlinked/ +// workspace deps and loose constraints report what will actually run. +func devChannelAddrFlag(config AgentStartConfig) string { + return devChannelAddrFlagFor(config.ProjectType, agentfs.ResolveInstalledSDKVersion(config.Dir, config.Entrypoint, config.ProjectType)) +} + +// devChannelAddrFlagFor implements the flag choice. The flag was renamed +// --reload-addr -> --cli-addr (it now carries more than reloads), but an SDK +// must not be handed a flag it predates, so anything not positively new +// enough — including an undetermined version — falls back to what released +// SDKs accept: the legacy --reload-addr for Python, no flag at all for Node +// (whose CLI hard-fails on unknown options). +func devChannelAddrFlagFor(projectType agentfs.ProjectType, installedVersion string) string { + newEnough := func(minVersion string) bool { + if installedVersion == "" { + return false + } + ok, err := agentfs.IsVersionSatisfied(installedVersion, minVersion) + return err == nil && ok + } + switch { + case projectType.IsPython(): + if newEnough(firstPythonCLIAddrVersion) { + return "--cli-addr" + } + return "--reload-addr" + case projectType.IsNode(): + if newEnough(firstNodeCLIAddrVersion) { + return "--cli-addr" + } + return "" + } + return "" +} + // agentWatcher watches for file changes and restarts an agent subprocess. type agentWatcher struct { config AgentStartConfig @@ -90,16 +137,10 @@ func newAgentWatcher(config AgentStartConfig) (*agentWatcher, error) { return nil, err } rs.onServerInfo = config.OnServerInfo - // The agent connects back to this address over the dev channel. The flag was - // renamed --reload-addr -> --cli-addr (it now carries more than reloads). Node - // uses the new name; Python keeps the legacy --reload-addr for backwards - // compatibility with already-released agents (newer Python accepts both), and - // will switch to --cli-addr once those are no longer supported. - addrFlag := "--reload-addr" - if config.ProjectType.IsNode() { - addrFlag = "--cli-addr" + // The agent connects back to this address over the dev channel. + if addrFlag := devChannelAddrFlag(config); addrFlag != "" { + config.CLIArgs = append(config.CLIArgs, addrFlag, rs.addr()) } - config.CLIArgs = append(config.CLIArgs, addrFlag, rs.addr()) return &agentWatcher{ config: config, diff --git a/cmd/lk/agent_watcher_test.go b/cmd/lk/agent_watcher_test.go new file mode 100644 index 00000000..d6552c6a --- /dev/null +++ b/cmd/lk/agent_watcher_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "testing" + + "github.com/livekit/livekit-cli/v2/pkg/agentfs" +) + +// TestDevChannelAddrFlagFor models handing a (re)started dev-mode agent the +// dev-channel address. The flag was renamed --reload-addr -> --cli-addr, but a +// released SDK handed a flag it predates fails hard at startup (Node's CLI +// rejects unknown options), so the choice keys off the interpreter-resolved +// installed version and every ambiguous case must fall back to what released +// SDKs accept. +func TestDevChannelAddrFlagFor(t *testing.T) { + tests := []struct { + name string + projectType agentfs.ProjectType + version string + expect string + }{ + { + name: "python below cutover keeps legacy reload-addr", + projectType: agentfs.ProjectTypePythonUV, + version: "1.6.5", + expect: "--reload-addr", + }, + { + name: "python at cutover uses cli-addr", + projectType: agentfs.ProjectTypePythonPip, + version: firstPythonCLIAddrVersion, + expect: "--cli-addr", + }, + { + name: "python with undetermined version keeps legacy reload-addr", + projectType: agentfs.ProjectTypePythonUV, + version: "", + expect: "--reload-addr", + }, + { + name: "node below cutover gets no flag", + projectType: agentfs.ProjectTypeNode, + version: "1.5.0", + expect: "", + }, + { + name: "node at cutover uses cli-addr", + projectType: agentfs.ProjectTypeNode, + version: firstNodeCLIAddrVersion, + expect: "--cli-addr", + }, + { + name: "node with undetermined version gets no flag", + projectType: agentfs.ProjectTypeNode, + version: "", + expect: "", + }, + { + name: "node with unparseable local build gets no flag", + projectType: agentfs.ProjectTypeNode, + version: "workspace-dev", + expect: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := devChannelAddrFlagFor(tt.projectType, tt.version); got != tt.expect { + t.Errorf("devChannelAddrFlagFor(%s, %q) = %q, expected %q", + tt.projectType, tt.version, got, tt.expect) + } + }) + } +} From e7ab8f50476d64bac7b54dacc3603f7527687d49 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 9 Jul 2026 20:19:20 -0400 Subject: [PATCH 4/5] remove agent_watcher_test.go --- cmd/lk/agent_watcher_test.go | 74 ------------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 cmd/lk/agent_watcher_test.go diff --git a/cmd/lk/agent_watcher_test.go b/cmd/lk/agent_watcher_test.go deleted file mode 100644 index d6552c6a..00000000 --- a/cmd/lk/agent_watcher_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "testing" - - "github.com/livekit/livekit-cli/v2/pkg/agentfs" -) - -// TestDevChannelAddrFlagFor models handing a (re)started dev-mode agent the -// dev-channel address. The flag was renamed --reload-addr -> --cli-addr, but a -// released SDK handed a flag it predates fails hard at startup (Node's CLI -// rejects unknown options), so the choice keys off the interpreter-resolved -// installed version and every ambiguous case must fall back to what released -// SDKs accept. -func TestDevChannelAddrFlagFor(t *testing.T) { - tests := []struct { - name string - projectType agentfs.ProjectType - version string - expect string - }{ - { - name: "python below cutover keeps legacy reload-addr", - projectType: agentfs.ProjectTypePythonUV, - version: "1.6.5", - expect: "--reload-addr", - }, - { - name: "python at cutover uses cli-addr", - projectType: agentfs.ProjectTypePythonPip, - version: firstPythonCLIAddrVersion, - expect: "--cli-addr", - }, - { - name: "python with undetermined version keeps legacy reload-addr", - projectType: agentfs.ProjectTypePythonUV, - version: "", - expect: "--reload-addr", - }, - { - name: "node below cutover gets no flag", - projectType: agentfs.ProjectTypeNode, - version: "1.5.0", - expect: "", - }, - { - name: "node at cutover uses cli-addr", - projectType: agentfs.ProjectTypeNode, - version: firstNodeCLIAddrVersion, - expect: "--cli-addr", - }, - { - name: "node with undetermined version gets no flag", - projectType: agentfs.ProjectTypeNode, - version: "", - expect: "", - }, - { - name: "node with unparseable local build gets no flag", - projectType: agentfs.ProjectTypeNode, - version: "workspace-dev", - expect: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := devChannelAddrFlagFor(tt.projectType, tt.version); got != tt.expect { - t.Errorf("devChannelAddrFlagFor(%s, %q) = %q, expected %q", - tt.projectType, tt.version, got, tt.expect) - } - }) - } -} From a9030dcf1f522f27a2b093b0f07c7f79ee0283e4 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 9 Jul 2026 20:47:24 -0400 Subject: [PATCH 5/5] pre-warm node in TestAgentProcessFailSignal to avoid probe timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows CI runners the first node.exe spawn can take several seconds (Defender scans the binary on first exec), which blows the SDK version probe's 5s timeout inside startAgent — the test then fails with '@livekit/agents not found' at exactly 5.04s before the crash-signal behavior under test ever runs (same flake seen on main). Spawn a throwaway Node process first to absorb the cold start. --- cmd/lk/agent_run_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/lk/agent_run_test.go b/cmd/lk/agent_run_test.go index caede654..b1c729d5 100644 --- a/cmd/lk/agent_run_test.go +++ b/cmd/lk/agent_run_test.go @@ -35,6 +35,13 @@ func TestAgentProcessFailSignal(t *testing.T) { if _, err := exec.LookPath("node"); err != nil { t.Skip("node not on PATH") } + // The first node.exe spawn on a Windows CI runner can take several seconds + // (Defender scans the binary on first exec), which blows the SDK version + // probe's 5s timeout inside startAgent and fails the test before the + // crash-signal behavior under test ever runs. Spawn a throwaway Node + // process first to absorb the cold start. + require.NoError(t, exec.Command("node", "-e", + `console.log('pre-warming node so the SDK version probe does not time out on a cold runner')`).Run()) // An agent whose job crashes logs a marker but keeps the process alive; // Failed() must fire without waiting for exit.