@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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/synapse +0 -0
- package/cmd/rubriceval/main.go +308 -0
- package/cmd/synapse/main.go +280 -0
- package/cmd/synapse/main_test.go +16 -0
- package/go.mod +7 -0
- package/go.sum +6 -0
- package/internal/adapter/adapter.go +1117 -0
- package/internal/adapter/adapter_test.go +518 -0
- package/internal/adapter/agy/adapter.go +193 -0
- package/internal/adapter/claudecode/adapter.go +415 -0
- package/internal/adapter/claudecode/adapter_test.go +260 -0
- package/internal/adapter/claudecode/agents.go +387 -0
- package/internal/adapter/claudecode/agents_test.go +480 -0
- package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
- package/internal/adapter/codex/adapter.go +919 -0
- package/internal/adapter/codex/adapter_test.go +920 -0
- package/internal/adapter/codex/agents.go +401 -0
- package/internal/adapter/codex/agents_test.go +610 -0
- package/internal/adapter/codex/summary_inputs_test.go +20 -0
- package/internal/adapter/pi/adapter.go +483 -0
- package/internal/adapter/pi/adapter_test.go +517 -0
- package/internal/citymap/builder.go +1124 -0
- package/internal/citymap/builder_test.go +818 -0
- package/internal/judge/cache.go +180 -0
- package/internal/judge/cli.go +240 -0
- package/internal/judge/cli_test.go +68 -0
- package/internal/judge/fresh_summary_test.go +37 -0
- package/internal/judge/input.go +233 -0
- package/internal/judge/judge.go +416 -0
- package/internal/judge/judge_test.go +288 -0
- package/internal/judge/prompt.go +129 -0
- package/internal/judge/rubric.go +275 -0
- package/internal/judge/rubric_test.go +642 -0
- package/internal/model/agent.go +63 -0
- package/internal/model/agent_schema_test.go +86 -0
- package/internal/model/agent_test.go +64 -0
- package/internal/model/model.go +175 -0
- package/internal/model/report.go +166 -0
- package/internal/model/stats.go +151 -0
- package/internal/model/stats_test.go +89 -0
- package/internal/model/trace_schema_test.go +67 -0
- package/internal/server/analyze.go +286 -0
- package/internal/server/analyze_test.go +297 -0
- package/internal/server/codex_index_test.go +40 -0
- package/internal/server/hardening_test.go +147 -0
- package/internal/server/reportindex.go +91 -0
- package/internal/server/reportindex_test.go +116 -0
- package/internal/server/server.go +1099 -0
- package/internal/server/server_test.go +1389 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +3 -0
- package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
- package/internal/server/static/index.html +26 -0
- package/internal/server/tracestore.go +173 -0
- package/internal/server/tracestore_test.go +51 -0
- package/internal/textutil/truncate.go +30 -0
- package/internal/textutil/truncate_test.go +39 -0
- package/package.json +35 -0
- package/web/e2e/agent-lens.spec.ts +688 -0
- package/web/index.html +23 -0
- package/web/package-lock.json +1933 -0
- package/web/package.json +33 -0
- package/web/playwright.config.ts +24 -0
- package/web/src/App.tsx +876 -0
- package/web/src/api/client.ts +74 -0
- package/web/src/main.tsx +12 -0
- package/web/src/playback/recorder.ts +160 -0
- package/web/src/playback/reducer.ts +91 -0
- package/web/src/scene/CityScene.tsx +638 -0
- package/web/src/scene/TreeScene.tsx +656 -0
- package/web/src/scene/dirLabels.ts +145 -0
- package/web/src/scene/sceneUtils.ts +144 -0
- package/web/src/scene/textures.ts +60 -0
- package/web/src/scene/trail.ts +79 -0
- package/web/src/scene/treeLayout.ts +169 -0
- package/web/src/state/filters.ts +40 -0
- package/web/src/state/store.ts +83 -0
- package/web/src/styles.css +2565 -0
- package/web/src/types.ts +315 -0
- package/web/src/ui/AgentsPanel.tsx +376 -0
- package/web/src/ui/Dock.tsx +104 -0
- package/web/src/ui/Hud.tsx +335 -0
- package/web/src/ui/Inspector.tsx +107 -0
- package/web/src/ui/LogoMark.tsx +38 -0
- package/web/src/ui/ReportPanel.tsx +491 -0
- package/web/src/ui/SessionRail.tsx +316 -0
- package/web/src/ui/Timeline.tsx +458 -0
- package/web/src/ui/ViewPanel.tsx +45 -0
- package/web/src/ui/shortcuts.ts +4 -0
- package/web/tsconfig.json +21 -0
- 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
|
+
}
|