Skip to content

Commit e6e30a5

Browse files
authored
fix(agent): trust the resolved livekit-agents version in the SDK check; add Windows test runner (#894)
* fix(agentfs): parse resolved livekit-agents version from uv.lock checkUvLock matched a `livekit-agents = "x"` line that never appears in uv.lock. uv.lock uses poetry.lock's TOML layout ([[package]] / name / version), so the resolved version was never found, and the SDK version check fell back to the pyproject.toml constraint floor — reading `>=1.0` as 1.0 and rejecting it as older than the minimum (e.g. 1.6.0) even when the lock resolved a newer version. Use the same [[package]] block regex as checkPoetryLock so the resolved version is read and preferred, matching how the Node lockfile parsers already read the installed version. Also replaces the test's fake uv.lock fixture with the real format and adds a regression test for the pyproject-floor-vs-resolved-version case. * feat(agent): resolve installed livekit-agents version for Python run check lk agent start/dev/console gated the Python SDK version by parsing project files, which can't know the resolved version from a loose constraint (and misread e.g. >=1.0 as 1.0). Resolve the installed version via the project interpreter (importlib.metadata) like the Node path already does, so the check reflects what will actually run regardless of installer. Falls back to static file parsing when dependencies aren't installed (e.g. before a sync). uv reads the existing env with --no-sync so the pre-flight check never syncs/downloads. * ci: share one Windows cross-build between unit tests and session e2e Combine the two Windows cross-builds into a single zig cross-build job in windows.yaml that produces lk.exe plus a test binary per package. Both the unit-test run (windows-tests) and the session e2e run (windows-session-e2e) depend on it, so the expensive cgo compile happens once. Move the Windows jobs out of session-e2e.yaml (now Linux/macOS only) and fold the standalone test-windows.yaml in. * fix(agent): never sync the uv environment as a launch side effect The CLI proxies the project's local environment; `uv run` implicitly re-locking and installing packages when launching an agent is a surprising side effect (and can change dependency versions mid-debug). Move --no-sync into findPythonBinary so both the version pre-flight and the launch run against the environment as it exists on disk. Since the launch no longer installs missing dependencies, the version probe now distinguishes "interpreter ran but livekit-agents isn't installed" and the pre-flight fails fast with a `uv sync` hint instead of letting the agent die with ModuleNotFoundError. * docs(e2e): fix stale auto-sync note on the echo-agent fixture The launch path now uses `uv run --no-sync`; deps come from the explicit `uv sync` step in CI. * fix(console): clear the spinner line before stopSpinner returns Stopping only signaled the spinner goroutine, which could sleep up to 80ms before clearing the line — so an error printed right after landed on the leftover "Starting agent" text. Block the stop function until the clear escape has been written.
1 parent 9e24a15 commit e6e30a5

9 files changed

Lines changed: 509 additions & 165 deletions

File tree

.github/workflows/session-e2e.yaml

Lines changed: 5 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
name: Session E2E
22

33
# Drives the real `lk agent daemon` start/say/stop lifecycle against the minimal
4-
# one-file echo agents in cmd/lk/testdata, on Linux, macOS, and Windows. This
5-
# exercises the detached daemon, the readiness handshake, the console IPC
6-
# transport, and the model round-trip end to end -- runtime behavior that
7-
# `go test` alone never covers.
4+
# one-file echo agents in cmd/lk/testdata, on Linux and macOS (the Windows arm
5+
# lives in windows.yaml, which cross-compiles with zig). This exercises the
6+
# detached daemon, the readiness handshake, the console IPC transport, and the
7+
# model round-trip end to end -- runtime behavior that `go test` alone never
8+
# covers.
89
#
910
# Both agent runtimes are exercised via the `lang` matrix dimension: Python
1011
# (testdata/echo-agent, the test's default) and Node (testdata/echo-agent-node,
@@ -22,16 +23,6 @@ name: Session E2E
2223
# fixture via `uv sync` from its pyproject.toml; the Node fixture via
2324
# `npm install` from its package.json. The Node `.ts` entrypoint runs through
2425
# Node's --experimental-strip-types loader, which needs Node >= 22.6.
25-
#
26-
# Windows is split into two jobs: pkg/apm/webrtc's C++ uses MSVC SEH
27-
# (__try/__except) that the runner's mingw GCC can't compile, so we build with
28-
# zig (clang) exactly as .goreleaser.yaml does. zig-as-CC must run on Linux: on
29-
# native Windows the cgo link of ~560 webrtc/portaudio objects overflows the
30-
# ~32KB command-line limit. So `cross-build-windows` cross-compiles lk.exe AND
31-
# the e2e test binary on Linux and uploads them; `e2e-windows` downloads them
32-
# and runs the test natively (LK_SESSION_E2E_BIN points the test at the
33-
# prebuilt lk, so nothing is rebuilt on the Windows runner). The binaries are
34-
# language-agnostic, so that build runs once and feeds every `lang` arm.
3526

3627
on:
3728
workflow_dispatch:
@@ -114,135 +105,3 @@ jobs:
114105
# Resolved relative to cmd/lk, the test binary's working directory.
115106
LK_SESSION_E2E_AGENT: ${{ matrix.lang == 'node' && 'testdata/echo-agent-node/agent.ts' || '' }}
116107
run: go test ./cmd/lk -run TestSessionE2E -count=1 -v -timeout 600s
117-
118-
# Cross-compile the Windows artifacts on Linux with zig, mirroring
119-
# .goreleaser.yaml's lk-windows-amd64 build. Produces lk.exe (the binary under
120-
# test) and the compiled e2e test binary, both shipped to e2e-windows.
121-
cross-build-windows:
122-
runs-on: ubuntu-latest
123-
name: Cross-build Windows artifacts (zig)
124-
125-
permissions:
126-
contents: read
127-
128-
steps:
129-
- name: Checkout livekit-cli
130-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
131-
with:
132-
submodules: true
133-
134-
- name: Set up Zig
135-
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
136-
with:
137-
version: 0.14.1
138-
139-
- name: Set up Go
140-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
141-
with:
142-
go-version-file: go.mod
143-
cache: true
144-
145-
# Generate the MinGW import libs lld needs for Go's /DEFAULTLIB references
146-
# (dbghelp, bcrypt, ...), exactly as goreleaser's before-hook does.
147-
- name: Generate MinGW import libs
148-
run: scripts/setup-cross.sh windows/amd64
149-
150-
- name: Cross-compile lk.exe and the e2e test binary
151-
env:
152-
GOOS: windows
153-
GOARCH: amd64
154-
CGO_ENABLED: "1"
155-
# zig cc/c++ as the cgo toolchain, matching .goreleaser.yaml's Windows
156-
# build. -fms-extensions (SEH) isn't on cgo's allowlist; -fno-sanitize
157-
# is a zig UBSan-under-lld workaround -- both copied from goreleaser.
158-
CC: zig cc -target x86_64-windows-gnu
159-
CXX: zig c++ -target x86_64-windows-gnu
160-
CGO_CXXFLAGS_ALLOW: -fms-extensions
161-
CGO_CXXFLAGS: -fno-sanitize=all
162-
run: |
163-
ldflags="-extldflags=-L$PWD/.cross/windows_amd64/mingw_lib"
164-
mkdir -p dist
165-
go build -ldflags="$ldflags" -o dist/lk.exe ./cmd/lk
166-
go test -c -ldflags="$ldflags" -o dist/session-e2e.test.exe ./cmd/lk
167-
168-
- name: Upload Windows artifacts
169-
uses: actions/upload-artifact@v4
170-
with:
171-
name: windows-e2e
172-
path: dist/*.exe
173-
retention-days: 1
174-
if-no-files-found: error
175-
176-
# Run the prebuilt Windows artifacts natively. No Go/zig/cgo here: the test
177-
# binary just drives lk.exe (via LK_SESSION_E2E_BIN) against the echo agent.
178-
e2e-windows:
179-
needs: cross-build-windows
180-
runs-on: windows-latest
181-
name: Agent Session with ${{ matrix.lang }} Agent on windows-latest
182-
183-
strategy:
184-
fail-fast: false
185-
matrix:
186-
lang: [python, node]
187-
188-
permissions:
189-
contents: read
190-
191-
steps:
192-
- name: Checkout livekit-cli
193-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
194-
# No submodules: the prebuilt binaries already contain the cgo code;
195-
# only the echo-agent fixtures (plain repo files) are needed at runtime.
196-
197-
- name: Download Windows artifacts
198-
uses: actions/download-artifact@v4
199-
with:
200-
name: windows-e2e
201-
path: dist
202-
203-
- name: Set up Python
204-
if: matrix.lang == 'python'
205-
uses: actions/setup-python@v5
206-
with:
207-
python-version: "3.12"
208-
209-
- name: Set up uv
210-
if: matrix.lang == 'python'
211-
uses: astral-sh/setup-uv@v5
212-
with:
213-
enable-cache: true
214-
215-
- name: Sync echo agent deps
216-
if: matrix.lang == 'python'
217-
working-directory: cmd/lk/testdata/echo-agent
218-
run: uv sync
219-
220-
- name: Set up Node
221-
if: matrix.lang == 'node'
222-
uses: actions/setup-node@v4
223-
with:
224-
# --experimental-strip-types (used to run the .ts entrypoint) needs >= 22.6.
225-
node-version: "22"
226-
227-
- name: Install Node echo agent deps
228-
if: matrix.lang == 'node'
229-
working-directory: cmd/lk/testdata/echo-agent-node
230-
run: npm install
231-
232-
- name: Run session e2e
233-
# Run from cmd/lk so the test's relative testdata/ entrypoint paths
234-
# resolve; point it at the cross-built lk.exe so nothing rebuilds.
235-
shell: bash
236-
working-directory: cmd/lk
237-
env:
238-
# Relative to cmd/lk; Go's filepath.Abs resolves it. Forward slashes
239-
# keep bash happy (GITHUB_WORKSPACE would carry Windows backslashes).
240-
LK_SESSION_E2E_BIN: ../../dist/lk.exe
241-
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
242-
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
243-
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
244-
# Default (unset) targets the Python fixture; point at the Node one for that arm.
245-
LK_SESSION_E2E_AGENT: ${{ matrix.lang == 'node' && 'testdata/echo-agent-node/agent.ts' || '' }}
246-
run: |
247-
../../dist/session-e2e.test.exe \
248-
-test.run TestSessionE2E -test.count=1 -test.v -test.timeout 600s

.github/workflows/windows.yaml

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
name: Windows
2+
3+
# All Windows CI for the repo. test.yaml covers Linux/macOS Go tests and the
4+
# `e2e` job in session-e2e.yaml covers Linux/macOS session e2e; Windows is here.
5+
#
6+
# pkg/apm/webrtc's C++ uses MSVC SEH (__try/__except) that the runner's mingw GCC
7+
# can't compile, and on native Windows the cgo link of ~560 webrtc/portaudio
8+
# objects overflows the ~32KB command-line limit. So -- exactly like
9+
# .goreleaser.yaml -- everything is cross-compiled on Linux with zig (clang) in a
10+
# single `cross-build` job, then run natively on windows-latest. Both consumers
11+
# (the unit-test run and the session e2e run) depend on that one build, so the
12+
# expensive cgo compile happens once.
13+
14+
on:
15+
push:
16+
branches: ['**']
17+
pull_request:
18+
branches: [main]
19+
workflow_dispatch:
20+
21+
concurrency:
22+
group: windows-${{ github.ref }}
23+
cancel-in-progress: true
24+
25+
jobs:
26+
# Cross-compile lk.exe and a test binary per package on Linux with zig.
27+
# manifest.tsv maps each test binary to its package directory so the unit-test
28+
# job can run it from there (tests read cwd-relative fixtures, e.g.
29+
# cmd/lk/testdata and pkg/agentfs/examples).
30+
cross-build:
31+
runs-on: ubuntu-latest
32+
name: Cross-build Windows artifacts (zig)
33+
34+
permissions:
35+
contents: read
36+
37+
steps:
38+
- name: Checkout livekit-cli
39+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
40+
with:
41+
# pkg/portaudio/pa_src is a submodule with the vendored PortAudio C
42+
# source linked into cmd/lk via cgo.
43+
submodules: true
44+
45+
- name: Set up Zig
46+
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
47+
with:
48+
version: 0.14.1
49+
50+
- name: Set up Go
51+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
52+
with:
53+
go-version-file: go.mod
54+
cache: true
55+
56+
# Generate the MinGW import libs lld needs for Go's /DEFAULTLIB references
57+
# (dbghelp, bcrypt, ...), exactly as goreleaser's before-hook does.
58+
- name: Generate MinGW import libs
59+
run: scripts/setup-cross.sh windows/amd64
60+
61+
- name: Cross-compile lk.exe and per-package test binaries
62+
env:
63+
GOOS: windows
64+
GOARCH: amd64
65+
CGO_ENABLED: "1"
66+
# zig cc/c++ as the cgo toolchain, matching .goreleaser.yaml's Windows
67+
# build. -fms-extensions (SEH) isn't on cgo's allowlist; -fno-sanitize
68+
# is a zig UBSan-under-lld workaround -- both copied from goreleaser.
69+
CC: zig cc -target x86_64-windows-gnu
70+
CXX: zig c++ -target x86_64-windows-gnu
71+
CGO_CXXFLAGS_ALLOW: -fms-extensions
72+
CGO_CXXFLAGS: -fno-sanitize=all
73+
run: |
74+
set -euo pipefail
75+
ldflags="-extldflags=-L$PWD/.cross/windows_amd64/mingw_lib"
76+
module=$(go list -m)
77+
mkdir -p dist
78+
go build -ldflags="$ldflags" -o dist/lk.exe ./cmd/lk
79+
: > dist/manifest.tsv
80+
for pkg in $(go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./...); do
81+
rel=${pkg#"$module"/}
82+
[ "$rel" = "$module" ] && rel="."
83+
safe=$(echo "$rel" | tr '/' '_')
84+
go test -c -ldflags="$ldflags" -o "dist/${safe}.test.exe" "$pkg"
85+
printf '%s\t%s\n' "${safe}.test.exe" "$rel" >> dist/manifest.tsv
86+
done
87+
echo "Built lk.exe and:"; cat dist/manifest.tsv
88+
89+
- name: Upload Windows artifacts
90+
uses: actions/upload-artifact@v4
91+
with:
92+
name: windows-artifacts
93+
path: dist
94+
retention-days: 1
95+
if-no-files-found: error
96+
97+
# Run every package's test binary natively, from its package directory so
98+
# cwd-relative fixtures resolve. No secrets: TestSessionE2E (in the cmd/lk
99+
# binary) skips without LIVEKIT_API_KEY and is exercised by windows-session-e2e
100+
# below. node ships on the runner; uv is installed for the Python SDK tests.
101+
windows-tests:
102+
needs: cross-build
103+
runs-on: windows-latest
104+
name: Go test on windows-latest
105+
106+
permissions:
107+
contents: read
108+
109+
steps:
110+
- name: Checkout livekit-cli
111+
# Test fixtures (cmd/lk/testdata, pkg/agentfs/examples) are read at
112+
# runtime; submodules are C source already compiled into the binaries.
113+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
114+
115+
- name: Download Windows artifacts
116+
uses: actions/download-artifact@v4
117+
with:
118+
name: windows-artifacts
119+
path: dist
120+
121+
- name: Set up Python
122+
uses: actions/setup-python@v5
123+
with:
124+
python-version: "3.12"
125+
126+
- name: Set up uv
127+
uses: astral-sh/setup-uv@v5
128+
with:
129+
enable-cache: true
130+
131+
- name: Run test binaries
132+
shell: bash
133+
run: |
134+
set -uo pipefail
135+
root=$(pwd)
136+
fail=0
137+
while IFS=$'\t' read -r exe rel; do
138+
echo "::group::$rel"
139+
( cd "$rel" && "$root/dist/$exe" -test.v -test.count=1 ) || fail=1
140+
echo "::endgroup::"
141+
done < "$root/dist/manifest.tsv"
142+
exit $fail
143+
144+
# Drive the real session lifecycle on Windows against the prebuilt lk.exe.
145+
# Needs live LiveKit credentials, so skip on pull_request (forks can't read
146+
# secrets); the cmd/lk test binary already exists from cross-build.
147+
windows-session-e2e:
148+
needs: cross-build
149+
if: github.event_name != 'pull_request'
150+
runs-on: windows-latest
151+
name: lk agent with ${{ matrix.lang }} agent on windows-latest
152+
153+
# Same `lang` matrix as session-e2e.yaml's e2e job: Python and Node echo
154+
# agents. The cross-built binaries are language-agnostic, so one build
155+
# feeds both arms.
156+
strategy:
157+
fail-fast: false
158+
matrix:
159+
lang: [python, node]
160+
161+
permissions:
162+
contents: read
163+
164+
steps:
165+
- name: Checkout livekit-cli
166+
# Needed for the echo-agent fixtures; submodules are compiled into lk.exe.
167+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
168+
169+
- name: Download Windows artifacts
170+
uses: actions/download-artifact@v4
171+
with:
172+
name: windows-artifacts
173+
path: dist
174+
175+
- name: Set up Python
176+
if: matrix.lang == 'python'
177+
uses: actions/setup-python@v5
178+
with:
179+
python-version: "3.12"
180+
181+
- name: Set up uv
182+
if: matrix.lang == 'python'
183+
uses: astral-sh/setup-uv@v5
184+
with:
185+
enable-cache: true
186+
187+
- name: Sync echo agent deps
188+
if: matrix.lang == 'python'
189+
working-directory: cmd/lk/testdata/echo-agent
190+
run: uv sync
191+
192+
- name: Set up Node
193+
if: matrix.lang == 'node'
194+
uses: actions/setup-node@v4
195+
with:
196+
# --experimental-strip-types (used to run the .ts entrypoint) needs >= 22.6.
197+
node-version: "22"
198+
199+
- name: Install Node echo agent deps
200+
if: matrix.lang == 'node'
201+
working-directory: cmd/lk/testdata/echo-agent-node
202+
run: npm install
203+
204+
- name: Run session e2e
205+
# Run from cmd/lk so the test's relative testdata/ entrypoint paths
206+
# resolve; point it at the cross-built lk.exe so nothing rebuilds.
207+
shell: bash
208+
working-directory: cmd/lk
209+
env:
210+
LK_SESSION_E2E_BIN: ../../dist/lk.exe
211+
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
212+
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
213+
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
214+
# Default (unset) targets the Python fixture; point at the Node one for that arm.
215+
LK_SESSION_E2E_AGENT: ${{ matrix.lang == 'node' && 'testdata/echo-agent-node/agent.ts' || '' }}
216+
run: |
217+
../../dist/cmd_lk.test.exe \
218+
-test.run TestSessionE2E -test.count=1 -test.v -test.timeout 600s

0 commit comments

Comments
 (0)