Skip to content

Commit bbe59df

Browse files
committed
test: raise internal-package coverage to 99.8%
Cover the remaining gaps across server, tokens, and tui: - server: Connect attach-not-ready and port-allocation errors, spawn start failure, Stop's kill fallback for SIGINT-ignoring children (extracts the hardcoded 8s kill wait into a stopTimeout var, mirroring readyTimeout), splitTokenURL's unparseable-URL return, legacyToken's probe error, and freePort under fd starvation (unix-only test) - tokens: concurrent Set/Get/Delete smoke test under -race - tui: shortenHome arms, welcome cwd line, resource glyphs, /stats conditional arms, esc/^R/^O keys, error-event fallback, disconnect log-path hint, thinking-enabled submit, step-glyph dedup/cap, fetchModels cmd, relayout-not-ready, syncAC query reuse and 6-file trim, elapsed minutes, multibyte capThinking, ctxGauge clamps, gaugeColor bands, acPopup cmd mode/narrow floor, footer gap floor The three remaining uncovered blocks are verified-unreachable defensive branches (json.MarshalIndent of map[string]string, glamour Render with its fixed config, and a pad guard that cannot fire by construction). Also gitignore the coverage.out written by `make cover`.
1 parent 76b8799 commit bbe59df

7 files changed

Lines changed: 504 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Build artifacts
22
/bin/
33
/bodek
4+
/coverage.out
45

56
# Editor / OS noise
67
.DS_Store

internal/server/server.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const wsTokenCookie = "odek_ws_token"
2424
// variable so tests can shorten it.
2525
var readyTimeout = 30 * time.Second
2626

27+
// stopTimeout bounds how long Stop waits for the spawned server's graceful
28+
// shutdown before killing it. It is a variable so tests can shorten it.
29+
var stopTimeout = 8 * time.Second
30+
2731
// Conn holds everything needed to talk to an odek serve instance.
2832
type Conn struct {
2933
BaseURL string // http://127.0.0.1:port
@@ -162,7 +166,7 @@ func (c *Conn) Stop() {
162166
go func() { _ = c.proc.Wait(); close(done) }()
163167
select {
164168
case <-done:
165-
case <-time.After(8 * time.Second):
169+
case <-time.After(stopTimeout):
166170
_ = c.proc.Process.Kill()
167171
}
168172
}

internal/server/server_internal_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,64 @@ func TestSpawnDefaultBinMissing(t *testing.T) {
6363
}
6464
}
6565

