Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ jobs:

- name: Run LSP integration tests
run: make test-lsp
# The same gopls this job already provides, now driving the TUI scenario that
# proves /diagnostics reaches a real server instead of reporting a config problem.
- name: TUI LSP scenario
run: make test-tui-lsp

cross:
name: Cross-compile + vet (${{ matrix.goos }}/${{ matrix.goarch }})
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ files run through `make test-tui-scenarios` — which is also the CI step in the
`tui-visual` job — and never through a Go test. `wait-exit` before `expect-exit` when
the program leaves on its own: checking its code too early kills it and reports `-1`.

A fifth file, `lsp-diagnostics`, is gated by `LSP_TEST=1` (like `make test-lsp`) because it
needs gopls on PATH: `make test-tui-lsp` runs it, and CI's `lsp` job already has the server.
It asserts `No LSP diagnostics.`, which is only reachable once a client exists — a file read
starts one — so it also guards the fix for issue #111, where the warmup that exists to start
the server refused to run until the server was started.

A fourth file, `live-answer`, is the one that spends money: it makes a real provider
call through this machine's `~/.tinycode/.env`. Run it deliberately with
`make test-tui-live` (gated by `TINYCODE_LIVE`; CI never runs it), and read the error
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,21 @@ test-tui-scenarios: build
# whatever earlier runs happened to leave behind.
cp -n tui/testdata/fixtures/*.json /tmp/tinyscen-home/sessions/ 2>/dev/null || true
@for f in tui/testdata/scenarios/*.scenario; do \
case "$$f" in *live-*) echo "== $$f (skipped: run make test-tui-live)"; continue;; esac; \
case "$$f" in *live-*) echo "== $$f (skipped: run make test-tui-live)"; continue;; *lsp-diagnostics*) echo "== $$f (skipped: run make test-tui-lsp)"; continue;; esac; \
echo "== $$f"; \
TUI_SHOT=1 go run github.com/yusiwen/TinyCode/tuiprobe/cmd/tuiprobe run --gate TUI_SHOT --dir . "$$f" || exit 1; \
done

# The one target here that spends money: a real provider call, deliberately gated
# (TINYCODE_LIVE) and never run by CI. It needs a key the usual way — DEEPSEEK_API_KEY
# in the environment, or ~/.tinycode/.env, which the binary reads at startup.
# The LSP scenario, gated the same way make test-lsp is: it needs gopls on PATH, which CI's
# lsp job has and a plain `make test` does not. Nothing here spends money, so unlike
# test-tui-live it is safe to run whenever gopls is available.
test-tui-lsp: build
mkdir -p /tmp/tinyscen-shots
LSP_TEST=1 go run github.com/yusiwen/TinyCode/tuiprobe/cmd/tuiprobe run --gate LSP_TEST --dir . tui/testdata/scenarios/lsp-diagnostics.scenario

test-tui-live: build
mkdir -p /tmp/tinyscen-live
@TINYCODE_LIVE=1 go run github.com/yusiwen/TinyCode/tuiprobe/cmd/tuiprobe run --gate TINYCODE_LIVE --dir . tui/testdata/scenarios/live-answer.scenario
Expand Down
8 changes: 8 additions & 0 deletions docs/tui-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ deliberately **not** asserted: its text cannot be read without gopls, and writin
assertion for a string nobody has seen is the mistake this suite keeps catching. That one
belongs to a gated line that runs where gopls exists.

`lsp-diagnostics` is the second gated scenario: it runs with `LSP_TEST=1` via
`make test-tui-lsp` (CI's `lsp` job, which already provides gopls) and asserts the
`/diagnostics` outcome `No LSP diagnostics.` — a string only reachable when a client exists.
The server is started the way a user starts it, by reading a file, so the scenario is also
the regression guard for issue #111: while that circle existed, the warmup never ran and the
status line read "LSP not available". The listing outcome would need real diagnostics to be
present and is not covered.

## Typing waits for the input

Every scenario that types waits for the input row (`wait --text "Type your request"`)
Expand Down
26 changes: 26 additions & 0 deletions tui/testdata/scenarios/lsp-diagnostics.scenario
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# "/diagnostics" once a language server is actually running. Gated by LSP_TEST because it
# needs gopls on PATH, which CI's lsp job provides and a plain `make test` does not.
#
# The server is started the way a user starts it: a file read. That path was dead until issue
# #111 — IsAvailable() gated the warmup whose purpose is to start the server — so this
# scenario is also the regression guard for that fix: if the circle returns, the warmup never
# runs, IsAvailable() stays false, and the status line says "LSP not available" instead of the
# literal asserted here.
#
# The assertion is "No LSP diagnostics." rather than a listing: this workspace has gopls with
# nothing to report, and that string is only reachable when a client exists. The listing (the
# third outcome) would need real diagnostics present.
open --dir . --size 120x30 --env HOME=/tmp/tinyscen-home-lsp --env TERM=xterm-256color --env COLORTERM=truecolor --env CLICOLOR_FORCE=1 --env NO_COLOR -- /bin/sh -c 'rm -rf /tmp/tinyscen-home-lsp && mkdir -p /tmp/tinyscen-home-lsp/.tinycode /tmp/tinyscen-home-lsp/sessions && printf %s "{\"lsp\":{\"enabled\":true}}" > /tmp/tinyscen-home-lsp/.tinycode/config.json && python3 tui/testdata/stub/openai_stub.py 18511 read & sleep 1; ./bin/tinycode --base-url=http://127.0.0.1:18511/v1 --api-key=stub --model=stub --session-dir=/tmp/tinyscen-home-lsp/sessions --log-level=debug; status=$?; kill %1 2>/dev/null; exit $status'
wait --text "Get started" --timeout 20s
wait --text "Type your request" --timeout 20s
send --text "read main.go"
send --key enter
wait --text "read_file" --timeout 20s
send --text /diagnostics
send --key enter
wait --text "No LSP diagnostics." --timeout 30s
screenshot /tmp/tinyscen-shots/lsp-diagnostics-120x30.png --scale 2
send --text /exit
send --key enter
wait-exit 20s
expect-exit 0
13 changes: 13 additions & 0 deletions tui/testdata/stub/openai_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
deterministic. Started by the scenario that needs it; see prompt-answer.scenario.
"""
import http.server
import json
import os
import sys

ANSWER = "stub-answer-ok" # keep in sync with prompt-answer.scenario
Expand Down Expand Up @@ -37,6 +39,16 @@
'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}\n\n'
'data: [DONE]\n\n'
)
# "read" asks for read_file on a file in the working directory. It exists because reading a
# file is what starts the language server: the warmup in tool/filesystem.go is the reachable
# path after issue #111, and the LSP scenario needs the server up before /diagnostics can say
# anything other than "not available".
READ_CALL = (
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_r","type":"function",'
'"function":{"name":"read_file","arguments":%s}}]}}]}\n\n'
'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}\n\n'
'data: [DONE]\n\n'
) % json.dumps(json.dumps({"path": os.path.join(os.getcwd(), "main.go")}))
WRITE_CALL = (
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_w","type":"function",'
'"function":{"name":"write_file","arguments":"{\\"path\\":\\"/tmp/stub-outside-write.txt\\",\\"content\\":\\"stub\\"}"}}]}}]}\n\n'
Expand Down Expand Up @@ -75,6 +87,7 @@ def do_POST(self):
"multi": MULTI_CALL,
"fail": FAIL_CALL,
"todo": TODO_CALL,
"read": READ_CALL,
}[MODE].encode()
self.send_response(200)
self.send_header("content-type", "text/event-stream")
Expand Down
Loading