Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
273 lines (232 loc) · 11.8 KB
/
Copy pathMakefile
File metadata and controls
273 lines (232 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
NAME=tinycode
BINDIR=bin
# The application's own tags only. Since the nested tuiprobe module ships in this
# repository, its tags (tuiprobe/v*) are often nearer to HEAD than an application tag,
# and an unfiltered `git describe` reported the tool's version as the product's
# (issue #81). The fallback is the short commit, which is more useful than "dev" and
# cannot be mistaken for a release.
VERSION=$(shell git --no-pager describe --tags --match 'v[0-9]*' 2>/dev/null || git --no-pager rev-parse --short HEAD 2>/dev/null || echo "dev")
COMMIT_SHA=$(shell git --no-pager rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "main.Version=$(VERSION)" \
-X "main.CommitSHA=$(COMMIT_SHA)" \
-X "main.BuildTime=$(BUILDTIME)" \
-w -s -buildid='
PLATFORM_LIST = \
linux-amd64 \
linux-arm64 \
darwin-arm64
.PHONY: default build run test test-race test-repeat test-lsp test-browser test-tui-visual install-gopls install-tsls test-tuiprobe test-tuiprobe-race test-tuiprobe-examples lint-tuiprobe build-tuiprobe lint staticcheck fmt fmt-check fuzz clean all test-tui-confine
default: build
build:
@mkdir -p $(BINDIR)
$(GOBUILD) -o $(BINDIR)/$(NAME) .
run: build
./$(BINDIR)/$(NAME) $(PROMPT)
test:
@go test ./... -count=1 2>&1; status=$$?; \
if [ $$status -eq 0 ]; then \
echo "=== ALL TESTS PASSED ==="; \
else \
echo "=== TESTS FAILED (exit $$status) ==="; \
fi; \
exit $$status
# Race detector run. Any data race fails the build.
test-race:
go test -race ./... -count=1
# Repeat the suite in one process to catch leaked global state between runs.
test-repeat:
go test ./... -count=3
# Language server used by the gated LSP integration tests. Keep this in step
# with the flake's pkgs.gopls (0.23.0).
GOPLS_VERSION ?= v0.23.0
install-gopls:
go install golang.org/x/tools/gopls@$(GOPLS_VERSION)
# The second language server the gated tests exercise (issue #10). TypeScript 5 is
# pinned on purpose: tsserver resolves `typescript` from the workspace, and
# versions 6+ restructured the package (no lib/tsserver.js), which
# typescript-language-server refuses with "Could not find a valid TypeScript
# installation". The test links the installation that sits next to the server into
# its fixture, which is where a global npm install puts it.
TSLS_VERSION ?= latest
install-tsls:
npm install -g typescript@5 typescript-language-server@$(TSLS_VERSION)
# The LSP integration tests spawn a real language server, so gopls must be on
# PATH (the Nix devShell provides it) and LSP_TEST must be set to un-skip them.
test-lsp:
# -v on purpose: which server was exercised, and which case skipped with which
# reason, is the point of this target — a silent skip is how the second server
# stayed unverified (issue #10).
LSP_TEST=1 go test -count=1 -v ./lsp/...
# Real-browser smoke tests: renders a local JavaScript page through both browser
# paths and asserts the filtering proxy was used (TestBrowserSmokeThroughProxy),
# then serves a page with a scoped loopback exemption and asserts a subresource on
# a second, live loopback service is refused by the proxy
# (TestBrowserSmokeRefusesABlockedSubresource). Needs a Chromium/Chrome (system
# install or the Playwright cache); skipped without BROWSER_TEST=1 so an ordinary
# `make test` never launches a browser.
test-browser:
BROWSER_TEST=1 go test -count=1 -timeout 5m -run TestBrowserSmoke ./tool/
# On-demand visual check for the TUI: renders the committed frame scenarios to
# PNGs (Chromium), runs the built binary on a real 80x24 PTY and again on a PTY
# whose window size was never set (the 0x0 report), replays the live terminal
# stream into a screen buffer and screenshots that too, then asserts the streams
# carried styling and the runs quit cleanly. Needs `bin/tinycode` (built here), a
# Chromium (system install or the Playwright cache) and a PTY; everything is
# skipped without TUI_SHOT=1, so an ordinary `make test` needs neither. PNGs
# land in TUI_SHOT_DIR (default /tmp) for a reviewer or an agent to open.
test-tui-visual: build
# -v on purpose, for the same reason make test-lsp has it: which gated check ran
# and which skipped, with the reason, is the point of this job. A silent skip is
# how a gate stops being a gate.
TUI_SHOT=1 go test -count=1 -v -timeout 10m -run 'TestFrameScreenshots|TestBinary' ./tui/
# The scenario files drive the real binary through tuiprobe, whose scenario runner is
# an internal package — so the command line is the only way to run them, and the
# version is whatever go.mod pins (no @version here on purpose: one place to bump).
# TUI_SHOT gates them exactly as it gates the Go tests.
test-tui-scenarios: build
# A clean home per run. Scenarios share it, so a leftover from an earlier run — or
# from an earlier scenario in this one — changes the world a later one sees:
# theme-command passed standalone and in CI, yet failed after the permission pair had
# written into the same directory. CI was green only because its /tmp starts empty
# (issue #96). The fixture is seeded below, after the wipe.
rm -rf /tmp/tinyscen-home
mkdir -p /tmp/tinyscen-home/sessions /tmp/tinyscen-shots
# Seed the committed session fixture so --resume asserts known content rather than
# 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;; *lsp-*) echo "== $$f (skipped: run make test-tui-lsp)"; continue;; *confine-*) echo "== $$f (skipped: run make test-tui-confine)"; 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 scenarios, gated the same way make test-lsp is: they need 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
@for f in tui/testdata/scenarios/lsp-*.scenario; do \
echo "== $$f"; \
LSP_TEST=1 go run github.com/yusiwen/TinyCode/tuiprobe/cmd/tuiprobe run --gate LSP_TEST --dir . "$$f" || exit 1; \
done
# The command-boundary scenarios, gated the way make test-lsp is: they need a kernel
# mechanism for subprocesses, which a plain `make test` host may not have. The gate is
# decided by asking the real launcher rather than by guessing from the platform name, and a
# host without the mechanism skips with the reason printed — a gated check that silently
# passed would look like evidence and be none.
# One shell for the probe and the loop: an `exit` in a recipe line only ends that line, so
# a skip written as its own line would still run the scenarios below it.
test-tui-confine: build
mkdir -p /tmp/tinyscen-shots
@if ! out=$$(./bin/tinycode __sandbox-exec --mode read-only -- true 2>&1); then \
echo "skipped: this host cannot apply a kernel file boundary to a subprocess"; \
echo " launcher said: $$out"; \
echo " kernel LSMs: $$(cat /sys/kernel/security/lsm 2>/dev/null || echo '(not readable)')"; \
else \
probe=$$(mktemp -d); \
pout=$$(./bin/tinycode __sandbox-exec --mode workspace-write --allow "$$probe" -- sh -c "echo ok > $$probe/inside.txt" 2>&1); prc=$$?; \
if [ $$prc -ne 0 ] || [ ! -f "$$probe/inside.txt" ]; then \
echo "FAILED: the launcher cannot write inside a root it was told to allow (rc=$$prc)"; \
echo " launcher said: $$pout"; \
echo " kernel LSMs: $$(cat /sys/kernel/security/lsm 2>/dev/null || echo '(not readable)')"; \
rm -rf "$$probe"; \
exit 1; \
fi; \
echo "mechanism check: a granted root is writable"; \
rm -rf "$$probe"; \
for f in tui/testdata/scenarios/confine-*.scenario; do \
echo "== $$f"; \
SANDBOX_TEST=1 go run github.com/yusiwen/TinyCode/tuiprobe/cmd/tuiprobe run --gate SANDBOX_TEST --dir . "$$f" || exit 1; \
done; \
fi
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
# Blocking lint: `go vet` failures fail the build.
lint:
go vet ./...
# Stricter linter, pinned so local runs match CI. Deliberately separate from
# `lint` so a missing tool can never turn that target into a no-op.
STATICCHECK_VERSION ?= v0.8.1
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) ./...
# tuiprobe is a nested module: the root's ./... does NOT see it (measured: go
# list/build/vet ./... all skip it silently), so it needs its own targets and its
# own CI steps. gofmt -l . from the root does cover it.
TUIPROBE_DIR = tuiprobe
test-tuiprobe:
cd $(TUIPROBE_DIR) && go vet ./... && go test ./... -count=1
test-tuiprobe-race:
cd $(TUIPROBE_DIR) && go test ./... -count=1 -race
lint-tuiprobe:
cd $(TUIPROBE_DIR) && go run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) ./...
build-tuiprobe:
@mkdir -p $(BINDIR)
cd $(TUIPROBE_DIR) && go build -o ../$(BINDIR)/tuiprobe ./cmd/tuiprobe
# The scenarios tuiprobe ships itself, under tuiprobe/testdata/scenarios/. They drive programs
# this repository does not contain (/bin/sh), which is what makes them the file a new consumer
# copies; the consumer's own suite is test-tui-scenarios. They need no browser, no network and no
# gate variable, so they run everywhere `go test` does and rot loudly rather than silently.
test-tuiprobe-examples: build-tuiprobe
@cd $(TUIPROBE_DIR) && for f in testdata/scenarios/*.scenario; do \
echo "== $$f"; \
../$(BINDIR)/tuiprobe run --dir testdata/scenarios "$$f" || exit 1; \
done
# Format the tracked Go sources in place.
fmt:
gofmt -w $$(git ls-files '*.go')
# Fail when a tracked Go file is not gofmt-clean.
fmt-check:
@files="$$(gofmt -l $$(git ls-files '*.go'))"; \
if [ -n "$$files" ]; then \
echo "=== gofmt required for: ==="; echo "$$files"; exit 1; \
fi
clean:
rm -rf $(BINDIR)
# ---- Cross-compilation ----
linux-amd64:
@mkdir -p $(BINDIR)
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64:
@mkdir -p $(BINDIR)
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64:
@mkdir -p $(BINDIR)
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
all: $(PLATFORM_LIST)
@echo "Built all platforms: $(PLATFORM_LIST)"
# Release builds: cross-compile all platforms and create compressed archives
gz_releases = $(addsuffix .tar.gz, $(PLATFORM_LIST))
zip_releases = $(addsuffix .zip, $(PLATFORM_LIST))
%.tar.gz: %
tar czf $(BINDIR)/$(NAME)-$*.tar.gz -C $(BINDIR) $(NAME)-$*
rm -f $(BINDIR)/$(NAME)-$*
%.zip: %
cd $(BINDIR) && zip $(NAME)-$*.zip $(NAME)-$*
rm -f $(BINDIR)/$(NAME)-$*
releases: $(gz_releases)
@echo "Release archives: $(gz_releases)"
# Fuzz targets, as "<package>:<function>". `go test -fuzz` runs exactly one
# target per invocation, so this loops over the list. The seed corpus of every
# target already runs as part of `make test`; this explores further.
FUZZTIME ?= 30s
FUZZ_TARGETS = \
./tool:FuzzFuzzyFindInvariants \
./tool:FuzzCorrectIndentation \
./tool:FuzzLevenshtein \
./tool:FuzzRelBeneath \
./tui:FuzzWordWrapPreservesWords \
./tui:FuzzParseMarkdown \
./mcp:FuzzReadMessageBounds \
./internal/netsafe:FuzzIsBlockedIP \
./internal/netsafe:FuzzNormalizeAuthority \
./internal/browserproxy:FuzzProxyHandlesArbitraryTargets
fuzz:
@for target in $(FUZZ_TARGETS); do \
pkg=$${target%%:*}; fn=$${target##*:}; \
echo "=== $$fn ($$pkg) for $(FUZZTIME)"; \
go test -run=^$$ -fuzz=^$$fn$$ -fuzztime=$(FUZZTIME) $$pkg || exit 1; \
done