mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
* 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]>
102 lines
3.5 KiB
Go
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])
|
|
}
|
|
}
|