@hybridlabor-api/bdb-synapse 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. package/web/vite.config.ts +28 -0
@@ -0,0 +1,818 @@
1
+ package citymap
2
+
3
+ import (
4
+ "encoding/json"
5
+ "math"
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ "time"
12
+
13
+ "github.com/hybridlabor-api/bdb-synapse/internal/model"
14
+ )
15
+
16
+ func TestBuildIsDeterministic(t *testing.T) {
17
+ root := t.TempDir()
18
+ writeFile(t, root, "src/main.go", "package main\nfunc main() {}\n")
19
+ writeFile(t, root, "README.md", "# Demo\n")
20
+ runGit(t, root, "init")
21
+ runGit(t, root, "add", ".")
22
+
23
+ first, err := (Builder{}).Build(root, nil)
24
+ if err != nil {
25
+ t.Fatal(err)
26
+ }
27
+ second, err := (Builder{}).Build(root, nil)
28
+ if err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ first.Repo.GeneratedAt = ""
32
+ second.Repo.GeneratedAt = ""
33
+ a, _ := json.Marshal(first)
34
+ b, _ := json.Marshal(second)
35
+ if string(a) != string(b) {
36
+ t.Fatalf("citymap is not deterministic\nfirst=%s\nsecond=%s", a, b)
37
+ }
38
+ if len(first.Files) != 2 {
39
+ t.Fatalf("files = %d", len(first.Files))
40
+ }
41
+ if first.Files[0].Rect.W <= 0 || first.Files[0].Rect.D <= 0 {
42
+ t.Fatalf("empty rect: %#v", first.Files[0].Rect)
43
+ }
44
+ }
45
+
46
+ func TestBuildIncludesUntrackedFiles(t *testing.T) {
47
+ root := t.TempDir()
48
+ writeFile(t, root, "tracked.go", "package main\n")
49
+ runGit(t, root, "init")
50
+ runGit(t, root, "add", "tracked.go")
51
+ writeFile(t, root, "new.go", "package main\nfunc New() {}\n")
52
+
53
+ city, err := (Builder{}).Build(root, nil)
54
+ if err != nil {
55
+ t.Fatal(err)
56
+ }
57
+ paths := map[string]bool{}
58
+ for _, file := range city.Files {
59
+ paths[file.Path] = true
60
+ }
61
+ if !paths["tracked.go"] || !paths["new.go"] {
62
+ t.Fatalf("paths = %#v", paths)
63
+ }
64
+ }
65
+
66
+ func TestBuildIsDeterministicAcrossNestedDirectories(t *testing.T) {
67
+ root := t.TempDir()
68
+ for d := 0; d < 12; d++ {
69
+ for f := 0; f < 8; f++ {
70
+ rel := filepath.Join("pkg", string(rune('a'+d)), "sub", string(rune('a'+f))+".go")
71
+ writeFile(t, root, rel, strings.Repeat("package pkg\n", 1+d*f+f))
72
+ }
73
+ }
74
+ runGit(t, root, "init")
75
+ runGit(t, root, "add", ".")
76
+
77
+ var first []byte
78
+ for i := 0; i < 20; i++ {
79
+ city, err := (Builder{}).Build(root, nil)
80
+ if err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ city.Repo.GeneratedAt = ""
84
+ data, _ := json.Marshal(city)
85
+ if i == 0 {
86
+ first = data
87
+ continue
88
+ }
89
+ if string(data) != string(first) {
90
+ t.Fatalf("citymap changed on run %d\nfirst=%s\ncurrent=%s", i, first, data)
91
+ }
92
+ }
93
+ }
94
+
95
+ func TestBuildSkipsWeakMissingTargetsButKeepsStrongGhosts(t *testing.T) {
96
+ root := t.TempDir()
97
+ writeFile(t, root, "tracked.go", "package main\n")
98
+ runGit(t, root, "init")
99
+ runGit(t, root, "add", "tracked.go")
100
+
101
+ trace := &model.Trace{Events: []model.Event{{
102
+ Targets: []model.Target{
103
+ {Path: "missing-weak.go", Touch: "hit", Weak: true},
104
+ {Path: "missing-strong.go", Touch: "edit"},
105
+ },
106
+ }}}
107
+ city, err := (Builder{}).Build(root, trace)
108
+ if err != nil {
109
+ t.Fatal(err)
110
+ }
111
+ files := map[string]model.CityFile{}
112
+ for _, file := range city.Files {
113
+ files[file.Path] = file
114
+ }
115
+ if _, ok := files["missing-weak.go"]; ok {
116
+ t.Fatalf("weak missing target became a city file: %#v", files["missing-weak.go"])
117
+ }
118
+ if file, ok := files["missing-strong.go"]; !ok || !file.Ghost {
119
+ t.Fatalf("strong missing target did not become ghost: %#v", file)
120
+ }
121
+ }
122
+
123
+ func TestSquarifiedLayoutAvoidsExtremeAspectRatios(t *testing.T) {
124
+ root := t.TempDir()
125
+ for i := 0; i < 80; i++ {
126
+ writeFile(t, root, filepath.Join("pkg", "file"+string(rune('a'+i%26))+string(rune('a'+i/26))+".go"), "package pkg\nfunc X() {}\n")
127
+ }
128
+ runGit(t, root, "init")
129
+ runGit(t, root, "add", ".")
130
+
131
+ city, err := (Builder{}).Build(root, nil)
132
+ if err != nil {
133
+ t.Fatal(err)
134
+ }
135
+ maxRatio := 0.0
136
+ for _, file := range city.Files {
137
+ if file.Rect.W <= 0 || file.Rect.D <= 0 {
138
+ t.Fatalf("empty rect for %s: %#v", file.Path, file.Rect)
139
+ }
140
+ ratio := math.Max(file.Rect.W/file.Rect.D, file.Rect.D/file.Rect.W)
141
+ maxRatio = math.Max(maxRatio, ratio)
142
+ }
143
+ if maxRatio > 25 {
144
+ t.Fatalf("max aspect ratio = %f", maxRatio)
145
+ }
146
+ }
147
+
148
+ func TestLargeTextFileCountsLines(t *testing.T) {
149
+ root := t.TempDir()
150
+ const lines = 100000 // ~1.3MB, under maxLineCountBytes
151
+ writeFile(t, root, "big.go", strings.Repeat("package main\n", lines))
152
+
153
+ city, err := (Builder{}).Build(root, nil)
154
+ if err != nil {
155
+ t.Fatal(err)
156
+ }
157
+ if len(city.Files) != 1 {
158
+ t.Fatalf("files = %#v", city.Files)
159
+ }
160
+ if city.Files[0].Lines != lines {
161
+ t.Fatalf("lines = %d, want %d", city.Files[0].Lines, lines)
162
+ }
163
+ }
164
+
165
+ func TestOversizedFileSkipsLineCounting(t *testing.T) {
166
+ root := t.TempDir()
167
+ const lines = 700000 // ~9MB, over maxLineCountBytes
168
+ writeFile(t, root, "huge.go", strings.Repeat("package main\n", lines))
169
+
170
+ city, err := (Builder{}).Build(root, nil)
171
+ if err != nil {
172
+ t.Fatal(err)
173
+ }
174
+ if len(city.Files) != 1 {
175
+ t.Fatalf("files = %#v", city.Files)
176
+ }
177
+ if city.Files[0].Lines != 0 {
178
+ t.Fatalf("lines = %d, want 0 (skipped)", city.Files[0].Lines)
179
+ }
180
+ if city.Files[0].Rect.W <= 0 || city.Files[0].Rect.D <= 0 {
181
+ t.Fatalf("byte-weight fallback produced empty rect: %#v", city.Files[0].Rect)
182
+ }
183
+ }
184
+
185
+ func withMapFileCap(t *testing.T, cap int) {
186
+ t.Helper()
187
+ prev := maxMapFiles
188
+ maxMapFiles = cap
189
+ t.Cleanup(func() { maxMapFiles = prev })
190
+ }
191
+
192
+ func TestBuildCapsFilesLevelByLevelPreferringNonDot(t *testing.T) {
193
+ withMapFileCap(t, 6)
194
+ root := t.TempDir()
195
+ writeFile(t, root, "go.mod", "module demo\n") // marker → project mode
196
+ writeFile(t, root, "a.go", "package main\n")
197
+ writeFile(t, root, ".hidden.go", "package main\n")
198
+ for _, name := range []string{"c1.go", "c2.go", "c3.go", "c4.go"} {
199
+ writeFile(t, root, "sub/"+name, "package sub\n")
200
+ }
201
+ writeFile(t, root, "sub/deep/d1.go", "package deep\n")
202
+ writeFile(t, root, "sub/deep/d2.go", "package deep\n")
203
+
204
+ city, err := (Builder{}).Build(root, nil)
205
+ if err != nil {
206
+ t.Fatal(err)
207
+ }
208
+ if !city.Repo.Truncated {
209
+ t.Fatal("expected truncated map")
210
+ }
211
+ got := make([]string, 0, len(city.Files))
212
+ for _, file := range city.Files {
213
+ got = append(got, file.Path)
214
+ }
215
+ want := []string{".hidden.go", "a.go", "go.mod", "sub/c1.go", "sub/c2.go", "sub/c3.go"}
216
+ if strings.Join(got, ",") != strings.Join(want, ",") {
217
+ t.Fatalf("files = %v, want %v", got, want)
218
+ }
219
+ }
220
+
221
+ func TestWorkspaceModeMapsOnlyProjectSubtrees(t *testing.T) {
222
+ root := t.TempDir()
223
+ writeFile(t, root, "proj/go.mod", "module proj\n")
224
+ writeFile(t, root, "proj/main.go", "package main\n")
225
+ writeFile(t, root, "proj/src/x.go", "package src\n")
226
+ writeFile(t, root, "clutter/big.txt", "x\n")
227
+ writeFile(t, root, ".cache/junk.go", "package junk\n")
228
+ writeFile(t, root, "loose.txt", "note\n")
229
+
230
+ city, err := (Builder{}).Build(root, nil)
231
+ if err != nil {
232
+ t.Fatal(err)
233
+ }
234
+ paths := map[string]bool{}
235
+ for _, file := range city.Files {
236
+ paths[file.Path] = true
237
+ }
238
+ for _, want := range []string{"proj/go.mod", "proj/main.go", "proj/src/x.go", "loose.txt"} {
239
+ if !paths[want] {
240
+ t.Fatalf("missing %s in %v", want, paths)
241
+ }
242
+ }
243
+ for _, banned := range []string{"clutter/big.txt", ".cache/junk.go"} {
244
+ if paths[banned] {
245
+ t.Fatalf("workspace scan leaked %s", banned)
246
+ }
247
+ }
248
+ if city.Repo.Truncated {
249
+ t.Fatal("policy pruning must not mark the map truncated")
250
+ }
251
+ }
252
+
253
+ func TestWorkspaceModeTraceSeedsNonProjectDirs(t *testing.T) {
254
+ root := t.TempDir()
255
+ writeFile(t, root, "proj/go.mod", "module proj\n")
256
+ writeFile(t, root, "notes/a.md", "# a\n")
257
+ writeFile(t, root, "notes/b.md", "# b\n")
258
+
259
+ trace := &model.Trace{Events: []model.Event{{
260
+ Targets: []model.Target{{Path: "notes/a.md", Touch: "read"}},
261
+ }}}
262
+ city, err := (Builder{}).Build(root, trace)
263
+ if err != nil {
264
+ t.Fatal(err)
265
+ }
266
+ paths := map[string]bool{}
267
+ for _, file := range city.Files {
268
+ paths[file.Path] = true
269
+ }
270
+ if !paths["notes/a.md"] || !paths["notes/b.md"] {
271
+ t.Fatalf("touched dir not seeded: %v", paths)
272
+ }
273
+
274
+ city, err = (Builder{}).Build(root, nil)
275
+ if err != nil {
276
+ t.Fatal(err)
277
+ }
278
+ for _, file := range city.Files {
279
+ if strings.HasPrefix(file.Path, "notes/") {
280
+ t.Fatalf("untouched non-project dir mapped: %s", file.Path)
281
+ }
282
+ }
283
+ }
284
+
285
+ func TestWorkspaceModeGatesProtectedDirsUntilTouched(t *testing.T) {
286
+ root := t.TempDir()
287
+ writeFile(t, root, "proj/go.mod", "module proj\n")
288
+ writeFile(t, root, "Desktop/demo/go.mod", "module demo\n")
289
+ writeFile(t, root, "Desktop/demo/main.go", "package main\n")
290
+
291
+ city, err := (Builder{}).Build(root, nil)
292
+ if err != nil {
293
+ t.Fatal(err)
294
+ }
295
+ for _, file := range city.Files {
296
+ if strings.HasPrefix(file.Path, "Desktop/") {
297
+ t.Fatalf("protected dir entered without a touch: %s", file.Path)
298
+ }
299
+ }
300
+
301
+ trace := &model.Trace{Events: []model.Event{{
302
+ Targets: []model.Target{{Path: "Desktop/demo/main.go", Touch: "edit"}},
303
+ }}}
304
+ city, err = (Builder{}).Build(root, trace)
305
+ if err != nil {
306
+ t.Fatal(err)
307
+ }
308
+ found := false
309
+ for _, file := range city.Files {
310
+ if file.Path == "Desktop/demo/main.go" && !file.Ghost {
311
+ found = true
312
+ }
313
+ }
314
+ if !found {
315
+ t.Fatalf("touched protected dir not mapped: %#v", city.Files)
316
+ }
317
+ }
318
+
319
+ func TestGitListOverflowTruncates(t *testing.T) {
320
+ prev := maxGitListPaths
321
+ maxGitListPaths = 3
322
+ t.Cleanup(func() { maxGitListPaths = prev })
323
+
324
+ root := t.TempDir()
325
+ for _, name := range []string{"a.go", "b.go", "c.go", "d.go", "e.go"} {
326
+ writeFile(t, root, name, "package main\n")
327
+ }
328
+ runGit(t, root, "init")
329
+ runGit(t, root, "add", ".")
330
+
331
+ city, err := (Builder{}).Build(root, nil)
332
+ if err != nil {
333
+ t.Fatal(err)
334
+ }
335
+ if len(city.Files) != 3 {
336
+ t.Fatalf("files = %d, want 3", len(city.Files))
337
+ }
338
+ if !city.Repo.Truncated {
339
+ t.Fatal("expected truncated map")
340
+ }
341
+ }
342
+
343
+ func TestDeletedTrackedFilesDoNotConsumeBudget(t *testing.T) {
344
+ withMapFileCap(t, 3)
345
+ root := t.TempDir()
346
+ for _, name := range []string{"a.go", "b.go", "c.go", "z-live.go"} {
347
+ writeFile(t, root, name, "package main\n")
348
+ }
349
+ runGit(t, root, "init")
350
+ runGit(t, root, "add", ".")
351
+ for _, name := range []string{"a.go", "b.go", "c.go"} {
352
+ if err := os.Remove(filepath.Join(root, name)); err != nil {
353
+ t.Fatal(err)
354
+ }
355
+ }
356
+
357
+ city, err := (Builder{}).Build(root, nil)
358
+ if err != nil {
359
+ t.Fatal(err)
360
+ }
361
+ if len(city.Files) != 1 || city.Files[0].Path != "z-live.go" {
362
+ t.Fatalf("files = %#v, want just z-live.go", city.Files)
363
+ }
364
+ if city.Repo.Truncated {
365
+ t.Fatal("map holds everything that exists — must not be truncated")
366
+ }
367
+ }
368
+
369
+ func TestTouchedFileBeyondWalkHorizonStaysReal(t *testing.T) {
370
+ withMapFileCap(t, 2)
371
+ root := t.TempDir()
372
+ writeFile(t, root, "go.mod", "module m\n")
373
+ writeFile(t, root, "a.go", "package main\n")
374
+ writeFile(t, root, "sub/important.go", "package sub\n")
375
+
376
+ trace := &model.Trace{Events: []model.Event{{
377
+ Targets: []model.Target{{Path: "sub/important.go", Touch: "edit"}},
378
+ }}}
379
+ city, err := (Builder{}).Build(root, trace)
380
+ if err != nil {
381
+ t.Fatal(err)
382
+ }
383
+ if len(city.Files) != 2 {
384
+ t.Fatalf("files = %#v, want 2 (budget)", city.Files)
385
+ }
386
+ var important *model.CityFile
387
+ for i := range city.Files {
388
+ if city.Files[i].Path == "sub/important.go" {
389
+ important = &city.Files[i]
390
+ }
391
+ }
392
+ if important == nil || important.Ghost || important.Lines == 0 || important.Bytes == 0 {
393
+ t.Fatalf("touched file beyond walk horizon mishandled: %#v", important)
394
+ }
395
+ if !city.Repo.Truncated {
396
+ t.Fatal("expected truncated map")
397
+ }
398
+ }
399
+
400
+ func TestGhostsShareTheMapBudget(t *testing.T) {
401
+ withMapFileCap(t, 2)
402
+ root := t.TempDir()
403
+ writeFile(t, root, "go.mod", "module m\n")
404
+ writeFile(t, root, "a.go", "package main\n")
405
+
406
+ trace := &model.Trace{Events: []model.Event{{
407
+ Targets: []model.Target{
408
+ {Path: "missing1.go", Touch: "edit"},
409
+ {Path: "missing2.go", Touch: "edit"},
410
+ {Path: "missing3.go", Touch: "edit"},
411
+ },
412
+ }}}
413
+ city, err := (Builder{}).Build(root, trace)
414
+ if err != nil {
415
+ t.Fatal(err)
416
+ }
417
+ if len(city.Files) != 2 {
418
+ t.Fatalf("files = %d, want the cap of 2 (ghosts included)", len(city.Files))
419
+ }
420
+ for _, file := range city.Files {
421
+ if !file.Ghost {
422
+ t.Fatalf("trace-derived entries outrank untouched files, got %#v", file)
423
+ }
424
+ }
425
+ if !city.Repo.Truncated {
426
+ t.Fatal("dropped targets must mark the map truncated")
427
+ }
428
+ }
429
+
430
+ func TestRealTouchedOutranksGhosts(t *testing.T) {
431
+ withMapFileCap(t, 1)
432
+ root := t.TempDir()
433
+ writeFile(t, root, "go.mod", "module m\n")
434
+ writeFile(t, root, "real.go", "package main\n")
435
+
436
+ trace := &model.Trace{Events: []model.Event{{
437
+ Targets: []model.Target{
438
+ {Path: "missing.go", Touch: "edit"},
439
+ {Path: "real.go", Touch: "edit"},
440
+ },
441
+ }}}
442
+ city, err := (Builder{}).Build(root, trace)
443
+ if err != nil {
444
+ t.Fatal(err)
445
+ }
446
+ if len(city.Files) != 1 || city.Files[0].Path != "real.go" || city.Files[0].Ghost {
447
+ t.Fatalf("existing touched file must outrank ghosts, got %#v", city.Files)
448
+ }
449
+ if !city.Repo.Truncated {
450
+ t.Fatal("dropped ghost must mark the map truncated")
451
+ }
452
+ }
453
+
454
+ func TestWeakMissDoesNotMaskLaterStrongTarget(t *testing.T) {
455
+ root := t.TempDir()
456
+ writeFile(t, root, "tracked.go", "package main\n")
457
+ runGit(t, root, "init")
458
+ runGit(t, root, "add", ".")
459
+
460
+ trace := &model.Trace{Events: []model.Event{{
461
+ Targets: []model.Target{
462
+ {Path: "x.go", Touch: "hit", Weak: true},
463
+ {Path: "x.go", Touch: "edit"},
464
+ },
465
+ }}}
466
+ city, err := (Builder{}).Build(root, trace)
467
+ if err != nil {
468
+ t.Fatal(err)
469
+ }
470
+ found := false
471
+ for _, file := range city.Files {
472
+ if file.Path == "x.go" && file.Ghost {
473
+ found = true
474
+ }
475
+ }
476
+ if !found {
477
+ t.Fatalf("strong miss must become ghost despite an earlier weak miss: %#v", city.Files)
478
+ }
479
+ }
480
+
481
+ func TestNoFalseTruncatedWhenEverythingFits(t *testing.T) {
482
+ withMapFileCap(t, 1)
483
+ root := t.TempDir()
484
+ writeFile(t, root, "only.go", "package main\n")
485
+ runGit(t, root, "init")
486
+ runGit(t, root, "add", ".")
487
+
488
+ trace := &model.Trace{Events: []model.Event{{
489
+ Targets: []model.Target{{Path: "only.go", Touch: "edit"}},
490
+ }}}
491
+ city, err := (Builder{}).Build(root, trace)
492
+ if err != nil {
493
+ t.Fatal(err)
494
+ }
495
+ if len(city.Files) != 1 || city.Files[0].Path != "only.go" {
496
+ t.Fatalf("files = %#v", city.Files)
497
+ }
498
+ if city.Repo.Truncated {
499
+ t.Fatal("nothing was dropped — truncated must stay false")
500
+ }
501
+ }
502
+
503
+ func TestNonRegularEntriesArePolicySkipsNotTruncation(t *testing.T) {
504
+ root := t.TempDir()
505
+ writeFile(t, root, "go.mod", "module m\n")
506
+ writeFile(t, root, "a.go", "package main\n")
507
+ writeFile(t, root, "sub/real.go", "package sub\n")
508
+ // a symlink resolving to a directory is collected by the walk but can
509
+ // never become a building
510
+ if err := os.Symlink(filepath.Join(root, "sub"), filepath.Join(root, "link")); err != nil {
511
+ t.Fatal(err)
512
+ }
513
+
514
+ city, err := (Builder{}).Build(root, nil)
515
+ if err != nil {
516
+ t.Fatal(err)
517
+ }
518
+ for _, file := range city.Files {
519
+ if file.Path == "link" {
520
+ t.Fatalf("symlinked directory became a building: %#v", file)
521
+ }
522
+ }
523
+ if city.Repo.Truncated {
524
+ t.Fatal("policy-skipped non-regular entries must not mark the map partial")
525
+ }
526
+
527
+ // same rule on the trace path: not gone, so no ghost — and no badge
528
+ trace := &model.Trace{Events: []model.Event{{
529
+ Targets: []model.Target{{Path: "link", Touch: "edit"}},
530
+ }}}
531
+ city, err = (Builder{}).Build(root, trace)
532
+ if err != nil {
533
+ t.Fatal(err)
534
+ }
535
+ for _, file := range city.Files {
536
+ if file.Path == "link" {
537
+ t.Fatalf("touched non-regular entry entered the map: %#v", file)
538
+ }
539
+ }
540
+ if city.Repo.Truncated {
541
+ t.Fatal("touched non-regular entries are policy skips too")
542
+ }
543
+ }
544
+
545
+ func TestMarkerlessWorkspaceMapsOnlyLooseFiles(t *testing.T) {
546
+ root := t.TempDir()
547
+ writeFile(t, root, "loose.txt", "note\n")
548
+ writeFile(t, root, "clutter/nested.txt", "x\n")
549
+
550
+ city, err := (Builder{}).Build(root, nil)
551
+ if err != nil {
552
+ t.Fatal(err)
553
+ }
554
+ paths := map[string]bool{}
555
+ for _, file := range city.Files {
556
+ paths[file.Path] = true
557
+ }
558
+ if !paths["loose.txt"] {
559
+ t.Fatalf("root loose file missing: %v", paths)
560
+ }
561
+ if paths["clutter/nested.txt"] {
562
+ t.Fatal("markerless workspace must not map nested clutter")
563
+ }
564
+ if city.Repo.Truncated {
565
+ t.Fatal("policy pruning must not mark the map truncated")
566
+ }
567
+
568
+ // a touched file still pulls its directory in via the seed mechanism
569
+ trace := &model.Trace{Events: []model.Event{{
570
+ Targets: []model.Target{{Path: "clutter/nested.txt", Touch: "read"}},
571
+ }}}
572
+ city, err = (Builder{}).Build(root, trace)
573
+ if err != nil {
574
+ t.Fatal(err)
575
+ }
576
+ found := false
577
+ for _, file := range city.Files {
578
+ if file.Path == "clutter/nested.txt" && !file.Ghost {
579
+ found = true
580
+ }
581
+ }
582
+ if !found {
583
+ t.Fatalf("touched clutter must be seeded into the map: %#v", city.Files)
584
+ }
585
+ }
586
+
587
+ func TestProbeReadFailureMarksTruncated(t *testing.T) {
588
+ if os.Geteuid() == 0 {
589
+ t.Skip("running as root — permissions are not enforced")
590
+ }
591
+ root := t.TempDir()
592
+ writeFile(t, root, "proj/go.mod", "module proj\n")
593
+ writeFile(t, root, "proj/main.go", "package main\n")
594
+ writeFile(t, root, "locked/hidden/go.mod", "module hidden\n")
595
+ locked := filepath.Join(root, "locked")
596
+ if err := os.Chmod(locked, 0o000); err != nil {
597
+ t.Fatal(err)
598
+ }
599
+ t.Cleanup(func() { _ = os.Chmod(locked, 0o755) })
600
+
601
+ city, err := (Builder{}).Build(root, nil)
602
+ if err != nil {
603
+ t.Fatal(err)
604
+ }
605
+ paths := map[string]bool{}
606
+ for _, file := range city.Files {
607
+ paths[file.Path] = true
608
+ }
609
+ if !paths["proj/main.go"] {
610
+ t.Fatalf("visible project missing: %v", paths)
611
+ }
612
+ if !city.Repo.Truncated {
613
+ t.Fatal("an unreadable dir may hide projects — the map must be partial")
614
+ }
615
+ }
616
+
617
+ func TestStaleTraceSeedsNeitherStallNorTruncate(t *testing.T) {
618
+ root := t.TempDir()
619
+ writeFile(t, root, "proj/go.mod", "module proj\n")
620
+ writeFile(t, root, "loose.txt", "note\n")
621
+
622
+ trace := &model.Trace{Events: []model.Event{{
623
+ Targets: []model.Target{
624
+ {Path: "gone/sub/file.go", Touch: "edit"}, // parent dir never existed
625
+ {Path: "loose.txt/x.go", Touch: "edit"}, // parent is a file, not a dir
626
+ },
627
+ }}}
628
+ city, err := (Builder{}).Build(root, trace)
629
+ if err != nil {
630
+ t.Fatal(err)
631
+ }
632
+ ghosts := map[string]bool{}
633
+ for _, file := range city.Files {
634
+ if file.Ghost {
635
+ ghosts[file.Path] = true
636
+ }
637
+ }
638
+ if !ghosts["gone/sub/file.go"] || !ghosts["loose.txt/x.go"] {
639
+ t.Fatalf("confirmed-gone strong targets must ghost: %v", ghosts)
640
+ }
641
+ if city.Repo.Truncated {
642
+ t.Fatal("stale seeds lose nothing — truncated must stay false")
643
+ }
644
+ }
645
+
646
+ // TestAllFilesystemAccessIsBounded pins the invariant that ended four review
647
+ // rounds of hang bugs: every filesystem or subprocess call in builder.go
648
+ // lives inside a bounded helper. New code must route through
649
+ // readDirBounded, inspectFileBounded (inspectFile and its readers), or the
650
+ // git helpers — or extend this list consciously.
651
+ func TestAllFilesystemAccessIsBounded(t *testing.T) {
652
+ src, err := os.ReadFile("builder.go")
653
+ if err != nil {
654
+ t.Fatal(err)
655
+ }
656
+ allowed := map[string][]string{
657
+ "os.ReadDir(": {"readDirBounded("},
658
+ "os.Stat(": {"inspectFile("},
659
+ "os.Open(": {"isBinaryLike(", "countLines("},
660
+ "exec.CommandContext(": {"gitListFiles(", "gitOutput("},
661
+ }
662
+ banned := []string{"exec.Command(", "filepath.Walk(", "filepath.WalkDir(", "os.Lstat(", "os.ReadFile(", "os.Create(", "ioutil."}
663
+ current := ""
664
+ for i, line := range strings.Split(string(src), "\n") {
665
+ if strings.HasPrefix(line, "func ") {
666
+ current = line
667
+ }
668
+ for pattern, owners := range allowed {
669
+ if !strings.Contains(line, pattern) {
670
+ continue
671
+ }
672
+ ok := false
673
+ for _, owner := range owners {
674
+ if strings.Contains(current, owner) {
675
+ ok = true
676
+ }
677
+ }
678
+ if !ok {
679
+ t.Errorf("builder.go:%d: %s outside its bounded owners %v (in %q)", i+1, pattern, owners, current)
680
+ }
681
+ }
682
+ for _, pattern := range banned {
683
+ if strings.Contains(line, pattern) {
684
+ t.Errorf("builder.go:%d: banned unbounded call %s — route through the bounded helpers", i+1, pattern)
685
+ }
686
+ }
687
+ }
688
+ }
689
+
690
+ func TestGitCallsAreBounded(t *testing.T) {
691
+ prev := gitTimeout
692
+ gitTimeout = 200 * time.Millisecond
693
+ t.Cleanup(func() { gitTimeout = prev })
694
+
695
+ fake := t.TempDir()
696
+ script := "#!/bin/sh\ncase \"$3\" in\nls-files) exit 1 ;;\nesac\nexec sleep 5\n"
697
+ if err := os.WriteFile(filepath.Join(fake, "git"), []byte(script), 0o755); err != nil {
698
+ t.Fatal(err)
699
+ }
700
+ t.Setenv("PATH", fake+string(os.PathListSeparator)+os.Getenv("PATH"))
701
+
702
+ root := t.TempDir()
703
+ writeFile(t, root, "go.mod", "module m\n")
704
+ writeFile(t, root, "a.go", "package main\n")
705
+
706
+ start := time.Now()
707
+ city, err := (Builder{}).Build(root, nil)
708
+ if err != nil {
709
+ t.Fatal(err)
710
+ }
711
+ if elapsed := time.Since(start); elapsed > 3*time.Second {
712
+ t.Fatalf("build took %v against a hanging git", elapsed)
713
+ }
714
+ if city.Repo.Commit != "" || city.Repo.Dirty {
715
+ t.Fatalf("repo state must degrade to empty on timeout: %#v", city.Repo)
716
+ }
717
+ }
718
+
719
+ func TestWalkSkipsUnreadableDirs(t *testing.T) {
720
+ if os.Geteuid() == 0 {
721
+ t.Skip("running as root — permissions are not enforced")
722
+ }
723
+ root := t.TempDir()
724
+ writeFile(t, root, "go.mod", "module demo\n")
725
+ writeFile(t, root, "ok/a.go", "package ok\n")
726
+ writeFile(t, root, "locked/b.go", "package locked\n")
727
+ locked := filepath.Join(root, "locked")
728
+ if err := os.Chmod(locked, 0o000); err != nil {
729
+ t.Fatal(err)
730
+ }
731
+ t.Cleanup(func() { _ = os.Chmod(locked, 0o755) })
732
+
733
+ city, err := (Builder{}).Build(root, nil)
734
+ if err != nil {
735
+ t.Fatal(err)
736
+ }
737
+ paths := map[string]bool{}
738
+ for _, file := range city.Files {
739
+ paths[file.Path] = true
740
+ }
741
+ if !paths["ok/a.go"] {
742
+ t.Fatalf("readable file missing: %v", paths)
743
+ }
744
+ if paths["locked/b.go"] {
745
+ t.Fatal("unreadable dir contents leaked into the map")
746
+ }
747
+ if !city.Repo.Truncated {
748
+ t.Fatal("losing a subtree to a read failure must mark the map partial")
749
+ }
750
+ }
751
+
752
+ func TestInspectFailureDoesNotGhost(t *testing.T) {
753
+ prev := inspectTimeout
754
+ inspectTimeout = time.Nanosecond
755
+ t.Cleanup(func() { inspectTimeout = prev })
756
+
757
+ root := t.TempDir()
758
+ writeFile(t, root, "go.mod", "module m\n")
759
+ writeFile(t, root, "real.go", "package main\n")
760
+ trace := &model.Trace{Events: []model.Event{{
761
+ Targets: []model.Target{{Path: "real.go", Touch: "edit"}},
762
+ }}}
763
+ city, err := (Builder{}).Build(root, trace)
764
+ if err != nil {
765
+ t.Fatal(err)
766
+ }
767
+ for _, file := range city.Files {
768
+ if file.Ghost {
769
+ t.Fatalf("unproven absence became ghost: %#v", file)
770
+ }
771
+ }
772
+ if !city.Repo.Truncated {
773
+ t.Fatal("inspect failures lose content — the map must be partial")
774
+ }
775
+ }
776
+
777
+ func TestDeletedTailCandidateDoesNotMarkTruncated(t *testing.T) {
778
+ withMapFileCap(t, 1)
779
+ root := t.TempDir()
780
+ writeFile(t, root, "a-live.go", "package main\n")
781
+ writeFile(t, root, "z-deleted.go", "package main\n")
782
+ runGit(t, root, "init")
783
+ runGit(t, root, "add", ".")
784
+ if err := os.Remove(filepath.Join(root, "z-deleted.go")); err != nil {
785
+ t.Fatal(err)
786
+ }
787
+
788
+ city, err := (Builder{}).Build(root, nil)
789
+ if err != nil {
790
+ t.Fatal(err)
791
+ }
792
+ if len(city.Files) != 1 || city.Files[0].Path != "a-live.go" {
793
+ t.Fatalf("files = %#v", city.Files)
794
+ }
795
+ if city.Repo.Truncated {
796
+ t.Fatal("every mappable file is on the map — truncated must stay false")
797
+ }
798
+ }
799
+
800
+ func writeFile(t *testing.T, root, rel, content string) {
801
+ t.Helper()
802
+ path := filepath.Join(root, filepath.FromSlash(rel))
803
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
804
+ t.Fatal(err)
805
+ }
806
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
807
+ t.Fatal(err)
808
+ }
809
+ }
810
+
811
+ func runGit(t *testing.T, root string, args ...string) {
812
+ t.Helper()
813
+ cmd := exec.Command("git", args...)
814
+ cmd.Dir = root
815
+ if out, err := cmd.CombinedOutput(); err != nil {
816
+ t.Fatalf("git %v: %v\n%s", args, err, out)
817
+ }
818
+ }