66+
func TestConnectURLNotReady(t *testing.T) {
67+
// Attaching to a URL whose server never answers must fail after the ready
68+
// timeout instead of hanging.
69+
old := readyTimeout
70+
readyTimeout = 250 * time.Millisecond
71+
defer func() { readyTimeout = old }()
72+
73+
if _, err := Connect(Options{URL: "http://127.0.0.1:1"}); err == nil {
74+
t.Error("expected Connect to fail when the attached server is not ready")
75+
}
76+
}
77+
78+
func TestSpawnStartError(t *testing.T) {
79+
// A binary that passes LookPath but cannot be exec'd (its shebang points
80+
// at a nonexistent interpreter) must surface the cmd.Start error.
81+
bin := filepath.Join(t.TempDir(), "odek-fake")
82+
if err := os.WriteFile(bin, []byte("#!/nonexistent-interpreter-xyz\n"), 0o755); err != nil {
83+
t.Fatalf("write fake odek: %v", err)
84+
}
85+
c := &Conn{}
86+
if err := c.spawn(Options{Bin: bin}, "127.0.0.1:0"); err == nil {
87+
t.Error("expected spawn to fail when the binary cannot be started")
88+
}
89+
}
90+
91+
func TestStopKillsLingeringProcess(t *testing.T) {
92+
// A server that ignores SIGINT must be force-killed once stopTimeout
93+
// elapses. `exec` preserves the ignored-signal disposition, so the sleep
94+
// itself ignores SIGINT and Stop falls back to Kill.
95+
sh, err := exec.LookPath("sh")
96+
if err != nil {
97+
t.Skip("no 'sh' binary available")
98+
}
99+
old := stopTimeout
100+
stopTimeout = 200 * time.Millisecond
101+
defer func() { stopTimeout = old }()
102+
103+
// The "ready" line guarantees the trap is installed before SIGINT is sent.
104+
c := &Conn{proc: exec.Command(sh, "-c", "trap '' INT; echo ready; exec sleep 30")}
105+
stdout, err := c.proc.StdoutPipe()
106+
if err != nil {
107+
t.Fatalf("stdout pipe: %v", err)
108+
}
109+
if err := c.proc.Start(); err != nil {
110+
t.Fatalf("start: %v", err)
111+
}
112+
if _, err := io.ReadFull(stdout, make([]byte, len("ready\n"))); err != nil {
113+
t.Fatalf("read readiness line: %v", err)
114+
}
115+
done := make(chan struct{})
116+
go func() { c.Stop(); close(done) }()
117+
select {
118+
case <-done:
119+
case <-time.After(5 * time.Second):
120+
t.Fatal("Stop did not fall back to Kill for a lingering process")
121+
}
122+
}
123+
66124
func TestConnectSpawnNotReady(t *testing.T) {
67125
bin, err := exec.LookPath("sleep")
68126
if err != nil {

internal/server/server_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ func TestFetchTokenRequestError(t *testing.T) {
6363
}
6464
}
6565

66+
func TestLegacyTokenProbeError(t *testing.T) {
67+
// Unreachable server: the cookie fetch fails and the auth-enforcement
68+
// probe fails too, so legacyToken returns a hard error.
69+
if _, err := legacyToken("http://127.0.0.1:1"); err == nil {
70+
t.Error("expected legacyToken probe error to unreachable host")
71+
}
72+
}
73+
6674
func TestConnectViaURL(t *testing.T) {
6775
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6876
http.SetCookie(w, &http.Cookie{Name: wsTokenCookie, Value: "tok"})
@@ -171,6 +179,7 @@ func TestSplitTokenURL(t *testing.T) {
171179
{"http://127.0.0.1:8080/?token=abc", "http://127.0.0.1:8080/", "abc"},
172180
{"http://127.0.0.1:8080", "http://127.0.0.1:8080", ""},
173181
{"http://127.0.0.1:8080/?token=a%20b", "http://127.0.0.1:8080/", "a b"},
182+
{"\x7f", "\x7f", ""}, // unparseable URL is returned as-is, without a token
174183
}
175184
for _, tc := range cases {
176185
base, token := splitTokenURL(tc.raw)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//go:build unix
2+
3+
package server
4+
5+
import (
6+
"net"
7+
"strings"
8+
"syscall"
9+
"testing"
10+
)
11+
12+
// TestFreePortFdStarvation exercises the freePort error path — and Connect's
13+
// "allocate port" branch on top of it — by temporarily clamping RLIMIT_NOFILE
14+
// so that no new file descriptor can be opened. The soft limit is lowered and
15+
// then raised back before the test returns, so no other test is affected.
16+
func TestFreePortFdStarvation(t *testing.T) {
17+
// Force netpoller initialization before clamping: it needs a kqueue/epoll
18+
// descriptor of its own, and creating one under the clamp is a fatal
19+
// runtime error instead of a clean Listen error.
20+
l, err := net.Listen("tcp", "127.0.0.1:0")
21+
if err != nil {
22+
t.Fatalf("warmup listen: %v", err)
23+
}
24+
_ = l.Close()
25+
26+
// New descriptors are always allocated at the lowest free number, and
27+
// RLIMIT_NOFILE caps exactly that number — clamping the soft limit to the
28+
// lowest free descriptor makes every subsequent open fail with EMFILE.
29+
lowest := 0
30+
var st syscall.Stat_t
31+
for lowest < 4096 && syscall.Fstat(lowest, &st) == nil {
32+
lowest++
33+
}
34+
var lim syscall.Rlimit
35+
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
36+
t.Fatalf("getrlimit: %v", err)
37+
}
38+
restore := lim
39+
lim.Cur = uint64(lowest)
40+
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
41+
t.Fatalf("setrlimit: %v", err)
42+
}
43+
defer func() {
44+
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &restore); err != nil {
45+
t.Errorf("restore rlimit: %v", err)
46+
}
47+
}()
48+
49+
if _, err := freePort(); err == nil {
50+
t.Error("expected freePort to fail with no descriptors available")
51+
}
52+
if _, err := Connect(Options{}); err == nil || !strings.Contains(err.Error(), "allocate port") {
53+
t.Errorf("Connect = %v, want allocate-port error", err)
54+
}
55+
}

internal/tokens/tokens_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ func TestPersistWriteAndRenameErrors(t *testing.T) {
131131
s2.Set("k", "v") // WriteFile ok, Rename onto a directory fails
132132
}
133133

134+
func TestConcurrentAccess(t *testing.T) {
135+
// Smoke test for the Store's locking: run with -race to catch data races.
136+
s := &Store{m: map[string]string{}}
137+
done := make(chan struct{})
138+
for i := 0; i < 4; i++ {
139+
go func(i int) {
140+
defer func() { done <- struct{}{} }()
141+
id := string(rune('a' + i))
142+
for j := 0; j < 50; j++ {
143+
s.Set(id, "tok")
144+
_ = s.Get(id)
145+
s.Delete(id)
146+
}
147+
}(i)
148+
}
149+
for i := 0; i < 4; i++ {
150+
<-done
151+
}
152+
}
153+
134154
func TestOpenWithCorruptFile(t *testing.T) {
135155
dir := t.TempDir()
136156
t.Setenv("HOME", dir)

0 commit comments

Comments
 (0)