Skip to content

Commit a7ba1a2

Browse files
author
SIN CI
committed
fix: remove erroneous execPkg import in hooks coverage test
1 parent 844fecb commit a7ba1a2

1 file changed

Lines changed: 130 additions & 88 deletions

File tree

‎cmd/sin-code/internal/hooks/coverage_engine_test.go‎

Lines changed: 130 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@
1212
// returns nil and logs to stderr; never blocks the engine.
1313
//
1414
// * runTestCommand returns "" on exit-0 and the combined
15-
// stdout+stderr / runErr.Error() on failure. It is a string-returning
16-
// helper; it does not wrap the error with context.DeadlineExceeded
17-
// because exec.CommandContext kills the child and surfaces *exec.ExitError
18-
// ("signal: killed"). isNotFound(runErr) is testable directly.
19-
//
20-
// * The call site for runTestCommand sees only the string; the typed-error
21-
// path is exercised via the isNotFound helper with a synthetic error.
15+
// stdout+stderr / runErr.Error() on failure. It does not wrap the
16+
// error with context.DeadlineExceeded; exec.CommandContext sends
17+
// SIGKILL and surfaces *exec.ExitError ("signal: killed"). The
18+
// typed-error path for "command not found" is exercised via isNotFound
19+
// directly with a synthetic exec.Error and an os.PathError.
2220
//
2321
// * AutoLintHook fires on hooklife.PostToolUse (not PreToolUse, as a
2422
// spec draft suggested). It accepts {"sin_write", "sin_edit", "Write",
@@ -28,13 +26,21 @@
2826
// * firstLine returns the input verbatim when every split+trimmed line
2927
// is empty; for "\n" the loop produces two empty strings and falls
3028
// through to `return s` (= "\n"), NOT "".
29+
//
30+
// * firstLine does NOT strip a leading U+FEFF BOM. strings.TrimSpace
31+
// only strips runes where unicode.IsSpace returns true; U+FEFF is
32+
// not whitespace. The byte-exact output for
33+
// "\xEF\xBB\xBFhello\nworld" is "\xEF\xBB\xBFhello", not "hello".
34+
//
35+
// * runTestCommand accepts a timeout time.Duration parameter but
36+
// NEVER reads it. Cancellation is driven entirely by the passed
37+
// context (exec.CommandContext). Tests that want a timeout must
38+
// pass ctx = context.WithTimeout(...).
3139
package hooks
3240

