Repository navigation
Expand file tree
/
Copy pathaudit_format_script_test.go
More file actions
709 lines (669 loc) · 25.9 KB
/
Copy pathaudit_format_script_test.go
File metadata and controls
709 lines (669 loc) · 25.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
//go:build linux
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/mjacobs/agy-reader/internal/cli"
"github.com/mjacobs/agy-reader/internal/completeness"
"github.com/mjacobs/agy-reader/internal/discovery"
"github.com/mjacobs/agy-reader/internal/testfixture"
)
const fakeShapeFingerprint = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
type auditFixture struct {
repo string
root string
script string
sidecar string
compatFile string
binDir string
}
func newAuditFixture(t *testing.T) *auditFixture {
t.Helper()
repo := t.TempDir()
scriptDir := filepath.Join(repo, "skills", "agy-format-audit", "scripts")
if err := os.MkdirAll(scriptDir, 0o755); err != nil {
t.Fatal(err)
}
scriptData, err := os.ReadFile(filepath.Join("skills", "agy-format-audit", "scripts", "audit_format.sh"))
if err != nil {
t.Fatal(err)
}
script := filepath.Join(scriptDir, "audit_format.sh")
if err := os.WriteFile(script, scriptData, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "go.mod"), []byte("module auditfixture\n\ngo 1.24\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(repo, "internal", "doctor"), 0o755); err != nil {
t.Fatal(err)
}
compatFile := filepath.Join(repo, "COMPATIBILITY.md")
if err := os.WriteFile(compatFile, []byte("sentinel compatibility record\n"), 0o644); err != nil {
t.Fatal(err)
}
runCommand(t, repo, "git", "init", "-q")
root := filepath.Join(repo, "agy-root")
conversations := filepath.Join(root, "conversations")
if err := os.MkdirAll(conversations, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "cli.log"), nil, 0o600); err != nil {
t.Fatal(err)
}
dbPath := filepath.Join(conversations, "11111111-1111-1111-1111-111111111111.db")
if err := os.WriteFile(dbPath, nil, 0o600); err != nil {
t.Fatal(err)
}
binDir := filepath.Join(repo, "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatal(err)
}
writeExecutable(t, filepath.Join(binDir, "agy"), `#!/usr/bin/env bash
case "${1:-}" in
--version) echo 9.9.9 ;;
changelog) printf '9.9.9:\nfixture release\n' ;;
*) exit 1 ;;
esac
`)
writeExecutable(t, filepath.Join(binDir, "sqlite3"), `#!/usr/bin/env bash
query="${2:-}"
case "$query" in
"PRAGMA user_version;") echo 1 ;;
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
printf '%s\n' battle_mode_infos executor_metadata gen_metadata parent_references steps trajectory_meta trajectory_metadata_blob
;;
"SELECT name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY name;")
printf '%s\n' idx_steps_status idx_steps_step_type
;;
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY type, name;")
printf '%s\n' 'CREATE TABLE steps (status TEXT, step_type TEXT)' 'CREATE INDEX idx_steps_status ON steps(status)' 'CREATE INDEX idx_steps_step_type ON steps(step_type)'
;;
*) exit 1 ;;
esac
`)
writeExecutable(t, filepath.Join(binDir, "sleep"), `#!/usr/bin/env bash
set -eu
if [ -n "${AUDIT_TEST_SLEEP_ENTERED:-}" ]; then
: > "$AUDIT_TEST_SLEEP_ENTERED"
while [ ! -e "$AUDIT_TEST_SLEEP_RELEASE" ]; do
/bin/sleep 0.01
done
exit 0
fi
exec /bin/sleep "$@"
`)
writeExecutable(t, filepath.Join(binDir, "go"), `#!/usr/bin/env bash
set -eu
case "${1:-}" in
build)
shift
output=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-o" ]; then
output="$2"
break
fi
shift
done
test -n "$output"
if [ -n "${AUDIT_TEST_READER_BIN:-}" ]; then
# Run the real agy-reader commands through the test binary.
printf '%s\n' '#!/usr/bin/env bash' 'AGY_READER_TEST_AS_CLI=1 exec "$AUDIT_TEST_READER_BIN" "$@"' > "$output"
else
cat > "$output" <<'FAKE'
#!/usr/bin/env bash
case "${1:-}" in
audit-qualify)
echo "reader sync: fixture ${AUDIT_TEST_QUALIFY_RC:-0}"
exit "${AUDIT_TEST_QUALIFY_RC:-0}"
;;
*) echo `+fakeShapeFingerprint+` ;;
esac
FAKE
fi
chmod +x "$output"
;;
test)
if [ -n "${AUDIT_TEST_GO_ENV_LOG:-}" ]; then
printf 'url=%s token=%s qualify=%s corpus=%s wait=%s\n' "${ANTIGRAVITY_DAEMON_URL-unset}" "${ANTIGRAVITY_CSRF_TOKEN-unset}" \
"${AGY_QUALIFY_SESSIONS-unset}" "${AGY_SIDECAR_CORPUS-unset}" "${AGY_SIDECAR_WAIT_SECONDS-unset}" > "$AUDIT_TEST_GO_ENV_LOG"
fi
if [ -n "${AUDIT_TEST_GO_FAIL:-}" ]; then
echo "--- FAIL: TestFixtureDiagnostic"
echo " fixture failure detail"
exit 1
fi
;;
*) exit 1 ;;
esac
`)
return &auditFixture{
repo: repo,
root: root,
script: script,
sidecar: filepath.Join(conversations, "11111111-1111-1111-1111-111111111111.trajectory.json"),
compatFile: compatFile,
binDir: binDir,
}
}
func TestAuditWaitsForNewestSidecar(t *testing.T) {
fixture := newAuditFixture(t)
sleepEntered := filepath.Join(fixture.repo, "sleep-entered")
sleepRelease := filepath.Join(fixture.repo, "sleep-release")
written := make(chan error, 1)
go func() {
var result error
defer func() {
if releaseErr := os.WriteFile(sleepRelease, nil, 0o600); result == nil {
result = releaseErr
}
written <- result
}()
if result = waitForPath(sleepEntered, 5*time.Second); result != nil {
return
}
result = os.WriteFile(fixture.sidecar, []byte("{\"steps\":[]}\n"), 0o600)
}()
output, err := fixture.runWithOverrides("3", map[string]string{
"AUDIT_TEST_SLEEP_ENTERED": sleepEntered,
"AUDIT_TEST_SLEEP_RELEASE": sleepRelease,
})
if writeErr := <-written; writeErr != nil {
t.Fatal(writeErr)
}
if err != nil {
t.Fatalf("audit failed: %v\n%s", err, output)
}
for _, want := range []string{
"waiting up to 3s for agy-reader's watcher",
"Sidecar appeared after",
"Shape fingerprint: " + fakeShapeFingerprint,
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
}
func waitForPath(path string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for {
if _, err := os.Stat(path); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
if time.Now().After(deadline) {
return fmt.Errorf("timed out waiting for %s", path)
}
time.Sleep(10 * time.Millisecond)
}
}
func TestAuditReportsTrueStepsCount(t *testing.T) {
if _, err := exec.LookPath("jq"); err != nil {
t.Skip("jq not installed; audit_format.sh falls back to step count unavailable")
}
fixture := newAuditFixture(t)
// Nested "type" keys (in step details and in unrelated metadata) must not
// inflate the reported count: the steps array here has 3 entries but
// contains 7 "type" keys in total.
sidecar := `{
"steps": [
{"type": "a", "detail": {"type": "nested1"}},
{"type": "b", "detail": {"type": "nested2"}},
{"type": "c"}
],
"metadata": {"type": "session", "extra": {"type": "meta2"}}
}`
if err := os.WriteFile(fixture.sidecar, []byte(sidecar), 0o600); err != nil {
t.Fatal(err)
}
output, err := fixture.run("0")
if err != nil {
t.Fatalf("audit failed: %v\n%s", err, output)
}
if !strings.Contains(output, "(3 steps detected)") {
t.Fatalf("output does not report the true steps array length (3):\n%s", output)
}
if strings.Contains(output, "(7 steps detected)") {
t.Fatalf("output counted every \"type\" key instead of steps array entries:\n%s", output)
}
}
func TestAuditRecordFailsClosedWithoutSidecarShape(t *testing.T) {
fixture := newAuditFixture(t)
output, err := fixture.run("0", "--record")
if err == nil {
t.Fatalf("audit unexpectedly recorded without a sidecar shape:\n%s", output)
}
if !strings.Contains(output, "Audit did not fully pass; no compatibility record produced.") {
t.Fatalf("output missing fail-closed diagnostic:\n%s", output)
}
content, readErr := os.ReadFile(fixture.compatFile)
if readErr != nil {
t.Fatal(readErr)
}
if got, want := string(content), "sentinel compatibility record\n"; got != want {
t.Fatalf("compatibility record changed: got %q want %q", got, want)
}
}
func TestAuditExplicitCorpusDoesNotWaitForNewestSidecar(t *testing.T) {
fixture := newAuditFixture(t)
corpus := filepath.Join(fixture.repo, "swept-corpus")
if err := os.MkdirAll(corpus, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(corpus, "fixture.trajectory.json"), []byte("{\"steps\":[]}\n"), 0o600); err != nil {
t.Fatal(err)
}
output, err := fixture.runWithOverrides("3", map[string]string{
"AGY_SIDECAR_CORPUS": corpus,
}, "--corpus-swept")
if err != nil {
t.Fatalf("audit failed: %v\n%s", err, output)
}
if strings.Contains(output, "waiting up to") {
t.Fatalf("explicit corpus audit waited on the live newest session:\n%s", output)
}
for _, want := range []string{
"configured corpus will be audited instead",
"Shape fingerprint: " + fakeShapeFingerprint,
"scope: explicit-version-scoped",
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
}
func (f *auditFixture) run(waitSeconds string, args ...string) (string, error) {
return f.runWithOverrides(waitSeconds, nil, args...)
}
func (f *auditFixture) runWithOverrides(waitSeconds string, overrides map[string]string, args ...string) (string, error) {
cmd := exec.Command("bash", append([]string{f.script}, args...)...)
cmd.Dir = f.repo
env := map[string]string{
"AGY_SIDECAR_WAIT_SECONDS": waitSeconds,
"AGY_SIDECAR_CORPUS": "", // Each fixture owns its corpus, even during a live audit.
"ANTIGRAVITY_CLI_ROOT": f.root,
"HOME": filepath.Join(f.repo, "home"),
"PATH": f.binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
"TMPDIR": f.repo,
}
for key, value := range overrides {
env[key] = value
}
cmd.Env = overriddenEnv(env)
output, err := cmd.CombinedOutput()
return string(output), err
}
// overriddenEnv is the caller's environment minus every audit input the helper
// or these fixtures read, plus overrides. The documented audit runs this suite
// with AGY_QUALIFY_SESSIONS naming live conversations; inheriting it (or any
// other AGY_*, AUDIT_TEST_* or AGENTSVIEW_DIR value) would audit the fixture
// store against the caller's settings.
func overriddenEnv(overrides map[string]string) []string {
env := make([]string, 0, len(os.Environ())+len(overrides))
for _, item := range os.Environ() {
key, _, _ := strings.Cut(item, "=")
if strings.HasPrefix(key, "AGY_") || strings.HasPrefix(key, "AUDIT_TEST_") || key == "AGENTSVIEW_DIR" {
continue
}
if _, replaced := overrides[key]; !replaced {
env = append(env, item)
}
}
for key, value := range overrides {
env = append(env, key+"="+value)
}
return env
}
func writeExecutable(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
t.Fatal(err)
}
}
func runCommand(t *testing.T, dir, name string, args ...string) {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, out)
}
}
func TestAuditDoesNotRequireGNUFindOrSha256sum(t *testing.T) {
fixture := newAuditFixture(t)
writeFileT(t, fixture.sidecar, []byte(`{"steps":[]}`))
for _, name := range []string{"find", "sha256sum"} {
writeExecutable(t, filepath.Join(fixture.binDir, name), "#!/bin/sh\necho unexpected GNU utility >&2\nexit 99\n")
}
output, err := fixture.run("0", "--record")
if err != nil {
t.Fatalf("portable audit failed: %v\n%s", err, output)
}
if !strings.Contains(output, "Recorded to:") {
t.Fatalf("record missing: %s", output)
}
}
// The suites see neither daemon pinning nor this audit's own inputs, such as
// the live scenario IDs the documented workflow passes in AGY_QUALIFY_SESSIONS.
func TestAuditIsolatesGoTestsFromDaemonOverrides(t *testing.T) {
fixture := newAuditFixture(t)
writeFileT(t, fixture.sidecar, []byte(`{"steps":[]}`))
envLog := filepath.Join(fixture.repo, "go-test-env")
output, err := fixture.runWithOverrides("0", map[string]string{
"ANTIGRAVITY_DAEMON_URL": "http://127.0.0.1:9",
"ANTIGRAVITY_CSRF_TOKEN": "dummy-token",
"AGY_QUALIFY_SESSIONS": "77777777-7777-4777-8777-777777777777",
"AUDIT_TEST_GO_ENV_LOG": envLog,
})
if err != nil {
t.Fatalf("audit failed: %v\n%s", err, output)
}
got, err := os.ReadFile(envLog)
if err != nil {
t.Fatalf("go test never ran: %v\n%s", err, output)
}
if strings.TrimSpace(string(got)) != "url=unset token=unset qualify=unset corpus=unset wait=unset" {
t.Errorf("go test inherited overrides: %q", got)
}
}
func TestAuditReportsGoTestFailureDetail(t *testing.T) {
fixture := newAuditFixture(t)
writeFileT(t, fixture.sidecar, []byte(`{"steps":[]}`))
output, err := fixture.runWithOverrides("0", map[string]string{"AUDIT_TEST_GO_FAIL": "1"})
if err == nil {
t.Fatalf("audit passed despite failing tests:\n%s", output)
}
for _, want := range []string{
"agy-reader unit tests: FAIL (exit 1)",
"--- FAIL: TestFixtureDiagnostic",
"fixture failure detail",
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
}
// sweptNewest selects the newest conversation in auditSweptStore.
type sweptNewest int
const (
newestVerified sweptNewest = iota
newestUnsupported
// newestUnsupportedInWAL: the unsupported conversation's main database is
// older than the verified one, but its nonempty, uncheckpointed -wal is the
// newest write in the store.
newestUnsupportedInWAL
// newestSourceOnly: the newest conversation verifies with the declared
// source-only field (sourceOnlySource).
newestSourceOnly
)
const sweptUnsupportedNewestID = "55555555-5555-4555-8555-555555555555"
// auditSweptStore runs the real audit-sweep over a store whose newest
// conversation is chosen by newest, returning the store and its swept corpus.
func auditSweptStore(t *testing.T, fixture *auditFixture, newest sweptNewest) (string, string) {
t.Helper()
root := filepath.Join(fixture.repo, "swept-root")
now := time.Now()
// History always holds a conversation the reader cannot verify; the newest
// conversation is the one the audit is qualifying.
qualifySource(t, root, qualifyUnsupportedID, true, now.Add(-48*time.Hour))
newestID := qualifyVerifiedID
switch newest {
case newestVerified:
qualifySource(t, root, qualifyVerifiedID, false, now.Add(-time.Minute))
case newestUnsupported:
newestID = sweptUnsupportedNewestID
qualifySource(t, root, qualifyVerifiedID, false, now.Add(-24*time.Hour))
qualifySource(t, root, newestID, true, now.Add(-time.Minute))
case newestUnsupportedInWAL:
newestID = sweptUnsupportedNewestID
qualifySource(t, root, qualifyVerifiedID, false, now.Add(-2*time.Hour))
qualifyWALSource(t, root, newestID, now.Add(-3*time.Hour), now.Add(-time.Hour))
case newestSourceOnly:
newestID = qualifySourceOnlyID
qualifySource(t, root, qualifyVerifiedID, false, now.Add(-24*time.Hour))
sourceOnlySource(t, root, newestID, now.Add(-time.Minute))
}
writeFileT(t, filepath.Join(root, "cli.log"), nil)
srv := qualifyDaemon(t, qualifyUnsupportedID, qualifyVerifiedID, newestID)
t.Setenv("ANTIGRAVITY_DAEMON_URL", srv.URL)
dest := filepath.Join(fixture.repo, "sweep")
var out strings.Builder
if err := cli.RunAuditSweepTo(t.Context(), []string{"--root", root, "--out", dest}, &out); err != nil {
t.Fatalf("sweep: %v\n%s", err, out.String())
}
if !strings.Contains(out.String(), "failures=0") {
t.Fatalf("fixture should fetch and render everything:\n%s", out.String())
}
return root, filepath.Join(dest, "corpus")
}
// qualifyWALSource writes an unsupported SQLite source the way agy leaves a
// conversation it still holds open: the unsupported executor metadata is
// committed only to a nonempty, uncheckpointed -wal. The connection stays open
// until the test ends so nothing checkpoints the WAL away.
func qualifyWALSource(t *testing.T, root, id string, dbTime, walTime time.Time) {
t.Helper()
path := filepath.Join(root, "conversations", id+".db")
db := testfixture.Source(t, path, qualifyPayload(id))
testfixture.Exec(t, db, "PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0")
testfixture.Exec(t, db, "INSERT INTO executor_metadata VALUES(0,?)", unknownPersistedField)
if info, err := os.Stat(path + "-wal"); err != nil || info.Size() == 0 {
t.Fatalf("fixture WAL is missing or empty: %v", err)
}
for p, mtime := range map[string]time.Time{path: dbTime, path + "-wal": walTime} {
if err := os.Chtimes(p, mtime, mtime); err != nil {
t.Fatal(err)
}
}
}
func (f *auditFixture) runReal(t *testing.T, root, corpus string, extra map[string]string, args ...string) (string, error) {
t.Helper()
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
env := map[string]string{
"ANTIGRAVITY_CLI_ROOT": root,
"AGY_SIDECAR_CORPUS": corpus,
"AUDIT_TEST_READER_BIN": self,
}
for k, v := range extra {
env[k] = v
}
return f.runWithOverrides("0", env, args...)
}
// The agy 1.2.15/1.2.16 audits: every conversation fetched and rendered and the
// explicit corpus was asserted swept, yet the newest scenario failed native
// verification and ordinary sync. No record may be offered or written.
func TestAuditRefusesRecordWhenRenderedConversationCannotSync(t *testing.T) {
fixture := newAuditFixture(t)
root, corpus := auditSweptStore(t, fixture, newestUnsupported)
output, err := fixture.runReal(t, root, corpus, nil, "--record", "--corpus-swept")
if err == nil {
t.Fatalf("audit offered a record for an unsyncable conversation:\n%s", output)
}
for _, want := range []string{
"scope: explicit-version-scoped",
sweptUnsupportedNewestID + " NOT VERIFIED (audit-sweep manifest): unknown: unknown persisted protobuf fields",
"Reader sync: FAIL",
"no compatibility record produced",
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
if strings.Contains(output, "Read-only run. To record") || strings.Contains(output, "Recorded to:") {
t.Errorf("record offered:\n%s", output)
}
if content, _ := os.ReadFile(fixture.compatFile); string(content) != "sentinel compatibility record\n" {
t.Fatalf("compatibility record changed: %q", content)
}
// Without the sweep manifest the helper falls back to the live sidecar,
// which ordinary sync never wrote; an explicit corpus is no substitute.
if err := os.Rename(filepath.Join(filepath.Dir(corpus), "manifest.json"), filepath.Join(fixture.repo, "moved-manifest.json")); err != nil {
t.Fatal(err)
}
output, err = fixture.runReal(t, root, corpus, nil, "--corpus-swept")
if err == nil || !strings.Contains(output, "NOT VERIFIED (live sidecar attestation): unknown: no sidecar") {
t.Fatalf("explicit corpus replaced the missing live sidecar: %v\n%s", err, output)
}
}
// Broad historical inspection must keep working when old conversations fall
// outside the current profile: they are reported, and only the newest and the
// explicitly named conversations gate the record.
func TestAuditRecordsDespiteUnsupportedHistoricalConversations(t *testing.T) {
fixture := newAuditFixture(t)
root, corpus := auditSweptStore(t, fixture, newestVerified)
output, err := fixture.runReal(t, root, corpus, nil, "--record", "--corpus-swept")
if err != nil {
t.Fatalf("historical unsupported conversation blocked the audit: %v\n%s", err, output)
}
for _, want := range []string{
"not gating): verified=1/2 SQLite not-verified=1 legacy-unverifiable=0",
qualifyVerifiedID + " VERIFIED (audit-sweep manifest)",
"Reader sync: PASS (1 conversation(s) natively verified via audit-sweep manifest)",
"Recorded to:",
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
content, err := os.ReadFile(fixture.compatFile)
if err != nil || !strings.Contains(string(content), "**Reader sync qualified:** 1 conversation(s) natively verified") {
t.Fatalf("record lacks reader-sync provenance: %v\n%s", err, content)
}
embedded, err := os.ReadFile(filepath.Join(fixture.repo, "internal", "doctor", "COMPATIBILITY.md"))
if err != nil || string(embedded) != string(content) {
t.Fatalf("embedded record differs: %v", err)
}
// Naming the old conversation as a scenario makes it gate the record.
writeFileT(t, fixture.compatFile, []byte("sentinel compatibility record\n"))
output, err = fixture.runReal(t, root, corpus, map[string]string{
"AGY_QUALIFY_SESSIONS": qualifyUnsupportedID,
}, "--record", "--corpus-swept")
if err == nil || !strings.Contains(output, qualifyUnsupportedID+" NOT VERIFIED") {
t.Fatalf("named unsupported conversation did not gate the record: %v\n%s", err, output)
}
if content, _ := os.ReadFile(fixture.compatFile); string(content) != "sentinel compatibility record\n" {
t.Fatalf("compatibility record changed: %q", content)
}
}
// agy keeps an open conversation's newest writes in its -wal until a
// checkpoint, so its main database can be older than another conversation's.
// The record gate must still treat it as the newest conversation, as discovery
// does, or an unsyncable conversation agy is still writing escapes the gate.
func TestAuditGatesNewestConversationByUncheckpointedWAL(t *testing.T) {
fixture := newAuditFixture(t)
root, corpus := auditSweptStore(t, fixture, newestUnsupportedInWAL)
sessions, err := discovery.ListConversationSessions(root)
if err != nil || len(sessions) == 0 || sessions[0].CascadeID != sweptUnsupportedNewestID {
t.Fatalf("fixture: discovery should rank the WAL conversation newest: %v %+v", err, sessions)
}
output, err := fixture.runReal(t, root, corpus, nil, "--record", "--corpus-swept")
if err == nil {
t.Fatalf("audit recorded although the newest conversation cannot sync:\n%s", output)
}
for _, want := range []string{
"Auditing Latest Session: " + sweptUnsupportedNewestID,
sweptUnsupportedNewestID + " NOT VERIFIED (audit-sweep manifest): unknown: unknown persisted protobuf fields",
"Reader sync: FAIL",
"no compatibility record produced",
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
if content, _ := os.ReadFile(fixture.compatFile); string(content) != "sentinel compatibility record\n" {
t.Fatalf("compatibility record changed: %q", content)
}
}
// The helper's newest conversation follows discovery's rule: a nonempty -wal
// or -journal counts, an empty -wal (agy merely opened the file) does not, and
// -shm never does because readers, including the audit itself, update it.
func TestAuditNewestSessionCountsOnlyCommittedSidecarWrites(t *testing.T) {
const otherID = "22222222-2222-4222-8222-222222222222"
now := time.Now()
for _, tc := range []struct {
name string
suffix string
data []byte
want string
}{
{"nonempty wal", "-wal", []byte("frames"), otherID},
{"nonempty journal", "-journal", []byte("pages"), otherID},
{"empty wal", "-wal", nil, "11111111-1111-1111-1111-111111111111"},
{"shm", "-shm", []byte("index"), "11111111-1111-1111-1111-111111111111"},
} {
t.Run(tc.name, func(t *testing.T) {
fixture := newAuditFixture(t)
conversations := filepath.Join(fixture.root, "conversations")
newestDB := filepath.Join(conversations, "11111111-1111-1111-1111-111111111111.db")
otherDB := filepath.Join(conversations, otherID+".db")
writeFileT(t, otherDB, nil)
writeFileT(t, otherDB+tc.suffix, tc.data)
for p, mtime := range map[string]time.Time{
otherDB: now.Add(-3 * time.Hour),
newestDB: now.Add(-2 * time.Hour),
otherDB + tc.suffix: now.Add(-time.Hour),
} {
if err := os.Chtimes(p, mtime, mtime); err != nil {
t.Fatal(err)
}
}
output, _ := fixture.run("0")
if !strings.Contains(output, "Auditing Latest Session: "+tc.want+"\n") {
t.Fatalf("newest session should be %s:\n%s", tc.want, output)
}
})
}
}
// The documented audit sets AGY_QUALIFY_SESSIONS to live scenario IDs and then
// runs this suite. Helper fixtures must not inherit the caller's audit inputs,
// or a valid live ID would fail the audit's own unit-test step.
func TestAuditFixturesIgnoreCallerAuditInputs(t *testing.T) {
t.Setenv("AGY_QUALIFY_SESSIONS", "77777777-7777-4777-8777-777777777777")
t.Setenv("AGENTSVIEW_DIR", t.TempDir())
fixture := newAuditFixture(t)
root, corpus := auditSweptStore(t, fixture, newestVerified)
output, err := fixture.runReal(t, root, corpus, nil, "--corpus-swept")
if err != nil {
t.Fatalf("caller's audit inputs leaked into the fixture: %v\n%s", err, output)
}
for _, want := range []string{
"Reader sync: PASS (1 conversation(s) natively verified",
"Skipping agentsview integration tests (AGENTSVIEW_DIR not specified).",
} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
}
// The format-audit helper still records when a required conversation verifies
// with declared source-only fields, and its summary and the record say how
// many did. It lives here, beside the Linux-only helpers it uses
// (newAuditFixture, auditSweptStore); the portable source-only report tests
// are in sourceonly_report_test.go.
func TestAuditRecordCountsSourceOnlyQualification(t *testing.T) {
fixture := newAuditFixture(t)
root, corpus := auditSweptStore(t, fixture, newestSourceOnly)
output, err := fixture.runReal(t, root, corpus, map[string]string{"AGY_QUALIFY_SESSIONS": qualifyVerifiedID}, "--record", "--corpus-swept")
if err != nil {
t.Fatalf("source-only acceptance blocked the audit: %v\n%s", err, output)
}
assertOutput(t, "audit output", output, []string{
"Auditing Latest Session: " + qualifySourceOnlyID,
qualifySourceOnlyID + " VERIFIED WITH SOURCE-ONLY FIELDS (audit-sweep manifest): " + completeness.SourceOnlyReason,
qualifyVerifiedID + " VERIFIED (audit-sweep manifest)",
"Reader sync: PASS (2 conversation(s) natively verified, 1 of them with declared source-only fields via audit-sweep manifest)",
"Recorded to:",
}, nil)
content, err := os.ReadFile(fixture.compatFile)
if err != nil || !strings.Contains(string(content), "**Reader sync qualified:** 2 conversation(s) natively verified, 1 of them with declared source-only fields\n") {
t.Fatalf("record does not count the source-only qualification: %v\n%s", err, content)
}
}