Files
zero/internal/selfverify/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

90 lines
3.0 KiB
Go

package selfverify
import (
"strings"
"testing"
"github.com/Gitlawb/zero/internal/testrunner"
"github.com/Gitlawb/zero/internal/verify"
)
func TestSnapshotFromReportPreservesAttemptsAndRedactsRemediation(t *testing.T) {
secret := "sk-proj-abcdefghijklmnopqrstuvwxyz"
report := Report{
Root: "/repo/" + secret,
StartedAt: "2026-06-06T11:00:00Z",
EndedAt: "2026-06-06T11:00:02Z",
OK: true,
StopReason: StopReasonPassed,
Summary: verify.Summary{Total: 1, Passed: 1},
Attempts: []Attempt{
{
Number: 1,
Report: verify.Report{
Root: "/repo/" + secret,
OK: false,
Summary: verify.Summary{Total: 1, Failed: 1},
Results: []verify.Result{{
ID: "go.test",
Name: "Go tests",
Command: []string{"go", "test", "./..."},
Kind: testrunner.KindTest,
Framework: testrunner.FrameworkGo,
Status: verify.StatusFail,
Stdout: "failed " + secret,
}},
},
Remediation: &Remediation{Applied: true, Message: "fixed " + secret},
},
{
Number: 2,
Report: verify.Report{Root: "/repo/" + secret, OK: true, Summary: verify.Summary{Total: 1, Passed: 1}},
},
},
}
snapshot := SnapshotFromReport(report)
if snapshot.Contract != LoopContractVersion || snapshot.Runtime != verify.RuntimeGo {
t.Fatalf("unexpected snapshot metadata: %#v", snapshot)
}
if len(snapshot.Attempts) != 2 || !snapshot.OK || snapshot.StopReason != StopReasonPassed {
t.Fatalf("attempts were not preserved: %#v", snapshot)
}
if len(snapshot.Attempts[0].Report.Results) != 1 || snapshot.Attempts[0].Report.Results[0].ID != "go.test" {
t.Fatalf("verify report snapshot missing attempt result: %#v", snapshot.Attempts[0].Report)
}
if snapshot.Attempts[0].Remediation == nil || !snapshot.Attempts[0].Remediation.Applied {
t.Fatalf("remediation was not preserved: %#v", snapshot.Attempts[0])
}
combined := snapshot.Root + snapshot.Attempts[0].Report.Root + snapshot.Attempts[0].Report.Results[0].Stdout + snapshot.Attempts[0].Remediation.Message
if strings.Contains(combined, secret) || !strings.Contains(combined, "[REDACTED]") {
t.Fatalf("loop snapshot leaked secret: %#v", snapshot)
}
if len(snapshot.Events) != 4 {
t.Fatalf("expected loop start, two attempts, and finish events, got %#v", snapshot.Events)
}
if snapshot.Events[1].Type != EventAttemptFinished || snapshot.Events[1].Attempt != 1 || snapshot.Events[1].Status != verify.StatusFail {
t.Fatalf("attempt event did not capture first attempt: %#v", snapshot.Events[1])
}
}
func TestEventsFromReportMarksAttemptErrorWhenSummaryHasErrors(t *testing.T) {
report := Report{
Root: "/repo",
Attempts: []Attempt{{
Number: 1,
Report: verify.Report{
OK: false,
Summary: verify.Summary{Total: 1, Errors: 1},
},
}},
}
events := EventsFromReport(report)
if len(events) < 2 || events[1].Type != EventAttemptFinished || events[1].Status != verify.StatusError {
t.Fatalf("expected attempt status error, got %#v", events)
}
}