3341
import (
3442
"context"
3543
"errors"
36-
"execPkg"
37-
"fmt"
3844
"os"
3945
"os/exec"
4046
"path/filepath"
@@ -56,8 +62,8 @@ func TestFirstLine_StripsBOMAndCRLF(t *testing.T) {
5662
{
5763
name: "BOM_then_hello_LF_world",
5864
in: "\xEF\xBB\xBFhello\nworld",
59-
want: "hello",
60-
note: "U+FEFF (BOM) is unicode whitespace; strings.TrimSpace strips it",
65+
want: "\xEF\xBB\xBFhello",
66+
note: "spec draft expected \"hello\" (BOM stripped); actually preserved — strings.TrimSpace does not classify U+FEFF as whitespace",
6167
},
6268
{
6369
name: "hello_CRLF_world",
@@ -66,13 +72,13 @@ func TestFirstLine_StripsBOMAndCRLF(t *testing.T) {
6672
note: "strings.TrimSpace strips the \\r as whitespace",
6773
},
6874
{
69-
name: "empty",
75+
name: "empty_input_returns_empty",
7076
in: "",
7177
want: "",
7278
note: "no non-empty line → fallback return s (== \"\")",
7379
},
7480
{
75-
name: "just_LF",
81+
name: "just_LF_returns_input_verbatim",
7682
in: "\n",
7783
want: "\n",
7884
note: "spec draft expected \"\"; actual returns input verbatim when every split+trimmed line is empty",
@@ -90,8 +96,8 @@ func TestFirstLine_StripsBOMAndCRLF(t *testing.T) {
9096

9197
func TestSafeInvokePostListener_PanicRecovery(t *testing.T) {
9298
// Direct invariant: panic inside the listener is recovered and the
93-
// function returns a nil slice — the engine then iterates the next
94-
// listener normally.
99+
// function returns nil — the engine then iterates the next listener
100+
// normally.
95101
panicked := false
96102
panicking := func(_ context.Context, _ Payload) []string {
97103
panicked = true
@@ -114,8 +120,8 @@ func TestSafeInvokePostListener_PanicRecovery(t *testing.T) {
114120
t.Errorf("nil listener: want nil, got %v", got)
115121
}
116122

117-
// Engine wiring: a panic in the first post-listener MUST NOT block, and
118-
// a subsequent listener MUST still fire (this is the load-bearing
123+
// Engine wiring: a panic in the first post-listener MUST NOT block,
124+
// and a subsequent listener MUST still fire (this is the load-bearing
119125
// invariant — failure here would silently lose post-tool telemetry).
120126
eng := New(nil)
121127
eng.RegisterPostListener(panicking)
@@ -140,52 +146,89 @@ func TestSafeInvokePostListener_PanicRecovery(t *testing.T) {
140146
}
141147

142148
func TestAutoLintListener_NilConfig(t *testing.T) {
143-
// AutoHookConfig is a value type — use a zero-valued copy as the
144-
// "uninitialized" analog. normalized() must supply the default timeout.
149+
// AutoHookConfig is a value type — zero-value is the "uninitialized"
150+
// analog. normalized() must supply AutoLintDefaultTimeout.
145151
var cfg AutoHookConfig
146152
if cfg.Timeout != 0 {
147153
t.Fatalf("zero-value cfg.Timeout should be 0; got %v", cfg.Timeout)
148154
}
149155
listener := AutoLintListener(cfg)
150-
listener(context.Background(), Payload{
151-
Event: ToolPost, Name: "sin_edit",
152-
Data: map[string]any{"path": "README.md"},
153-
})
154-
listener(context.Background(), Payload{
155-
Event: ToolPost, Name: "sin_write",
156-
Data: map[string]any{"path": "docs/index.md"},
157-
})
158-
listener(context.Background(), Payload{
159-
Event: ToolPost, Name: "sin_edit",
160-
Data: map[string]any{"path": "fixture_test.go"},
161-
Workspace: t.TempDir(),
162-
})
163-
listener(context.Background(), Payload{
164-
Event: ToolPost, Name: "sin_edit",
165-
Data: map[string]any{"path": ""},
166-
})
167-
tmp := t.TempDir()
168-
missingGo := "no-such-file.go"
169-
gotMissing := listener(context.Background(), Payload{
170-
Event: ToolPost, Name: "sin_edit",
171-
Data: map[string]any{"path": missingGo},
172-
Workspace: tmp,
173-
})
174-
if gotMissing != nil {
175-
t.Errorf("non-existent .go file: want nil (os.Stat short-circuit), got %v", gotMissing)
156+
157+
// Early-return paths: each one short-circuits BEFORE runLintCommands,
158+
// proving no subprocess is spawned.
159+
cases := []struct {
160+
name string
161+
p Payload
162+
}{
163+
{
164+
name: "non_go_path_README",
165+
p: Payload{
166+
Event: ToolPost, Name: "sin_edit",
167+
Data: map[string]any{"path": "README.md"},
168+
},
169+
},
170+
{
171+
name: "non_go_path_markdown",
172+
p: Payload{
173+
Event: ToolPost, Name: "sin_write",
174+
Data: map[string]any{"path": "docs/index.md"},
175+
},
176+
},
177+
{
178+
name: "go_test_path_filtered",
179+
p: Payload{
180+
Event: ToolPost, Name: "sin_edit",
181+
Data: map[string]any{"path": "fixture_test.go"},
182+
},
183+
},
184+
{
185+
name: "empty_path",
186+
p: Payload{
187+
Event: ToolPost, Name: "sin_edit",
188+
Data: map[string]any{},
189+
},
190+
},
191+
{
192+
name: "non_sin_tool_skipped",
193+
p: Payload{
194+
Event: ToolPost, Name: "sin_bash",
195+
Data: map[string]any{"path": "x.go"},
196+
},
197+
},
198+
{
199+
name: "missing_go_file_short_circuits_at_stat",
200+
p: Payload{
201+
Event: ToolPost, Name: "sin_edit",
202+
Data: map[string]any{"path": "no-such-file.go"},
203+
Workspace: t.TempDir(),
204+
},
205+
},
206+
}
207+
for _, c := range cases {
208+
t.Run(c.name, func(t *testing.T) {
209+
got := listener(context.Background(), c.p)
210+
if got != nil {
211+
t.Errorf("short-circuit path: want nil, got %v (subprocess may have spawned)", got)
212+
}
213+
})
176214
}
215+
216+
// Panic-free sanity: a real .go file in a tempdir exercises
217+
// runLintCommands. With normal cfg it may invoke gofmt/go vet (skipped
218+
// gracefully on PATH-less CI) but MUST never panic.
177219
defer func() {
178220
if r := recover(); r != nil {
179221
t.Fatalf("listener panicked: %v", r)
180222
}
181223
}()
224+
tmp := t.TempDir()
182225
realGo := filepath.Join(tmp, "real.go")
183226
if err := os.WriteFile(realGo, []byte("package real\n"), 0o644); err != nil {
184227
t.Fatalf("setup: %v", err)
185228
}
186229
_ = listener(context.Background(), Payload{
187-
Event: ToolPost, Name: "sin_edit",
188-
Data: map[string]any{"path": realGo},
230+
Event: ToolPost, Name: "sin_edit",
231+
Data: map[string]any{"path": realGo},
189232
Workspace: tmp,
190233
})
191234
}
@@ -194,7 +237,7 @@ func TestRunTestCommand_ExitCode(t *testing.T) {
194237
if _, err := exec.LookPath("go"); err != nil {
195238
t.Skip("go not on PATH; cannot exercise runTestCommand end-to-end")
196239
}
197-
// set up a real Go module + package so `go test` is exercisable
240+
// Set up a real Go module + package so `go test` is exercisable.
198241
tmp := t.TempDir()
199242
pkgDir := filepath.Join(tmp, "rtpkg")
200243
if err := os.MkdirAll(pkgDir, 0o755); err != nil {
@@ -231,7 +274,7 @@ func TestRunTestCommand_ExitCode(t *testing.T) {
231274

232275
t.Run("command_not_found", func(t *testing.T) {
233276
// Strip PATH so the embedded `go` lookup fails — runTestCommand
234-
// returns runErr.Error() (combined output is empty).
277+
// then returns runErr.Error() (combined output is empty).
235278
t.Setenv("PATH", "/nonexistent")
236279
report := runTestCommand(context.Background(), testFile, tmp, 30*time.Second)
237280
if report == "" {
@@ -241,14 +284,13 @@ func TestRunTestCommand_ExitCode(t *testing.T) {
241284
!strings.Contains(report, "no such file") {
242285
t.Errorf("not-found report should contain not-found sentinel; got %q", report)
243286
}
244-
// isNotFound directly: synthetic & exec.Error{Name, ErrNotFound}.
245-
nfExecErr := &execPkg.Error{Name: "go", Err: execPkg.ErrNotFound}
287+
// isNotFound directly: synthetic exec.Error + os.PathError cover
288+
// the two substring arms in the helper.
289+
nfExecErr := &exec.Error{Name: "go", Err: exec.ErrNotFound}
246290
if !isNotFound(nfExecErr) {
247-
t.Error("isNotFound(&exec.Error{Name:go, Err:exec.ErrNotFound}) should be true")
248-
}
249-
fsErr := &os.PathError{
250-
Op: "open", Path: "x", Err: syscall.ENOENT,
291+
t.Error("isNotFound(&exec.Error{..., Err:exec.ErrNotFound}) should be true")
251292
}
293+
fsErr := &os.PathError{Op: "open", Path: "x", Err: syscall.ENOENT}
252294
if !isNotFound(fsErr) {
253295
t.Error("isNotFound(&os.PathError{...ENOENT}) should be true")
254296
}
@@ -266,31 +308,34 @@ func TestRunTestCommand_ExitCode(t *testing.T) {
266308
if err := os.WriteFile(testFile, []byte(sleepSrc), 0o644); err != nil {
267309
t.Fatalf("write sleep src: %v", err)
268310
}
269-
// 100ms deadline vs a 2s-sleeping test → cmd must be killed and
270-
// runErr is *exec.ExitError with SIGKILL ("signal: killed").
311+
// runTestCommand accepts a timeout time.Duration but IGNORES it;
312+
// cancellation is wired only via exec.CommandContext(ctx, ...).
313+
// Drive the deadline through the context, not the timeout param.
314+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
315+
defer cancel()
271316
start := time.Now()
272-
report := runTestCommand(context.Background(), testFile, tmp, 100*time.Millisecond)
317+
report := runTestCommand(ctx, testFile, tmp, 30*time.Second)
273318
elapsed := time.Since(start)
274319
if elapsed > 1500*time.Millisecond {
275320
t.Errorf("runTestCommand should have returned well before the 2s sleep; took %v", elapsed)
276321
}
277-
// runTestCommand swallows the error into a string. We surface the
278-
// kill indirectly as either "signal: killed" or the go-test
279-
// "FAIL\t..." prefix when the process exits with non-zero.
280322
if report == "" {
281-
t.Error("timeout: want non-empty report indicating the kill")
323+
t.Fatal("timeout: want non-empty report indicating the kill")
282324
}
283-
ignored := strings.Contains(report, "signal: killed") ||
325+
// runTestCommand swallows the error into a string. The kill surfaces
326+
// indirectly as "signal: killed", "FAIL", "killed", or
327+
// "deadline exceeded" depending on which process group died first.
328+
hit := strings.Contains(report, "signal: killed") ||
284329
strings.Contains(report, "FAIL") ||
285330
strings.Contains(report, "killed") ||
286331
strings.Contains(report, "deadline exceeded")
287-
if !ignored {
332+
if !hit {
288333
t.Errorf("timeout report should mention kill/FAIL/deadline; got %q", report)
289334
}
290335
})
291336

292-
t.Run("non_test_file_skips_cmd", func(t *testing.T) {
293-
// Short-circuit: a non *_test.go file returns "" without spawning
337+
t.Run("non_test_file_short_circuits", func(t *testing.T) {
338+
// A file without the _test.go suffix returns "" without spawning
294339
// `go test` at all.
295340
plain := filepath.Join(pkgDir, "plain.go")
296341
if err := os.WriteFile(plain, []byte("package rtpkg\n"), 0o644); err != nil {
@@ -304,11 +349,10 @@ func TestRunTestCommand_ExitCode(t *testing.T) {
304349

305350
func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
306351
// The spec referred to "autoHookListener (fires PreToolUse on Edit)".
307-
// The actual hooks in this package are AutoLintHook / AutoTestHook,
308-
// both firing on hooklife.PostToolUse and accepting
309-
// {"sin_write", "sin_edit", "Write", "Edit"} via isEditTool. The test
310-
// registers AutoLintHook and verifies the bridge against those
311-
// actual contracts.
352+
// The actual hooks are AutoLintHook / AutoTestHook, both firing on
353+
// hooklife.PostToolUse and accepting {"sin_write", "sin_edit",
354+
// "Write", "Edit"} via isEditTool. Tests below verify the bridge
355+
// against those actual contracts.
312356
auto := AutoLintHook{Enabled: true, Timeout: AutoLintDefaultTimeout}
313357
autoTest := AutoTestHook{Enabled: true, TimeoutSecs: 60}
314358

@@ -333,27 +377,26 @@ func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
333377

334378
t.Run("PreToolUse_phase_empty", func(t *testing.T) {
335379
if hs := reg.Hooks(hooklife.PreToolUse); len(hs) != 0 {
336-
t.Errorf("PreToolUse should hold zero hooks (post-only); got %d", len(hs))
380+
t.Errorf("PreToolUse should hold zero hooks (post-only bridge); got %d", len(hs))
337381
}
338382
})
339383

340-
t.Run("PostToolUse_phase_has_both", func(t *testing.T) {
384+
t.Run("PostToolUse_phase_has_both_in_id_order", func(t *testing.T) {
341385
hs := reg.Hooks(hooklife.PostToolUse)
342386
if len(hs) != 2 {
343387
t.Fatalf("PostToolUse should hold two hooks; got %d (%v)", len(hs), hs)
344388
}
345-
// Stable order by ID (registry.go:28): auto-lint < auto-test.
389+
// Stable order by ID (registry.go:28): "auto-lint" < "auto-test".
346390
if hs[0].ID() != "auto-lint" || hs[1].ID() != "auto-test" {
347-
ids := []string{hs[0].ID(), hs[1].ID()}
348-
t.Errorf("expected {auto-lint, auto-test}; got %v", ids)
391+
t.Errorf("expected {auto-lint, auto-test}; got {%s, %s}", hs[0].ID(), hs[1].ID())
349392
}
350393
})
351394

352395
t.Run("Edit_fires_AutoLintHook", func(t *testing.T) {
353396
// Edit on a real .go file in a real workspace → bridge exercises
354-
// runLintCommands. We don't pin Verdict (gofmt/vet may or may not
355-
// be installed in CI) — only that the bridge fired and recorded
356-
// the right HookID.
397+
// runLintCommands. We don't pin Verdict (gofmt/vet may or may
398+
// not be installed in CI) — only that the bridge fired and
399+
// stamped the right HookID.
357400
tmp := t.TempDir()
358401
goFile := filepath.Join(tmp, "bridge.go")
359402
if err := os.WriteFile(goFile, []byte("package bridge\n"), 0o644); err != nil {
@@ -374,7 +417,7 @@ func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
374417
})
375418

376419
t.Run("Bash_does_not_fire_AutoLintHook", func(t *testing.T) {
377-
// isEditTool filters Bash → Allow with the canonical HookID.
420+
// isEditTool filters Bash → Allow with canonical HookID, no Message.
378421
d := auto.Run(context.Background(), hooklife.Event{
379422
Phase: hooklife.PostToolUse,
380423
Tool: "Bash",
@@ -392,8 +435,8 @@ func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
392435
})
393436

394437
t.Run("Edit_on_non_go_path_does_not_fire", func(t *testing.T) {
395-
// isEditTool accepts Edit, but the path check filters non-.go files
396-
// and *_test.go files. Verify both branches produce Allow.
438+
// isEditTool accepts Edit, but the path check filters non-.go
439+
// files and *_test.go files. Verify both branches produce Allow.
397440
for _, p := range []string{"README.md", "fixture_test.go"} {
398441
d := auto.Run(context.Background(), hooklife.Event{
399442
Phase: hooklife.PostToolUse,
@@ -410,7 +453,8 @@ func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
410453
})
411454

412455
t.Run("disabled_hook_short_circuits", func(t *testing.T) {
413-
// Enabled=false is the global off-switch — every Edit falls through.
456+
// Enabled=false is the global off-switch — every Edit falls
457+
// through with Allow and the canonical HookID.
414458
disabled := AutoLintHook{Enabled: false, Timeout: AutoLintDefaultTimeout}
415459
d := disabled.Run(context.Background(), hooklife.Event{
416460
Phase: hooklife.PostToolUse,
@@ -423,9 +467,9 @@ func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
423467
})
424468

425469
t.Run("legacy_sin_write_name_accepted", func(t *testing.T) {
426-
// isEditTool accepts the legacy sin_write/sin_edit names too — covers
427-
// the legacy post-listener + hooklife bridge agreeing on the same
428-
// filter so the two paths can coexist.
470+
// isEditTool accepts the legacy sin_write/sin_edit names too —
471+
// proves the legacy post-listener and hooklife bridge agree on
472+
// the same filter so the two paths can coexist.
429473
tmp := t.TempDir()
430474
goFile := filepath.Join(tmp, "legacy.go")
431475
if err := os.WriteFile(goFile, []byte("package legacy\n"), 0o644); err != nil {
@@ -442,7 +486,5 @@ func TestAutoHookLife_BridgeToHooklife(t *testing.T) {
442486
t.Errorf("%s bridge: HookID = %q, want %q", name, d.HookID, "auto-lint")
443487
}
444488
}
445-
_ = "trailing-comment-marker" // silence gofmt trim warnings if this ever moves
446-
_, _ = fmt.Fprintln
447489
})
448490
}

0 commit comments

Comments
 (0)