Files
zero/internal/verify/contracts_test.go
KRATOSandKRATOS 34ad21a551 Add runtime permission and verification event contracts (#82)
* feat: add runtime permission event contract

* feat: stream permission events from exec

* feat: add runtime command snapshots

* feat: add session rewind compaction plans

* feat: add verification event contracts

* fix: harden runtime contract review gaps

* test: assert approved exec stderr contract

* fix: address CodeRabbit CHANGES_REQUESTED items for #82

- Make OnSandboxDecision callback async + panic-safe (non-blocking, recovered) in tools/registry.go per CR.
- Add explicit regression test for ConfigSnapshot when ResolveRuntimeMetadata errors (no secret leak in Message/BaseURL).
- (Many prior items like shapedPayloadPreview for permission/tool events, dual-target conflict test, stderr empty on approved path, Kind/Framework preservation, redactCommand empty slice, etc. were already landed on this branch.)

All relevant tests now pass.

* fix(agent): make sandbox decision available synchronously on Result for permission events

The recent change to invoke OnSandboxDecision asynchronously (to satisfy non-blocking requirement) broke the agent's immediate capture of the decision for building PermissionEvent after registry.RunWithOptions.

- Add SandboxDecision *sandbox.Decision to tools.Result (not serialized).
- Populate it on all sandbox-involved return paths in registry.RunWithOptions (sync, before/while firing async callback for observers).
- In agent executeToolCall, use result.SandboxDecision for buildPermissionEvent (reliable ordering) instead of side-effect capture via the callback.
- The async callback is still supported for any external OnSandboxDecision consumers.

This fixes the two failing agent tests that were causing the cross-platform 'Smoke' CI jobs to fail on this PR:
- TestRunEmitsPermissionEventForPersistentSandboxGrant
- TestRunAppliesSandboxEvenInUnsafeMode

All go tests now pass.

---------

Co-authored-by: KRATOS <[email protected]>
2026-06-06 13:13:08 +05:30

102 lines
3.5 KiB
Go

package verify
import (
"strings"
"testing"
"github.com/Gitlawb/zero/internal/testrunner"
)
func TestSnapshotFromReportRedactsLogsAndBuildsEvents(t *testing.T) {
secret := "sk-proj-abcdefghijklmnopqrstuvwxyz"
report := Report{
Root: "/workspace/" + secret,
StartedAt: "2026-06-06T10:00:00Z",
EndedAt: "2026-06-06T10:00:01Z",
OK: false,
Summary: Summary{Total: 1, Failed: 1},
Results: []Result{{
ID: "go.test",
Name: "Go tests",
Command: []string{"go", "test", "./..."},
Kind: testrunner.KindTest,
Framework: testrunner.FrameworkGo,
Status: StatusFail,
ExitCode: 1,
Stdout: "--- FAIL: TestSecret token " + secret,
Stderr: "stderr " + secret,
StartedAt: "2026-06-06T10:00:00Z",
EndedAt: "2026-06-06T10:00:01Z",
DurationMs: 1000,
OutputSummary: &OutputSummary{
Lines: []string{"failure " + secret},
Truncated: true,
},
TestSummary: &testrunner.Summary{
Framework: testrunner.FrameworkGo,
Total: 1,
Failed: 1,
Failures: []testrunner.Failure{{
Name: "TestSecret",
File: "secret_test.go:12",
Message: "leaked " + secret,
}},
},
}},
}
snapshot := SnapshotFromReport(report)
if snapshot.Contract != ReportContractVersion || snapshot.Runtime != RuntimeGo {
t.Fatalf("unexpected snapshot metadata: %#v", snapshot)
}
if snapshot.Root == report.Root || strings.Contains(snapshot.Root, secret) {
t.Fatalf("root was not redacted: %#v", snapshot)
}
if len(snapshot.Results) != 1 || snapshot.Results[0].ID != "go.test" || snapshot.Results[0].Kind != testrunner.KindTest {
t.Fatalf("result contract fields were not preserved: %#v", snapshot.Results)
}
combined := snapshot.Results[0].Stdout + snapshot.Results[0].Stderr + snapshot.Results[0].OutputSummary.Lines[0] + snapshot.Results[0].TestSummary.Failures[0].Message
if strings.Contains(combined, secret) || !strings.Contains(combined, "[REDACTED]") {
t.Fatalf("snapshot leaked secret: %#v", snapshot.Results[0])
}
if len(snapshot.Events) != 3 {
t.Fatalf("expected start/check/finish events, got %#v", snapshot.Events)
}
if snapshot.Events[0].Type != EventVerifyStarted || snapshot.Events[1].Type != EventCheckFinished || snapshot.Events[2].Type != EventVerifyFinished {
t.Fatalf("unexpected event order: %#v", snapshot.Events)
}
if snapshot.Events[1].CheckID != "go.test" || snapshot.Events[1].Status != StatusFail {
t.Fatalf("check event did not capture result: %#v", snapshot.Events[1])
}
if snapshot.Events[1].Kind != testrunner.KindTest || snapshot.Events[1].Framework != testrunner.FrameworkGo {
t.Fatalf("check event did not preserve kind/framework: %#v", snapshot.Events[1])
}
}
func TestEventsFromReportHandlesEmptyReport(t *testing.T) {
events := EventsFromReport(Report{Root: "/workspace", OK: true})
if len(events) != 2 {
t.Fatalf("expected start and finish events for empty report, got %#v", events)
}
if events[0].Type != EventVerifyStarted || events[1].Type != EventVerifyFinished {
t.Fatalf("unexpected empty report events: %#v", events)
}
}
func TestSnapshotFromReportUsesEmptyCommandSlice(t *testing.T) {
snapshot := SnapshotFromReport(Report{
Root: "/workspace",
OK: true,
Results: []Result{{ID: "manual", Status: StatusPass}},
})
if snapshot.Results[0].Command == nil {
t.Fatalf("expected empty command slice, got nil: %#v", snapshot.Results[0])
}
if snapshot.Events[1].Command == nil {
t.Fatalf("expected empty event command slice, got nil: %#v", snapshot.Events[1])
}
}