test(agentcompat): add evidence validation

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
naiba
2026-07-20 04:50:03 +00:00
co-authored by naiba/CloudCode
parent 1c3e926819
commit b3b3d92895
16 changed files with 1901 additions and 0 deletions
@@ -0,0 +1,59 @@
package evidence
import (
"fmt"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
type ProfileMetadata struct {
Name string `json:"name"`
JobTimeoutSeconds int64 `json:"job_timeout_seconds"`
SuiteDeadlineSeconds int64 `json:"suite_deadline_seconds"`
DefaultSeed string `json:"default_seed"`
AgentCount int `json:"agent_count"`
StressRounds int `json:"stress_rounds"`
ConcurrentOperations int `json:"concurrent_operations"`
ConcurrentSessionsPerKind int `json:"concurrent_sessions_per_kind"`
TransferPairs int `json:"transfer_pairs"`
TransferBytes uint64 `json:"transfer_bytes"`
DashboardRestartCycles int `json:"dashboard_restart_cycles"`
Iterations int `json:"iterations"`
StreamBoundaryAllowed int `json:"stream_boundary_allowed"`
StreamBoundaryRejected int `json:"stream_boundary_rejected"`
}
type ResourceBudgetMetadata struct {
WarmupRunsPerPath int `json:"warmup_runs_per_path"`
BaselineSampleCount int `json:"baseline_sample_count"`
EndSampleCount int `json:"end_sample_count"`
SampleIntervalMilliseconds int64 `json:"sample_interval_milliseconds"`
ChildProcessCountDrift int `json:"child_process_count_drift"`
ListenerCountDrift int `json:"listener_count_drift"`
NonStdioFDCountDrift int `json:"non_stdio_fd_count_drift"`
DashboardRSSDeltaBytes uint64 `json:"dashboard_rss_delta_bytes"`
AgentRSSDeltaBytes uint64 `json:"agent_rss_delta_bytes"`
TransferHeapBytes uint64 `json:"transfer_heap_bytes"`
}
func profileMetadata(profile contract.Profile) ProfileMetadata {
return ProfileMetadata{Name: string(profile.Name()), JobTimeoutSeconds: int64(profile.JobTimeout().Seconds()), SuiteDeadlineSeconds: int64(profile.SuiteDeadline().Seconds()), DefaultSeed: fmt.Sprintf("0x%x", uint64(profile.Seed())), AgentCount: profile.AgentCount(), StressRounds: profile.StressRounds(), ConcurrentOperations: profile.ConcurrentOperations(), ConcurrentSessionsPerKind: profile.ConcurrentSessions(), TransferPairs: profile.TransferPairs(), TransferBytes: profile.TransferBytes(), DashboardRestartCycles: profile.DashboardRestartCycles(), Iterations: profile.Iterations(), StreamBoundaryAllowed: profile.StreamBoundaryAllowed(), StreamBoundaryRejected: profile.StreamBoundaryRejected()}
}
func resourceBudgetMetadata(budget contract.ResourceBudget) ResourceBudgetMetadata {
return ResourceBudgetMetadata{WarmupRunsPerPath: budget.WarmupRuns(), BaselineSampleCount: budget.SampleCount(), EndSampleCount: budget.SampleCount(), SampleIntervalMilliseconds: budget.SampleInterval().Milliseconds(), ChildProcessCountDrift: budget.ChildProcessCountDrift(), ListenerCountDrift: budget.ListenerCountDrift(), NonStdioFDCountDrift: budget.NonStdioFDCountDrift(), DashboardRSSDeltaBytes: budget.DashboardRSSDeltaBytes(), AgentRSSDeltaBytes: budget.AgentRSSDeltaBytes(), TransferHeapBytes: budget.TransferHeapBytes()}
}
func (p ProfileMetadata) Validate() error {
if p.Name == "" || p.JobTimeoutSeconds < 1 || p.SuiteDeadlineSeconds < 1 || p.DefaultSeed == "" || p.DefaultSeed == "0x0" || p.AgentCount < 1 || p.StressRounds < 1 || p.ConcurrentOperations < 1 || p.ConcurrentSessionsPerKind < 1 || p.TransferPairs < 1 || p.TransferBytes == 0 || p.DashboardRestartCycles < 1 || p.Iterations < 1 || p.StreamBoundaryAllowed < 1 || p.StreamBoundaryRejected <= p.StreamBoundaryAllowed {
return fmt.Errorf("profile fields are invalid")
}
return nil
}
func (b ResourceBudgetMetadata) Validate() error {
if b.WarmupRunsPerPath < 1 || b.BaselineSampleCount < 1 || b.EndSampleCount < 1 || b.SampleIntervalMilliseconds < 1 || b.ChildProcessCountDrift < 0 || b.ListenerCountDrift < 0 || b.NonStdioFDCountDrift < 0 || b.DashboardRSSDeltaBytes == 0 || b.AgentRSSDeltaBytes == 0 || b.TransferHeapBytes == 0 {
return fmt.Errorf("resource budget fields are invalid")
}
return nil
}
@@ -0,0 +1,213 @@
package evidence
import (
"errors"
"strings"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
type transferEvidence struct {
WarmupUploadBytes uint64 `json:"warmup_upload_bytes"`
WarmupDownloadBytes uint64 `json:"warmup_download_bytes"`
WarmupSHA256 string `json:"warmup_sha256"`
WarmupDuration time.Duration `json:"warmup_duration"`
WarmupDeadlineRemaining time.Duration `json:"warmup_deadline_remaining"`
WarmupQuiescent bool `json:"warmup_quiescent"`
UploadBytes uint64 `json:"upload_bytes"`
DownloadBytes uint64 `json:"download_bytes"`
UploadSHA256 string `json:"upload_sha256"`
DownloadSHA256 string `json:"download_sha256"`
UploadChunks uint64 `json:"upload_chunks"`
DownloadChunks uint64 `json:"download_chunks"`
UploadDuration time.Duration `json:"upload_duration"`
DownloadDuration time.Duration `json:"download_duration"`
RetainedHeapBytes uint64 `json:"retained_heap_bytes"`
Mode string `json:"mode"`
CreateDirs bool `json:"create_dirs"`
UploadReplayRejected bool `json:"upload_replay_rejected"`
DownloadReplayRejected bool `json:"download_replay_rejected"`
OversizeRejected bool `json:"oversize_rejected"`
AgentTempResidue int `json:"agent_temp_residue"`
DashboardSpoolResidue int `json:"dashboard_spool_residue"`
OutsideRootSentinelsUnchanged bool `json:"outside_root_sentinels_unchanged"`
}
type transferArtifact struct {
Scenario string `json:"scenario"`
Fault string `json:"fault,omitempty"`
Passed bool `json:"passed"`
CleanupOK bool `json:"cleanup_ok"`
Error string `json:"error,omitempty"`
Evidence transferEvidence `json:"evidence"`
}
type reconnectObservation struct {
ServerID uint64
UUID string
OldGeneration uint64
NewGeneration uint64
DisconnectAt time.Time
ReconnectAt time.Time
TaskIDs []uint64
ResultIDs []uint64
PostReconnect bool
AgentRestarted bool
}
type listenerIdentity struct {
Address string
Inode uint64
}
type fixtureIdentity struct {
WorkspaceRoot string
ConfigPath string
DatabasePath string
BinaryPath string
HTTP listenerIdentity
Receipt listenerIdentity
HTTPS listenerIdentity
}
type runtimeIdentity struct {
Generation uint64
PID int
ProcessGroupID int
}
type receiptEvent struct {
Sequence uint64 `json:"sequence"`
DashboardGeneration uint64 `json:"dashboard_generation"`
GateGeneration uint64 `json:"gate_generation"`
ServerID uint64 `json:"server_id"`
TaskID uint64 `json:"task_id"`
TaskType uint64 `json:"task_type"`
Kind string `json:"kind"`
}
type receiptPair struct {
Task receiptEvent `json:"task"`
Result receiptEvent `json:"result"`
}
type cleanupRecord struct {
Name string `json:"name"`
PID int `json:"pid"`
Forced bool `json:"forced"`
Error string `json:"error,omitempty"`
}
type cleanupReceipt struct {
Passed bool `json:"passed"`
Forced bool `json:"forced"`
Processes []cleanupRecord `json:"processes"`
}
type reconnectEvidence struct {
Fixture struct {
Dashboard fixtureIdentity `json:"dashboard"`
AgentRoot string `json:"agent_root"`
AgentConfigPath string `json:"agent_config_path"`
AgentBinaryPath string `json:"agent_binary_path"`
} `json:"fixture"`
Runtime struct {
DashboardBefore runtimeIdentity `json:"dashboard_before"`
DashboardAfter runtimeIdentity `json:"dashboard_after"`
AgentBefore runtimeIdentity `json:"agent_before"`
AgentAfter runtimeIdentity `json:"agent_after"`
StateGenerationBeforeAgentRestart uint64 `json:"state_generation_before_agent_restart"`
StateGenerationAfterAgentRestart uint64 `json:"state_generation_after_agent_restart"`
} `json:"runtime"`
Identity struct {
ServerID uint64 `json:"server_id"`
UUID string `json:"uuid"`
DashboardConfigUnchanged bool `json:"dashboard_config_unchanged"`
AgentConfigUnchanged bool `json:"agent_config_unchanged"`
DashboardFixtureUnchanged bool `json:"dashboard_fixture_unchanged"`
ClientsRecreated bool `json:"clients_recreated"`
BootstrapRecreated bool `json:"bootstrap_recreated"`
} `json:"identity"`
Lifecycle struct {
DisconnectAt time.Time `json:"disconnect_at"`
ReconnectAt time.Time `json:"reconnect_at"`
ReconnectInterval time.Duration `json:"reconnect_interval"`
DashboardReceipts []receiptPair `json:"dashboard_receipts"`
AgentReceipts []receiptPair `json:"agent_receipts"`
StaleGenerationReceipts int `json:"stale_generation_receipts"`
DuplicateTaskIDs int `json:"duplicate_task_ids"`
LostResultIDs int `json:"lost_result_ids"`
OutsideRootSentinelUnchanged bool `json:"outside_root_sentinel_unchanged"`
} `json:"lifecycle"`
Observation reconnectObservation `json:"observation"`
AgentCleanup cleanupReceipt `json:"agent_cleanup"`
DashboardCleanup cleanupReceipt `json:"dashboard_cleanup"`
}
type reconnectArtifact struct {
Scenario string `json:"scenario"`
Fault string `json:"fault,omitempty"`
Passed bool `json:"passed"`
CleanupOK bool `json:"cleanup_ok"`
Error string `json:"error,omitempty"`
Evidence reconnectEvidence `json:"evidence"`
}
func validateTransferArtifact(metadata Metadata, result ScenarioResult, artifact transferArtifact) error {
if err := validateArtifactHeader(metadata, result, artifact.Scenario, artifact.Fault, artifact.Passed, artifact.CleanupOK, artifact.Error); err != nil {
return err
}
switch artifact.Fault {
case "":
if !artifact.Passed {
return errors.New("transfer success artifact reports failure")
}
if err := validateTransferSuccess(artifact.Evidence); err != nil {
return err
}
case contract.FaultTransferHash:
if artifact.Passed || !strings.Contains(artifact.Error, "injected hash mismatch") {
return errors.New("transfer-hash artifact does not identify the injected failure")
}
evidence := artifact.Evidence
if evidence.WarmupUploadBytes != 65536 || evidence.WarmupDownloadBytes != 65536 || evidence.WarmupSHA256 == "" || evidence.WarmupDuration <= 0 || evidence.WarmupDeadlineRemaining <= 0 || !evidence.WarmupQuiescent {
return errors.New("transfer-hash artifact omitted warmup evidence")
}
if evidence.UploadBytes != 0 || evidence.DownloadBytes != 0 || evidence.UploadSHA256 != "" || evidence.DownloadSHA256 != "" || evidence.UploadChunks != 0 || evidence.DownloadChunks != 0 || evidence.UploadDuration != 0 || evidence.DownloadDuration != 0 || evidence.RetainedHeapBytes != 0 || evidence.Mode != "" || evidence.CreateDirs || evidence.UploadReplayRejected || evidence.DownloadReplayRejected || evidence.OversizeRejected {
return errors.New("transfer-hash artifact presents stale success evidence")
}
if evidence.AgentTempResidue != 0 || evidence.DashboardSpoolResidue != 0 || !evidence.OutsideRootSentinelsUnchanged {
return errors.New("transfer-hash artifact cleanup or sentinel evidence failed")
}
default:
return errors.New("transfer artifact has unsupported fault")
}
return nil
}
func validateArtifactHeader(metadata Metadata, result ScenarioResult, scenarioName, fault string, passed, cleanupOK bool, errorText string) error {
if scenarioName != result.Name || scenarioName != metadata.Scenarios[0] || fault != metadata.Fault || passed != result.Passed || !cleanupOK || errorText != result.Error {
return errors.New("dedicated artifact does not agree with metadata, results, or cleanup")
}
if passed && errorText != "" || !passed && errorText == "" {
return errors.New("dedicated artifact pass/error state is inconsistent")
}
return nil
}
func validateTransferSuccess(evidence transferEvidence) error {
if evidence.WarmupUploadBytes != 65536 || evidence.WarmupDownloadBytes != 65536 || evidence.WarmupSHA256 == "" || evidence.WarmupDuration <= 0 || evidence.WarmupDeadlineRemaining <= 0 || !evidence.WarmupQuiescent {
return errors.New("transfer warmup evidence is invalid")
}
if evidence.UploadBytes != contract.TransferBytes || evidence.DownloadBytes != contract.TransferBytes || evidence.UploadSHA256 == "" || !strings.EqualFold(evidence.UploadSHA256, evidence.DownloadSHA256) {
return errors.New("transfer byte counts or hashes are invalid")
}
if evidence.UploadChunks == 0 || evidence.DownloadChunks == 0 || evidence.UploadDuration <= 0 || evidence.DownloadDuration <= 0 || evidence.RetainedHeapBytes > contract.TransferHeapBytes {
return errors.New("transfer measurement evidence is invalid")
}
if evidence.Mode != "0640" || !evidence.CreateDirs || !evidence.UploadReplayRejected || !evidence.DownloadReplayRejected || !evidence.OversizeRejected || evidence.AgentTempResidue != 0 || evidence.DashboardSpoolResidue != 0 || !evidence.OutsideRootSentinelsUnchanged {
return errors.New("transfer contract evidence is invalid")
}
return nil
}
@@ -0,0 +1,46 @@
package evidence
import (
"testing"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func TestEvidence_DedicatedArtifactsRequireExactScenarioAssertions(t *testing.T) {
tests := []struct {
name string
scenario string
assertion string
artifact func(*testing.T, string)
}{
{"transfer generic", contract.ScenarioTransfer100MiB, "scenario passed", func(t *testing.T, dir string) {
writeJSONEvidenceFile(t, dir, "transfer.json", validTransferArtifact("", true))
}},
{"transfer mismatched", contract.ScenarioTransfer100MiB, contract.AssertionReconnectDisconnect, func(t *testing.T, dir string) {
writeJSONEvidenceFile(t, dir, "transfer.json", validTransferArtifact("", true))
}},
{"reconnect generic", contract.ScenarioReconnect, "scenario passed", func(t *testing.T, dir string) {
writeJSONEvidenceFile(t, dir, "reconnect.json", validReconnectArtifact("", true))
}},
{"reconnect mismatched", contract.ScenarioReconnect, contract.AssertionTransferWarmup, func(t *testing.T, dir string) {
writeJSONEvidenceFile(t, dir, "reconnect.json", validReconnectArtifact("", true))
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeDedicatedExecutableEvidence(t, dir, test.scenario, "", true)
results := Results{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: test.scenario, Passed: true, Assertions: []Assertion{{Name: test.assertion, Passed: true}}}}}
writeJSONEvidenceFile(t, dir, "results.json", results)
junit, err := JUnit(results)
if err != nil {
t.Fatalf("JUnit: %v", err)
}
writeEvidenceFile(t, dir, "junit.xml", string(junit))
test.artifact(t, dir)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("generic assertion accepted for dedicated evidence")
}
})
}
}
@@ -0,0 +1,86 @@
package evidence
import (
"testing"
"time"
)
func TestEvidence_TransferHashRejectsEverySuccessOnlyField(t *testing.T) {
tests := []struct {
name string
mutate func(*transferEvidence)
}{
{"upload bytes", func(value *transferEvidence) { value.UploadBytes = 1 }},
{"download bytes", func(value *transferEvidence) { value.DownloadBytes = 1 }},
{"upload hash", func(value *transferEvidence) { value.UploadSHA256 = "stale" }},
{"download hash", func(value *transferEvidence) { value.DownloadSHA256 = "stale" }},
{"upload chunks", func(value *transferEvidence) { value.UploadChunks = 1 }},
{"download chunks", func(value *transferEvidence) { value.DownloadChunks = 1 }},
{"upload duration", func(value *transferEvidence) { value.UploadDuration = time.Nanosecond }},
{"download duration", func(value *transferEvidence) { value.DownloadDuration = time.Nanosecond }},
{"retained heap", func(value *transferEvidence) { value.RetainedHeapBytes = 1 }},
{"mode", func(value *transferEvidence) { value.Mode = "0640" }},
{"create dirs", func(value *transferEvidence) { value.CreateDirs = true }},
{"upload replay", func(value *transferEvidence) { value.UploadReplayRejected = true }},
{"download replay", func(value *transferEvidence) { value.DownloadReplayRejected = true }},
{"oversize", func(value *transferEvidence) { value.OversizeRejected = true }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeDedicatedExecutableEvidence(t, dir, "transfer-100mib", "transfer-hash", false)
artifact := validTransferArtifact("transfer-hash", false)
test.mutate(&artifact.Evidence)
writeJSONEvidenceFile(t, dir, "transfer.json", artifact)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("stale transfer success field accepted")
}
})
}
}
func TestEvidence_DashboardExitRejectsEveryPostFaultField(t *testing.T) {
tests := []struct {
name string
mutate func(*reconnectEvidence)
}{
{"dashboard after", func(value *reconnectEvidence) { value.Runtime.DashboardAfter.PID = 1 }},
{"agent after", func(value *reconnectEvidence) { value.Runtime.AgentAfter.PID = 1 }},
{"state before restart", func(value *reconnectEvidence) { value.Runtime.StateGenerationBeforeAgentRestart = 1 }},
{"state after restart", func(value *reconnectEvidence) { value.Runtime.StateGenerationAfterAgentRestart = 1 }},
{"identity dashboard config", func(value *reconnectEvidence) { value.Identity.DashboardConfigUnchanged = true }},
{"identity agent config", func(value *reconnectEvidence) { value.Identity.AgentConfigUnchanged = true }},
{"identity fixture", func(value *reconnectEvidence) { value.Identity.DashboardFixtureUnchanged = true }},
{"clients recreated", func(value *reconnectEvidence) { value.Identity.ClientsRecreated = true }},
{"bootstrap recreated", func(value *reconnectEvidence) { value.Identity.BootstrapRecreated = true }},
{"reconnect timestamp", func(value *reconnectEvidence) { value.Lifecycle.ReconnectAt = time.Now() }},
{"reconnect interval", func(value *reconnectEvidence) { value.Lifecycle.ReconnectInterval = time.Second }},
{"dashboard receipts", func(value *reconnectEvidence) { value.Lifecycle.DashboardReceipts = []receiptPair{{}} }},
{"agent receipts", func(value *reconnectEvidence) { value.Lifecycle.AgentReceipts = []receiptPair{{}} }},
{"stale accounting", func(value *reconnectEvidence) { value.Lifecycle.StaleGenerationReceipts = 1 }},
{"duplicate accounting", func(value *reconnectEvidence) { value.Lifecycle.DuplicateTaskIDs = 1 }},
{"lost accounting", func(value *reconnectEvidence) { value.Lifecycle.LostResultIDs = 1 }},
{"observation server", func(value *reconnectEvidence) { value.Observation.ServerID = 1 }},
{"observation uuid", func(value *reconnectEvidence) { value.Observation.UUID = "stale" }},
{"old generation", func(value *reconnectEvidence) { value.Observation.OldGeneration = 1 }},
{"new generation", func(value *reconnectEvidence) { value.Observation.NewGeneration = 1 }},
{"disconnect observation", func(value *reconnectEvidence) { value.Observation.DisconnectAt = time.Now() }},
{"reconnect observation", func(value *reconnectEvidence) { value.Observation.ReconnectAt = time.Now() }},
{"task ids", func(value *reconnectEvidence) { value.Observation.TaskIDs = []uint64{1} }},
{"result ids", func(value *reconnectEvidence) { value.Observation.ResultIDs = []uint64{1} }},
{"post reconnect", func(value *reconnectEvidence) { value.Observation.PostReconnect = true }},
{"agent restarted", func(value *reconnectEvidence) { value.Observation.AgentRestarted = true }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeDedicatedExecutableEvidence(t, dir, "reconnect", "dashboard-exit", false)
artifact := validReconnectArtifact("dashboard-exit", false)
test.mutate(&artifact.Evidence)
writeJSONEvidenceFile(t, dir, "reconnect.json", artifact)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("stale reconnect success field accepted")
}
})
}
}
@@ -0,0 +1,212 @@
package evidence
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func TestEvidence_TransferAndReconnectRequireDedicatedArtifacts(t *testing.T) {
for _, scenarioName := range []string{"transfer-100mib", "reconnect"} {
t.Run(scenarioName, func(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", scenarioName, true, true, true)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("missing dedicated artifact accepted")
}
})
}
}
func TestEvidence_ValidatesTypedTransferSuccessAndFaultArtifacts(t *testing.T) {
tests := []struct {
name string
fault string
passed bool
}{
{"success", "", true},
{"hash fault", "transfer-hash", false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeDedicatedExecutableEvidence(t, dir, "transfer-100mib", test.fault, test.passed)
artifact := validTransferArtifact(test.fault, test.passed)
writeJSONEvidenceFile(t, dir, "transfer.json", artifact)
if err := ValidateDirectory(dir); err != nil {
t.Fatalf("validate transfer evidence: %v", err)
}
})
}
}
func TestEvidence_ValidatesTypedReconnectSuccessAndFaultArtifacts(t *testing.T) {
tests := []struct {
name string
fault string
passed bool
}{
{"success", "", true},
{"dashboard fault", "dashboard-exit", false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeDedicatedExecutableEvidence(t, dir, "reconnect", test.fault, test.passed)
artifact := validReconnectArtifact(test.fault, test.passed)
writeJSONEvidenceFile(t, dir, "reconnect.json", artifact)
if err := ValidateDirectory(dir); err != nil {
t.Fatalf("validate reconnect evidence: %v", err)
}
})
}
}
func TestEvidence_RejectsMisleadingDedicatedArtifact(t *testing.T) {
dir := t.TempDir()
writeDedicatedExecutableEvidence(t, dir, "transfer-100mib", "transfer-hash", false)
artifact := validTransferArtifact("transfer-hash", false)
artifact.Evidence.UploadBytes = 104857600
writeJSONEvidenceFile(t, dir, "transfer.json", artifact)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("fault artifact containing stale success accepted")
}
}
func TestEvidence_RejectsWrongOrStaleDedicatedArtifact(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", "transfer-100mib", true, true, true)
writeEvidenceFile(t, dir, "reconnect.json", `{}`)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("wrong dedicated artifact accepted")
}
}
func TestEvidence_RejectsDedicatedArtifactWithPublicMode(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", "transfer-100mib", true, true, true)
path := filepath.Join(dir, "transfer.json")
if err := os.WriteFile(path, []byte(`{}`), 0o644); err != nil {
t.Fatalf("write transfer artifact: %v", err)
}
if err := ValidateDirectory(dir); err == nil {
t.Fatal("public dedicated artifact mode accepted")
}
}
func writeDedicatedExecutableEvidence(t *testing.T, dir, scenarioName, fault string, passed bool) {
t.Helper()
metadata := validMetadata(t, dir, scenarioName)
metadata.Fault = fault
writeJSONEvidenceFile(t, dir, "metadata.json", metadata)
errorText := ""
if !passed {
if scenarioName == "transfer-100mib" {
errorText = "transfer scenario: injected hash mismatch"
} else {
errorText = "reconnect scenario: injected Dashboard exit"
}
}
definition, err := contract.ScenarioDefinitionByName(scenarioName)
if err != nil {
t.Fatalf("scenario definition: %v", err)
}
assertions := make([]Assertion, 0, len(definition.Assertions(fault)))
for _, assertion := range definition.Assertions(fault) {
assertions = append(assertions, Assertion{Name: assertion.Name, Passed: assertion.Passed})
}
results := Results{Profile: "pr-full", Passed: passed, Scenarios: []ScenarioResult{{Name: scenarioName, Passed: passed, Assertions: assertions, Error: errorText}}}
writeJSONEvidenceFile(t, dir, "results.json", results)
junit, err := JUnit(results)
if err != nil {
t.Fatalf("JUnit: %v", err)
}
writeEvidenceFile(t, dir, "junit.xml", string(junit))
writeEvidenceFile(t, dir, "cleanup.json", fmt.Sprintf(`{"passed":true,"scenario":%q,"finished_at":"2026-01-02T03:04:05Z"}`, scenarioName))
}
func validTransferArtifact(fault string, passed bool) transferArtifact {
errorText := ""
if !passed {
errorText = "transfer scenario: injected hash mismatch"
}
evidence := transferEvidence{WarmupUploadBytes: 65536, WarmupDownloadBytes: 65536, WarmupSHA256: "abc", WarmupDuration: time.Nanosecond, WarmupDeadlineRemaining: time.Second, WarmupQuiescent: true, OutsideRootSentinelsUnchanged: true}
if passed {
evidence.UploadBytes = 104857600
evidence.DownloadBytes = 104857600
evidence.UploadSHA256 = "abc"
evidence.DownloadSHA256 = "abc"
evidence.UploadChunks = 2
evidence.DownloadChunks = 2
evidence.UploadDuration = time.Second
evidence.DownloadDuration = time.Second
evidence.Mode = "0640"
evidence.CreateDirs = true
evidence.UploadReplayRejected = true
evidence.DownloadReplayRejected = true
evidence.OversizeRejected = true
}
return transferArtifact{Scenario: "transfer-100mib", Fault: fault, Passed: passed, CleanupOK: true, Error: errorText, Evidence: evidence}
}
func validReconnectArtifact(fault string, passed bool) reconnectArtifact {
var artifact reconnectArtifact
artifact.Scenario = "reconnect"
artifact.Fault = fault
artifact.Passed = passed
artifact.CleanupOK = true
if !passed {
artifact.Error = "reconnect scenario: injected Dashboard exit"
}
evidence := &artifact.Evidence
evidence.Fixture.Dashboard.WorkspaceRoot = "/tmp/dashboard"
evidence.Fixture.Dashboard.ConfigPath = "/tmp/dashboard/config"
evidence.Fixture.Dashboard.BinaryPath = "/tmp/dashboard/bin"
evidence.Fixture.AgentRoot = "/tmp/agent"
evidence.Fixture.AgentConfigPath = "/tmp/agent/config"
evidence.Fixture.AgentBinaryPath = "/tmp/agent/bin"
evidence.Runtime.DashboardBefore = runtimeIdentity{Generation: 1, PID: 10, ProcessGroupID: 10}
evidence.Runtime.AgentBefore = runtimeIdentity{Generation: 1, PID: 20, ProcessGroupID: 20}
evidence.Identity.ServerID = 1
evidence.Identity.UUID = "uuid"
evidence.Lifecycle.DisconnectAt = time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
evidence.Lifecycle.OutsideRootSentinelUnchanged = true
evidence.AgentCleanup = validCleanupReceipt("agent", expectedProcessCount(passed))
evidence.DashboardCleanup = validCleanupReceipt("dashboard", expectedProcessCount(passed))
if passed {
evidence.Runtime.DashboardAfter = runtimeIdentity{Generation: 2, PID: 11, ProcessGroupID: 11}
evidence.Runtime.AgentAfter = runtimeIdentity{Generation: 2, PID: 21, ProcessGroupID: 21}
evidence.Runtime.StateGenerationBeforeAgentRestart = 1
evidence.Runtime.StateGenerationAfterAgentRestart = 2
evidence.Identity.DashboardConfigUnchanged = true
evidence.Identity.AgentConfigUnchanged = true
evidence.Identity.DashboardFixtureUnchanged = true
evidence.Identity.ClientsRecreated = true
evidence.Identity.BootstrapRecreated = true
evidence.Lifecycle.ReconnectAt = evidence.Lifecycle.DisconnectAt.Add(time.Second)
evidence.Lifecycle.ReconnectInterval = time.Second
evidence.Lifecycle.DashboardReceipts = make([]receiptPair, 3)
evidence.Lifecycle.AgentReceipts = make([]receiptPair, 2)
evidence.Observation = reconnectObservation{ServerID: 1, UUID: "uuid", OldGeneration: 1, NewGeneration: 2, DisconnectAt: evidence.Lifecycle.DisconnectAt, ReconnectAt: evidence.Lifecycle.ReconnectAt, TaskIDs: []uint64{1, 2, 3, 4, 5}, ResultIDs: []uint64{1, 2, 3, 4, 5}, PostReconnect: true, AgentRestarted: true}
}
return artifact
}
func validCleanupReceipt(name string, count int) cleanupReceipt {
receipt := cleanupReceipt{Passed: true, Processes: make([]cleanupRecord, count)}
for index := range receipt.Processes {
receipt.Processes[index] = cleanupRecord{Name: name, PID: index + 1}
}
return receipt
}
func expectedProcessCount(passed bool) int {
if passed {
return 2
}
return 1
}
@@ -0,0 +1,133 @@
package evidence
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const (
maxEvidenceFiles = 128
maxEvidenceBytes = 32 << 20
)
func scanDirectory(resultsDir string) (map[string]os.FileInfo, error) {
if strings.TrimSpace(resultsDir) == "" {
return nil, errors.New("evidence directory is required")
}
info, err := os.Lstat(resultsDir)
if err != nil {
return nil, fmt.Errorf("stat evidence directory: %w", err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return nil, errors.New("evidence path must be a directory")
}
if info.Mode().Perm() != 0o700 {
return nil, errors.New("evidence directory must use mode 0700")
}
seen := make(map[string]os.FileInfo)
var totalBytes int64
err = filepath.WalkDir(resultsDir, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if path == resultsDir {
return nil
}
if entry.IsDir() {
relative, err := filepath.Rel(resultsDir, path)
if err != nil {
return err
}
if relative != "agents" {
return fmt.Errorf("evidence path is not allowed: %s", relative)
}
return nil
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("evidence symlink is not allowed: %s", path)
}
if !entry.Type().IsRegular() {
return fmt.Errorf("evidence file is not regular: %s", path)
}
relative, err := filepath.Rel(resultsDir, path)
if err != nil {
return err
}
fileInfo, err := entry.Info()
if err != nil {
return err
}
if !allowedEvidencePath(relative) {
return fmt.Errorf("evidence path is not allowed: %s", relative)
}
if fileInfo.Mode().Perm() != 0o600 {
return fmt.Errorf("evidence file must use mode 0600: %s", relative)
}
seen[relative] = fileInfo
if len(seen) > maxEvidenceFiles {
return errors.New("too many evidence files")
}
totalBytes += fileInfo.Size()
if totalBytes > maxEvidenceBytes {
return errors.New("evidence files exceed size limit")
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
if Redact(string(data)) != string(data) {
return fmt.Errorf("credential detected in evidence file: %s", path)
}
switch filepath.Ext(path) {
case ".json":
if !json.Valid(data) {
return fmt.Errorf("invalid JSON evidence file: %s", path)
}
case ".xml":
var document any
if err := xml.Unmarshal(data, &document); err != nil {
return fmt.Errorf("invalid XML evidence file: %s: %w", path, err)
}
}
return nil
})
return seen, err
}
func allowedEvidencePath(relative string) bool {
for _, name := range EvidenceFiles() {
if name == relative {
return true
}
}
if filepath.Dir(relative) != "agents" || filepath.Ext(relative) != ".log" {
return false
}
base := strings.TrimSuffix(filepath.Base(relative), ".log")
if base == "" || base == "." {
return false
}
for _, character := range base {
if character != '-' && character != '_' && character != '.' && (character < '0' || character > '9') && (character < 'A' || character > 'Z') && (character < 'a' || character > 'z') {
return false
}
}
return true
}
func readJSONFile[T any](resultsDir, name string) (T, error) {
var value T
data, err := os.ReadFile(filepath.Join(resultsDir, name))
if err != nil {
return value, fmt.Errorf("read %s: %w", name, err)
}
if err := json.Unmarshal(data, &value); err != nil {
return value, fmt.Errorf("parse %s: %w", name, err)
}
return value, nil
}
@@ -0,0 +1,69 @@
package evidence
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func TestEvidence_ValidateDirectoryRejectsPublicRootOrEvidenceFile(t *testing.T) {
tests := []struct {
name string
path string
}{
{name: "root", path: ""},
{name: "metadata", path: "metadata.json"},
{name: "results", path: "results.json"},
{name: "junit", path: "junit.xml"},
{name: "cleanup", path: "cleanup.json"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioRegistrationConfigExec, true, true, true)
path := dir
if test.path != "" {
path = filepath.Join(dir, test.path)
}
if err := os.Chmod(path, 0o644); err != nil {
t.Fatalf("chmod fixture: %v", err)
}
if err := ValidateDirectory(dir); err == nil {
t.Fatal("public evidence mode accepted")
}
})
}
}
func TestEvidence_ValidateDirectoryRejectsUnexpectedPaths(t *testing.T) {
tests := []struct {
name string
path string
mode os.FileMode
}{
{name: "payload", path: "payload.bin", mode: 0o600},
{name: "executable", path: "agentcompat", mode: 0o700},
{name: "nested log", path: "agents/nested/agent.log", mode: 0o600},
{name: "unknown agent file", path: "agents/agent.bin", mode: 0o600},
{name: "unknown directory", path: "unexpected/file.log", mode: 0o600},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioRegistrationConfigExec, true, true, true)
path := filepath.Join(dir, test.path)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("mkdir unexpected parent: %v", err)
}
if err := os.WriteFile(path, []byte("unexpected"), test.mode); err != nil {
t.Fatalf("write unexpected file: %v", err)
}
if err := ValidateDirectory(dir); err == nil || !strings.Contains(err.Error(), "not allowed") {
t.Fatalf("unexpected path rejection err=%v", err)
}
})
}
}
@@ -0,0 +1,174 @@
package evidence
import (
"encoding/xml"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
type cleanupEvidence struct {
Passed bool `json:"passed"`
Scenario string `json:"scenario"`
FinishedAt string `json:"finished_at"`
}
type currentEvidenceProfile struct {
RequiredFiles []string
DedicatedFile string
Executable bool
}
func currentProfile(metadata Metadata) (currentEvidenceProfile, error) {
if len(metadata.Scenarios) == 0 {
return currentEvidenceProfile{}, errors.New("metadata scenarios are required")
}
if len(metadata.Scenarios) == 1 && metadata.Scenarios[0] == contract.ScenarioMetadata {
if metadata.Fault != "" {
return currentEvidenceProfile{}, errors.New("metadata scenario does not support fault injection")
}
return currentEvidenceProfile{RequiredFiles: []string{"metadata.json"}}, nil
}
if len(metadata.Scenarios) != 1 {
return currentEvidenceProfile{}, errors.New("current evidence profile requires one supported scenario")
}
scenarioValue, err := contract.NewScenario(metadata.Scenarios[0])
if err != nil {
return currentEvidenceProfile{}, fmt.Errorf("construct metadata scenario: %w", err)
}
if !contract.IsSupportedScenario(metadata.Scenarios[0]) || metadata.Scenarios[0] == contract.ScenarioMetadata {
return currentEvidenceProfile{}, errors.New("current evidence profile requires one supported scenario")
}
faultValue := contract.Fault{}
if metadata.Fault != "" {
faultValue, err = contract.NewFault(metadata.Fault)
if err != nil {
return currentEvidenceProfile{}, fmt.Errorf("construct metadata fault: %w", err)
}
}
if err := contract.ValidateScenarioFault(scenarioValue, faultValue); err != nil {
return currentEvidenceProfile{}, err
}
definition, err := contract.ScenarioDefinitionByName(metadata.Scenarios[0])
if err != nil {
return currentEvidenceProfile{}, err
}
profile := currentEvidenceProfile{RequiredFiles: []string{"metadata.json", "results.json", "junit.xml", "cleanup.json"}, DedicatedFile: definition.DedicatedArtifactName(), Executable: true}
if profile.DedicatedFile != "" {
profile.RequiredFiles = append(profile.RequiredFiles, profile.DedicatedFile)
}
return profile, nil
}
func ValidateDirectory(resultsDir string) error {
files, err := scanDirectory(resultsDir)
if err != nil {
return err
}
if len(files) == 0 {
return errors.New("evidence directory contains no files")
}
metadata, err := readJSONFile[Metadata](resultsDir, "metadata.json")
if err != nil {
return err
}
if err := metadata.Validate(); err != nil {
return fmt.Errorf("validate metadata evidence: %w", err)
}
profile, err := currentProfile(metadata)
if err != nil {
return err
}
for _, required := range profile.RequiredFiles {
if _, exists := files[required]; !exists {
return fmt.Errorf("required evidence file is missing: %s", required)
}
}
if err := rejectStaleDedicatedFiles(files, profile.DedicatedFile); err != nil {
return err
}
if !profile.Executable {
return nil
}
results, err := readJSONFile[Results](resultsDir, "results.json")
if err != nil {
return err
}
if err := results.Validate(); err != nil {
return fmt.Errorf("validate results evidence: %w", err)
}
if results.Profile != metadata.Profile.Name || !slices.Equal(metadata.Scenarios, scenarioResultNames(results.Scenarios)) {
return errors.New("metadata and results do not agree")
}
if err := validateJUnit(resultsDir, results); err != nil {
return err
}
cleanup, err := readJSONFile[cleanupEvidence](resultsDir, "cleanup.json")
if err != nil {
return err
}
if !cleanup.Passed || cleanup.Scenario != results.Scenarios[0].Name {
return errors.New("cleanup evidence is missing or failed")
}
if _, err := time.Parse(time.RFC3339, cleanup.FinishedAt); err != nil {
return errors.New("cleanup finish time is invalid")
}
if profile.DedicatedFile != "" {
definition, err := contract.ScenarioDefinitionByName(results.Scenarios[0].Name)
if err != nil {
return err
}
if err := validateScenarioAssertions(results.Scenarios[0], definition.Assertions(metadata.Fault)); err != nil {
return err
}
return validateDedicatedArtifact(resultsDir, metadata, results.Scenarios[0])
}
return nil
}
func validateScenarioAssertions(result ScenarioResult, expected []contract.AssertionDefinition) error {
if len(result.Assertions) != len(expected) {
return errors.New("scenario assertions do not match dedicated evidence contract")
}
for index, assertion := range result.Assertions {
if assertion.Name != expected[index].Name || assertion.Passed != expected[index].Passed {
return errors.New("scenario assertions do not match dedicated evidence contract")
}
}
return nil
}
func rejectStaleDedicatedFiles(files map[string]os.FileInfo, expected string) error {
for _, name := range []string{"transfer.json", "reconnect.json"} {
if _, exists := files[name]; exists && name != expected {
return fmt.Errorf("stale or wrong dedicated evidence file: %s", name)
}
}
return nil
}
func validateJUnit(resultsDir string, results Results) error {
data, err := os.ReadFile(filepath.Join(resultsDir, "junit.xml"))
if err != nil {
return fmt.Errorf("read JUnit evidence: %w", err)
}
var suite junitSuite
if err := xml.Unmarshal(data, &suite); err != nil {
return fmt.Errorf("parse JUnit evidence: %w", err)
}
if suite.Name != results.Profile || suite.Tests != len(results.Scenarios) || suite.Failures != countFailedScenarios(results.Scenarios) || len(suite.Cases) != len(results.Scenarios) {
return errors.New("results and JUnit evidence do not agree")
}
for index, scenario := range results.Scenarios {
caseResult := suite.Cases[index]
if caseResult.Name != scenario.Name || (caseResult.Failure != nil) != !scenario.Passed {
return errors.New("results and JUnit scenario states do not agree")
}
}
return nil
}
@@ -0,0 +1,204 @@
package evidence
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func TestEvidence_CurrentProfilePropagatesMalformedScenarioAndFault(t *testing.T) {
tests := []struct {
name string
metadata Metadata
wantContext string
wantCause string
}{
{name: "scenario", metadata: Metadata{Scenarios: []string{"invalid scenario"}}, wantContext: "construct metadata scenario", wantCause: "invalid scenario name"},
{name: "fault", metadata: Metadata{Scenarios: []string{contract.ScenarioTransfer100MiB}, Fault: "invalid fault"}, wantContext: "construct metadata fault", wantCause: "invalid fault name"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := currentProfile(test.metadata)
if err == nil || !strings.Contains(err.Error(), test.wantContext) || !strings.Contains(err.Error(), test.wantCause) {
t.Fatalf("currentProfile error=%v, want context %q and cause %q", err, test.wantContext, test.wantCause)
}
})
}
}
func TestEvidence_NoCredentialsInDirectory(t *testing.T) {
if len(flag.Args()) == 0 {
t.Skip("requires a results directory argument: go test ./integration/agentcompat/internal/evidence -run TestEvidence_NoCredentialsInDirectory -args RESULTS")
}
if err := ValidateDirectory(flag.Args()[0]); err != nil {
t.Fatalf("validate evidence directory: %v", err)
}
}
func TestEvidence_NoCredentialsInDirectoryFixture(t *testing.T) {
resultsDir := t.TempDir()
metadata := validMetadata(t, resultsDir, contract.ScenarioRegistrationConfigExec)
writeJSONEvidenceFile(t, resultsDir, "metadata.json", metadata)
results := Results{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: contract.ScenarioRegistrationConfigExec, Passed: true, Assertions: []Assertion{{Name: "safe fixture", Passed: true}}}}}
writeJSONEvidenceFile(t, resultsDir, "results.json", results)
junit, err := JUnit(results)
if err != nil {
t.Fatalf("marshal safe JUnit: %v", err)
}
writeEvidenceFile(t, resultsDir, "junit.xml", string(junit))
writeEvidenceFile(t, resultsDir, "cleanup.json", `{"passed":true,"scenario":"registration-config-exec","finished_at":"2026-01-02T03:04:05Z"}`)
if err := ValidateDirectory(resultsDir); err != nil {
t.Fatalf("validate evidence directory: %v", err)
}
}
func TestEvidence_ValidateDirectoryRejectsCrossFileMismatches(t *testing.T) {
tests := map[string]func(*testing.T, string){
"profile mismatch": func(t *testing.T, dir string) {
writeExecutableEvidence(t, dir, "soak", contract.ScenarioRegistrationConfigExec, true, true, true)
},
"scenario mismatch": func(t *testing.T, dir string) {
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioRegistrationConfigExec, true, false, true)
},
"junit mismatch": func(t *testing.T, dir string) {
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioRegistrationConfigExec, false, true, true)
},
"missing cleanup": func(t *testing.T, dir string) {
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioRegistrationConfigExec, true, true, false)
},
}
for name, setup := range tests {
t.Run(name, func(t *testing.T) {
dir := t.TempDir()
setup(t, dir)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("inconsistent evidence accepted")
}
})
}
}
func TestEvidence_ValidateDirectoryRejectsFailedCleanup(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioRegistrationConfigExec, true, true, true)
writeEvidenceFile(t, dir, "cleanup.json", `{"passed":false,"scenario":"registration-config-exec","finished_at":"2026-01-02T03:04:05Z"}`)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("failed cleanup accepted")
}
}
func TestEvidence_MCPFilesystemExecutableProfileValidates(t *testing.T) {
dir := t.TempDir()
writeExecutableEvidence(t, dir, "pr-full", contract.ScenarioMCPFilesystem, true, true, true)
if err := ValidateDirectory(dir); err != nil {
t.Fatalf("validate mcp-filesystem evidence: %v", err)
}
}
func TestEvidence_NoCredentialsInDirectoryRejectsMissingPathAndFiles(t *testing.T) {
if err := ValidateDirectory(filepath.Join(t.TempDir(), "missing")); err == nil {
t.Fatal("missing evidence directory accepted")
}
for name, dir := range map[string]string{"empty": t.TempDir(), "incomplete": t.TempDir()} {
if name == "incomplete" {
writeEvidenceFile(t, dir, "metadata.json", `{ "ok": true }`)
}
if err := ValidateDirectory(dir); err == nil {
t.Fatalf("%s evidence directory accepted", name)
}
}
}
func TestEvidence_NoCredentialsInDirectoryRejectsCredentialsAndMalformedDocuments(t *testing.T) {
tests := []struct{ name, results, junit string }{
{"credentials", `{ "token": "secret-value" }`, `<testsuite></testsuite>`},
{"malformed json", `{ malformed`, `<testsuite></testsuite>`},
{"malformed xml", `{ "ok": true }`, `<testsuite>`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
writeEvidenceFile(t, dir, "metadata.json", `{ "ok": true }`)
writeEvidenceFile(t, dir, "results.json", test.results)
writeEvidenceFile(t, dir, "junit.xml", test.junit)
if err := ValidateDirectory(dir); err == nil {
t.Fatal("invalid evidence accepted")
}
})
}
}
func writeEvidenceFile(t *testing.T, resultsDir, name, content string) {
t.Helper()
if err := os.Chmod(resultsDir, 0o700); err != nil {
t.Fatalf("secure evidence root: %v", err)
}
path := filepath.Join(resultsDir, name)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("create evidence parent: %v", err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write evidence file: %v", err)
}
}
func writeJSONEvidenceFile(t *testing.T, resultsDir, name string, value any) {
t.Helper()
data, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal %s: %v", name, err)
}
writeEvidenceFile(t, resultsDir, name, string(data))
}
func validMetadata(t *testing.T, resultsDir, scenarioName string) Metadata {
t.Helper()
profile, err := contract.ProfileByName("pr-full")
if err != nil {
t.Fatalf("profile: %v", err)
}
paths, err := contract.NewPaths("/src/nezha", "/src/agent", resultsDir)
if err != nil {
t.Fatalf("paths: %v", err)
}
scenarioValue, err := contract.NewScenario(scenarioName)
if err != nil {
t.Fatalf("scenario: %v", err)
}
metadata, err := NewMetadata(MetadataInput{Profile: profile, Seed: contract.DefaultSeed, Paths: paths, ResourceBudget: contract.DefaultResourceBudget(), Scenarios: []contract.Scenario{scenarioValue}, StartedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), EvidenceFiles: EvidenceFiles()})
if err != nil {
t.Fatalf("metadata: %v", err)
}
return metadata
}
func writeExecutableEvidence(t *testing.T, dir, profileName, scenarioName string, matchingJUnit, matchingScenario, writeCleanup bool) {
t.Helper()
metadata := validMetadata(t, dir, scenarioName)
metadata.Profile.Name = profileName
writeJSONEvidenceFile(t, dir, "metadata.json", metadata)
resultsScenario := scenarioName
if !matchingScenario {
resultsScenario = contract.ScenarioMetadata
}
results := Results{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: resultsScenario, Passed: true, Assertions: []Assertion{{Name: "fixture", Passed: true}}}}}
writeJSONEvidenceFile(t, dir, "results.json", results)
junit, err := JUnit(results)
if err != nil {
t.Fatalf("JUnit: %v", err)
}
if !matchingJUnit {
junit = []byte(`<testsuite name="pr-full" tests="1" failures="1"><testcase name="other"><failure message="bad"></failure></testcase></testsuite>`)
}
writeEvidenceFile(t, dir, "junit.xml", string(junit))
if writeCleanup {
writeEvidenceFile(t, dir, "cleanup.json", fmt.Sprintf(`{"passed":true,"scenario":%q,"finished_at":"2026-01-02T03:04:05Z"}`, scenarioName))
}
}
@@ -0,0 +1,223 @@
package evidence
import (
"encoding/json"
"encoding/xml"
"strings"
"testing"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func TestEvidence_Redaction(t *testing.T) {
input := `"Authorization":"Basic authorization-secret" JWT=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature PAT=pat-secret agent_secret_key=agent-secret client_secret=config-secret https://local/mcp/upload/path-secret?token=query-secret`
redacted := Redact(input)
for _, secret := range []string{"authorization-secret", "eyJhbGciOiJIUzI1NiJ9", "pat-secret", "agent-secret", "config-secret", "path-secret", "query-secret"} {
if strings.Contains(redacted, secret) {
t.Fatalf("secret survived redaction: %q", secret)
}
}
if !strings.Contains(redacted, "[REDACTED]") {
t.Fatalf("redaction marker missing: %q", redacted)
}
junit, err := JUnit(Results{Profile: "pr-full", Passed: false, Scenarios: []ScenarioResult{{Name: "secret", Passed: false, Assertions: []Assertion{{Name: "secret failure", Passed: false}}, Error: input}}})
if err != nil {
t.Fatalf("marshal redacted junit: %v", err)
}
if strings.Contains(string(junit), "authorization-secret") || strings.Contains(string(junit), "path-secret") {
t.Fatalf("secret survived JUnit redaction: %s", junit)
}
}
func TestEvidence_Golden(t *testing.T) {
profile, err := contract.ProfileByName("pr-full")
if err != nil {
t.Fatalf("construct profile: %v", err)
}
paths, err := contract.NewPaths("/src/nezha", "/src/agent", "/tmp/results")
if err != nil {
t.Fatalf("construct paths: %v", err)
}
metadata, err := NewMetadata(MetadataInput{
Profile: profile,
Seed: contract.DefaultSeed,
Paths: paths,
ResourceBudget: contract.DefaultResourceBudget(),
StartedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
EvidenceFiles: EvidenceFiles(),
})
if err != nil {
t.Fatalf("construct metadata: %v", err)
}
data, err := json.Marshal(metadata)
if err != nil {
t.Fatalf("marshal metadata: %v", err)
}
const wantJSON = `{"agent_source":"/src/agent","evidence_files":["metadata.json","results.json","junit.xml","dashboard.log","agents/*.log","transfer.json","reconnect.json","stress.json","cleanup.json","step-summary.md"],"load_classification":"regression loads, not capacity claims","nezha_source":"/src/nezha","profile":{"name":"pr-full","job_timeout_seconds":4500,"suite_deadline_seconds":3300,"default_seed":"0x4e5a4841","agent_count":8,"stress_rounds":4,"concurrent_operations":64,"concurrent_sessions_per_kind":4,"transfer_pairs":1,"transfer_bytes":104857600,"dashboard_restart_cycles":1,"iterations":1,"stream_boundary_allowed":40,"stream_boundary_rejected":41},"resource_budget":{"warmup_runs_per_path":1,"baseline_sample_count":5,"end_sample_count":5,"sample_interval_milliseconds":250,"child_process_count_drift":0,"listener_count_drift":0,"non_stdio_fd_count_drift":0,"dashboard_rss_delta_bytes":67108864,"agent_rss_delta_bytes":33554432,"transfer_heap_bytes":16777216},"results_dir":"/tmp/results","scenarios":[],"seed":"0x4e5a4841","started_at":"2026-01-02T03:04:05Z"}`
if string(data) != wantJSON {
t.Fatalf("metadata golden mismatch\nwant: %s\ngot: %s", wantJSON, data)
}
results := Results{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: "metadata", Passed: true, Assertions: []Assertion{{Name: "metadata written", Passed: true}}}}}
resultsJSON, err := json.Marshal(results)
if err != nil {
t.Fatalf("marshal results: %v", err)
}
const wantResultsJSON = `{"profile":"pr-full","passed":true,"scenarios":[{"name":"metadata","passed":true,"assertions":[{"name":"metadata written","passed":true}]}]}`
if string(resultsJSON) != wantResultsJSON {
t.Fatalf("results golden mismatch\nwant: %s\ngot: %s", wantResultsJSON, resultsJSON)
}
junitXML, err := JUnit(results)
if err != nil {
t.Fatalf("marshal junit: %v", err)
}
const wantXML = `<testsuite name="pr-full" tests="1" failures="0"><testcase name="metadata"></testcase></testsuite>`
if string(junitXML) != wantXML {
t.Fatalf("junit golden mismatch\nwant: %s\ngot: %s", wantXML, junitXML)
}
var parsedJUnit junitSuite
if err := xml.Unmarshal(junitXML, &parsedJUnit); err != nil {
t.Fatalf("parse junit golden: %v", err)
}
if parsedJUnit.Tests != 1 || parsedJUnit.Failures != 0 || len(parsedJUnit.Cases) != 1 {
t.Fatalf("unexpected parsed junit: %#v", parsedJUnit)
}
}
func TestEvidence_SchemaValidation(t *testing.T) {
invalid := Metadata{}
if err := invalid.Validate(); err == nil {
t.Fatal("invalid metadata accepted")
}
invalidBudget := ResourceBudgetMetadata{WarmupRunsPerPath: 1, BaselineSampleCount: 5, EndSampleCount: 5, SampleIntervalMilliseconds: 250, DashboardRSSDeltaBytes: 1, AgentRSSDeltaBytes: 1}
if err := invalidBudget.Validate(); err == nil {
t.Fatal("missing transfer heap threshold accepted")
}
results := Results{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: "failed", Passed: false, Assertions: []Assertion{{Name: "failed assertion", Passed: false}}, Error: "failure"}}}
if err := results.Validate(); err == nil {
t.Fatal("inconsistent results accepted")
}
for _, result := range []Results{
{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: "../escape", Passed: true}}},
{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: "duplicate", Passed: true}, {Name: "duplicate", Passed: true}}},
} {
if err := result.Validate(); err == nil {
t.Fatalf("invalid scenario results accepted: %#v", result)
}
}
}
func TestEvidence_ResultsRejectsSuccessfulScenarioWithoutAssertions(t *testing.T) {
results := Results{Profile: "pr-full", Passed: true, Scenarios: []ScenarioResult{{Name: "registration-config-exec", Passed: true}}}
if err := results.Validate(); err == nil {
t.Fatal("successful executable scenario without assertions accepted")
}
}
func TestEvidence_ResultsJSONRedactsSecrets(t *testing.T) {
results := Results{Profile: "pr-full", Passed: false, Scenarios: []ScenarioResult{
{Name: "authorization", Passed: false, Assertions: []Assertion{{Name: "authorization failure", Passed: false}}, Error: "Authorization: Bearer jwt-secret"},
{Name: "credential-field", Passed: false, Assertions: []Assertion{{Name: "credential failure", Passed: false}}, Error: "agent_secret=agent-secret"},
{Name: "transfer-token", Passed: false, Assertions: []Assertion{{Name: "transfer failure", Passed: false}}, Error: "/mcp/download/path-token?token=query-token"},
{Name: "access-token", Passed: false, Assertions: []Assertion{{Name: "access failure", Passed: false}}, Error: "https://local/file?access_token=access-secret"},
{Name: "api-key", Passed: false, Assertions: []Assertion{{Name: "api failure", Passed: false}}, Error: "https://local/file?api_key=api-secret"},
{Name: "signature", Passed: false, Assertions: []Assertion{{Name: "signature failure", Passed: false}}, Error: "https://local/file?sig=sig-secret&signature=signature-secret&X-Amz-Signature=aws-secret"},
}}
data, err := MarshalResults(results)
if err != nil {
t.Fatalf("marshal results: %v", err)
}
for _, secret := range []string{"jwt-secret", "agent-secret", "path-token", "query-token", "access-secret", "api-secret", "sig-secret", "signature-secret", "aws-secret"} {
if strings.Contains(string(data), secret) {
t.Fatalf("secret survived results JSON: %q in %s", secret, data)
}
}
directData, err := json.Marshal(results)
if err != nil {
t.Fatalf("direct marshal results: %v", err)
}
if strings.Contains(string(directData), "jwt-secret") || strings.Contains(string(directData), "path-token") {
t.Fatalf("direct JSON marshal bypassed redaction: %s", directData)
}
}
func TestEvidence_RedactsXMLTextAttributesAndMalformedFragments(t *testing.T) {
input := `<record Authorization="Bearer authorization-secret"><agent_secret_key>agent-secret</agent_secret_key><transfer_token>transfer-secret</transfer_token><password>password-secret</password><credential>credential-secret</credential></record>`
redacted := Redact(input)
for _, secret := range []string{"authorization-secret", "agent-secret", "transfer-secret", "password-secret", "credential-secret"} {
if strings.Contains(redacted, secret) {
t.Fatalf("XML secret survived redaction: %q in %s", secret, redacted)
}
}
var parsed struct {
XMLName xml.Name `xml:"record"`
Secret string `xml:"agent_secret_key"`
}
if err := xml.Unmarshal([]byte(redacted), &parsed); err != nil {
t.Fatalf("redacted XML is not parseable: %v; output=%s", err, redacted)
}
if parsed.Secret != "[REDACTED]" {
t.Fatalf("unexpected redacted element text: %q", parsed.Secret)
}
results := Results{Profile: "pr-full", Passed: false, Scenarios: []ScenarioResult{{Name: "xml", Passed: false, Assertions: []Assertion{{Name: "xml failure", Passed: false}}, Error: input}}}
junit, err := JUnit(results)
if err != nil {
t.Fatalf("marshal XML evidence: %v", err)
}
var parsedSuite junitSuite
if err := xml.Unmarshal(junit, &parsedSuite); err != nil {
t.Fatalf("redacted JUnit is not parseable: %v; output=%s", err, junit)
}
if strings.Contains(string(junit), "agent-secret") || strings.Contains(string(junit), "credential-secret") {
t.Fatalf("XML evidence secret survived: %s", junit)
}
malformed := `<credential>config-secret`
malformedRedacted := Redact(malformed)
if strings.Contains(malformedRedacted, "config-secret") {
t.Fatalf("malformed XML secret survived redaction: %s", malformedRedacted)
}
}
func TestEvidence_RedactsXMLSensitiveAttributesWithWhitespaceAndMalformedTags(t *testing.T) {
input := `<record><agent_secret_key value="agent secret with spaces"><credential value=config-secret></credential><password data="password secret with spaces">password text</password></record>`
redacted := Redact(input)
for _, secret := range []string{"agent secret with spaces", "config-secret", "password secret with spaces", "password text"} {
if strings.Contains(redacted, secret) {
t.Fatalf("XML attribute or text secret survived redaction: %q in %s", secret, redacted)
}
}
if !strings.Contains(redacted, `<agent_secret_key value="[REDACTED]">`) {
t.Fatalf("sensitive XML attribute was not structurally redacted: %s", redacted)
}
}
func TestEvidence_RedactsCredentialAttributesWithSpacesAndTruncatedXML(t *testing.T) {
input := `<record credential="credential secret with spaces"><credential value="truncated secret`
redacted := Redact(input)
for _, secret := range []string{"credential secret with spaces", "truncated secret"} {
if strings.Contains(redacted, secret) {
t.Fatalf("credential secret survived redaction: %q in %s", secret, redacted)
}
}
}
func TestEvidence_RedactsSelfClosingSensitiveXMLWithoutBreakingDocument(t *testing.T) {
input := `<record><credential value="self-closing secret"/><result>ok</result></record>`
redacted := Redact(input)
if strings.Contains(redacted, "self-closing secret") {
t.Fatalf("self-closing XML secret survived redaction: %s", redacted)
}
var parsed struct {
XMLName xml.Name `xml:"record"`
Result string `xml:"result"`
}
if err := xml.Unmarshal([]byte(redacted), &parsed); err != nil {
t.Fatalf("redacted self-closing XML is not parseable: %v; output=%s", err, redacted)
}
if parsed.Result != "ok" {
t.Fatalf("redacted self-closing XML lost sibling content: %#v", parsed)
}
}
@@ -0,0 +1,46 @@
package evidence
import "encoding/xml"
type junitSuite struct {
XMLName xml.Name `xml:"testsuite"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"`
Cases []junitCase `xml:"testcase"`
}
type junitCase struct {
Name string `xml:"name,attr"`
Failure *junitFailure `xml:"failure,omitempty"`
}
type junitFailure struct {
Message string `xml:"message,attr"`
}
func JUnit(results Results) ([]byte, error) {
if err := results.Validate(); err != nil {
return nil, err
}
suite := junitSuite{Name: results.Profile, Tests: len(results.Scenarios)}
for _, scenario := range results.Scenarios {
caseResult := junitCase{Name: scenario.Name}
if !scenario.Passed {
suite.Failures++
caseResult.Failure = &junitFailure{Message: Redact(scenario.Error)}
}
suite.Cases = append(suite.Cases, caseResult)
}
return xml.Marshal(suite)
}
func countFailedScenarios(scenarios []ScenarioResult) int {
failed := 0
for _, scenario := range scenarios {
if !scenario.Passed {
failed++
}
}
return failed
}
@@ -0,0 +1,134 @@
package evidence
import (
"encoding/json"
"fmt"
"path/filepath"
"slices"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
type MetadataInput struct {
Profile contract.Profile
Seed contract.Seed
Paths contract.Paths
ResourceBudget contract.ResourceBudget
Scenarios []contract.Scenario
Fault contract.Fault
StartedAt time.Time
EvidenceFiles []string
}
type Metadata struct {
AgentSource string `json:"agent_source"`
EvidenceFiles []string `json:"evidence_files"`
LoadClassification string `json:"load_classification"`
NezhaSource string `json:"nezha_source"`
Profile ProfileMetadata `json:"profile"`
ResourceBudget ResourceBudgetMetadata `json:"resource_budget"`
ResultsDir string `json:"results_dir"`
Scenarios []string `json:"scenarios"`
Seed string `json:"seed"`
Fault string `json:"fault,omitempty"`
StartedAt string `json:"started_at"`
}
func NewMetadata(input MetadataInput) (Metadata, error) {
metadata := Metadata{
AgentSource: Redact(input.Paths.AgentSource().String()),
EvidenceFiles: append([]string(nil), input.EvidenceFiles...),
LoadClassification: "regression loads, not capacity claims",
NezhaSource: Redact(input.Paths.NezhaSource().String()),
Profile: profileMetadata(input.Profile),
ResourceBudget: resourceBudgetMetadata(input.ResourceBudget),
ResultsDir: Redact(input.Paths.ResultsDir().String()),
Scenarios: scenarioNames(input.Scenarios),
Seed: fmt.Sprintf("0x%x", uint64(input.Seed)),
Fault: input.Fault.String(),
StartedAt: input.StartedAt.UTC().Format(time.RFC3339),
}
if err := metadata.Validate(); err != nil {
return Metadata{}, err
}
return metadata, nil
}
func EvidenceFiles() []string {
files := []string{"metadata.json", "results.json", "junit.xml", "dashboard.log", "agents/*.log"}
for _, definition := range contract.ScenarioDefinitions() {
if name := definition.DedicatedArtifactName(); name != "" {
files = append(files, name)
}
}
return append(files, "stress.json", "cleanup.json", "step-summary.md")
}
func FixedEvidenceFiles() []string {
files := make([]string, 0, len(EvidenceFiles()))
for _, name := range EvidenceFiles() {
if !slices.Contains([]rune(name), '*') {
files = append(files, name)
}
}
return files
}
func scenarioNames(scenarios []contract.Scenario) []string {
names := make([]string, 0, len(scenarios))
for _, scenario := range scenarios {
names = append(names, scenario.String())
}
return names
}
func (metadata Metadata) Validate() error {
if metadata.AgentSource == "" || len(metadata.EvidenceFiles) == 0 || metadata.LoadClassification == "" || metadata.NezhaSource == "" || metadata.ResultsDir == "" || metadata.Seed == "" || metadata.Seed == "0x0" || metadata.StartedAt == "" {
return fmt.Errorf("metadata fields are incomplete")
}
if err := metadata.Profile.Validate(); err != nil {
return err
}
if _, err := contract.ProfileByName(metadata.Profile.Name); err != nil {
return fmt.Errorf("metadata profile is invalid: %w", err)
}
if err := metadata.ResourceBudget.Validate(); err != nil {
return err
}
if !filepath.IsAbs(metadata.AgentSource) || !filepath.IsAbs(metadata.NezhaSource) || !filepath.IsAbs(metadata.ResultsDir) {
return fmt.Errorf("metadata paths must be absolute")
}
if _, err := time.Parse(time.RFC3339, metadata.StartedAt); err != nil {
return fmt.Errorf("metadata start time is invalid: %w", err)
}
if !slices.Equal(metadata.EvidenceFiles, EvidenceFiles()) {
return fmt.Errorf("metadata evidence files are invalid")
}
seen := make(map[string]struct{}, len(metadata.Scenarios))
for _, scenarioName := range metadata.Scenarios {
_, err := contract.NewScenario(scenarioName)
if err != nil {
return fmt.Errorf("metadata scenario is invalid: %w", err)
}
if _, exists := seen[scenarioName]; exists {
return fmt.Errorf("metadata scenario is duplicated")
}
seen[scenarioName] = struct{}{}
}
if metadata.Fault != "" {
if _, err := contract.NewFault(metadata.Fault); err != nil {
return fmt.Errorf("metadata fault is invalid: %w", err)
}
}
return nil
}
func (metadata Metadata) MarshalJSON() ([]byte, error) {
type metadataWire Metadata
redacted := metadataWire(metadata)
redacted.AgentSource = Redact(redacted.AgentSource)
redacted.NezhaSource = Redact(redacted.NezhaSource)
redacted.ResultsDir = Redact(redacted.ResultsDir)
return json.Marshal(redacted)
}
@@ -0,0 +1,124 @@
package evidence
import (
"errors"
"path/filepath"
"slices"
"strings"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func validateDedicatedArtifact(resultsDir string, metadata Metadata, result ScenarioResult) error {
definition, err := contract.ScenarioDefinitionByName(result.Name)
if err != nil {
return err
}
switch definition.DedicatedArtifact {
case contract.DedicatedArtifactTransfer:
artifact, err := readJSONFile[transferArtifact](resultsDir, "transfer.json")
if err != nil {
return err
}
return validateTransferArtifact(metadata, result, artifact)
case contract.DedicatedArtifactReconnect:
artifact, err := readJSONFile[reconnectArtifact](resultsDir, "reconnect.json")
if err != nil {
return err
}
return validateReconnectArtifact(metadata, result, artifact)
case contract.DedicatedArtifactNone:
return nil
default:
return errors.New("unsupported dedicated artifact kind")
}
}
func validateReconnectArtifact(metadata Metadata, result ScenarioResult, artifact reconnectArtifact) error {
if err := validateArtifactHeader(metadata, result, artifact.Scenario, artifact.Fault, artifact.Passed, artifact.CleanupOK, artifact.Error); err != nil {
return err
}
switch artifact.Fault {
case "":
if !artifact.Passed {
return errors.New("reconnect success artifact reports failure")
}
if err := validateReconnectSuccess(artifact.Evidence); err != nil {
return err
}
if err := validateReconnectSuccessReceipts(artifact.Evidence); err != nil {
return err
}
return validateReconnectFinalEvidence(artifact.Evidence, 2)
case contract.FaultDashboardExit:
if artifact.Passed || !strings.Contains(artifact.Error, "injected Dashboard exit") {
return errors.New("dashboard-exit artifact does not identify the injected failure")
}
evidence := artifact.Evidence
if reconnectPostFaultEvidencePresent(evidence) {
return errors.New("dashboard-exit artifact presents stale reconnect success")
}
if evidence.Lifecycle.DisconnectAt.IsZero() || !evidence.Lifecycle.ReconnectAt.IsZero() || !evidence.Lifecycle.OutsideRootSentinelUnchanged {
return errors.New("dashboard-exit lifecycle evidence is invalid")
}
return validateReconnectFinalEvidence(evidence, 1)
default:
return errors.New("reconnect artifact has unsupported fault")
}
}
func reconnectPostFaultEvidencePresent(evidence reconnectEvidence) bool {
return evidence.Runtime.DashboardAfter != (runtimeIdentity{}) || evidence.Runtime.AgentAfter != (runtimeIdentity{}) ||
evidence.Runtime.StateGenerationBeforeAgentRestart != 0 || evidence.Runtime.StateGenerationAfterAgentRestart != 0 ||
evidence.Identity.DashboardConfigUnchanged || evidence.Identity.AgentConfigUnchanged || evidence.Identity.DashboardFixtureUnchanged || evidence.Identity.ClientsRecreated || evidence.Identity.BootstrapRecreated ||
!evidence.Lifecycle.ReconnectAt.IsZero() || evidence.Lifecycle.ReconnectInterval != 0 || len(evidence.Lifecycle.DashboardReceipts) != 0 || len(evidence.Lifecycle.AgentReceipts) != 0 ||
evidence.Lifecycle.StaleGenerationReceipts != 0 || evidence.Lifecycle.DuplicateTaskIDs != 0 || evidence.Lifecycle.LostResultIDs != 0 ||
evidence.Observation.ServerID != 0 || evidence.Observation.UUID != "" || evidence.Observation.OldGeneration != 0 || evidence.Observation.NewGeneration != 0 ||
!evidence.Observation.DisconnectAt.IsZero() || !evidence.Observation.ReconnectAt.IsZero() || len(evidence.Observation.TaskIDs) != 0 || len(evidence.Observation.ResultIDs) != 0 || evidence.Observation.PostReconnect || evidence.Observation.AgentRestarted
}
func validateReconnectSuccess(evidence reconnectEvidence) error {
observation := evidence.Observation
if observation.ServerID == 0 || observation.UUID == "" || observation.OldGeneration == 0 || observation.NewGeneration <= observation.OldGeneration || observation.DisconnectAt.IsZero() || !observation.ReconnectAt.After(observation.DisconnectAt) || len(observation.TaskIDs) != 5 || !slices.Equal(observation.TaskIDs, observation.ResultIDs) || !observation.PostReconnect || !observation.AgentRestarted {
return errors.New("reconnect observation evidence is invalid")
}
if evidence.Runtime.DashboardAfter.Generation <= evidence.Runtime.DashboardBefore.Generation || evidence.Runtime.DashboardAfter.PID == evidence.Runtime.DashboardBefore.PID || evidence.Runtime.AgentAfter.Generation <= evidence.Runtime.AgentBefore.Generation || evidence.Runtime.AgentAfter.PID == evidence.Runtime.AgentBefore.PID || evidence.Runtime.StateGenerationAfterAgentRestart <= evidence.Runtime.StateGenerationBeforeAgentRestart {
return errors.New("reconnect runtime generations are invalid")
}
identity := evidence.Identity
if identity.ServerID != observation.ServerID || identity.UUID != observation.UUID || !identity.DashboardConfigUnchanged || !identity.AgentConfigUnchanged || !identity.DashboardFixtureUnchanged || !identity.ClientsRecreated || !identity.BootstrapRecreated {
return errors.New("reconnect identity evidence is invalid")
}
if evidence.Lifecycle.StaleGenerationReceipts != 0 || evidence.Lifecycle.DuplicateTaskIDs != 0 || evidence.Lifecycle.LostResultIDs != 0 || !evidence.Lifecycle.OutsideRootSentinelUnchanged {
return errors.New("reconnect lifecycle accounting is invalid")
}
return nil
}
func validateReconnectSuccessReceipts(evidence reconnectEvidence) error {
if len(evidence.Lifecycle.DashboardReceipts) != 3 || len(evidence.Lifecycle.AgentReceipts) != 2 || len(evidence.Observation.TaskIDs) != 5 {
return errors.New("reconnect receipt accounting is not exact")
}
if evidence.Lifecycle.DisconnectAt.IsZero() || evidence.Lifecycle.ReconnectAt.IsZero() || evidence.Lifecycle.ReconnectAt.Sub(evidence.Lifecycle.DisconnectAt) != evidence.Lifecycle.ReconnectInterval {
return errors.New("reconnect timestamp interval is inconsistent")
}
if !evidence.Observation.DisconnectAt.Equal(evidence.Lifecycle.DisconnectAt) || !evidence.Observation.ReconnectAt.Equal(evidence.Lifecycle.ReconnectAt) {
return errors.New("reconnect lifecycle and observation timestamps differ")
}
return nil
}
func validateReconnectFinalEvidence(evidence reconnectEvidence, expectedProcessCount int) error {
if !evidence.AgentCleanup.Passed || evidence.AgentCleanup.Forced || !evidence.DashboardCleanup.Passed || evidence.DashboardCleanup.Forced {
return errors.New("reconnect cleanup receipts failed")
}
if len(evidence.AgentCleanup.Processes) != expectedProcessCount || len(evidence.DashboardCleanup.Processes) != expectedProcessCount {
return errors.New("reconnect cleanup receipt process count is invalid")
}
for _, path := range []string{evidence.Fixture.Dashboard.WorkspaceRoot, evidence.Fixture.AgentRoot, evidence.Fixture.Dashboard.ConfigPath, evidence.Fixture.AgentConfigPath, evidence.Fixture.Dashboard.BinaryPath, evidence.Fixture.AgentBinaryPath} {
if path == "" || !filepath.IsAbs(path) {
return errors.New("reconnect fixture paths are invalid")
}
}
return nil
}
@@ -0,0 +1,41 @@
package evidence
import (
"regexp"
"strings"
)
type redactionRule struct {
pattern *regexp.Regexp
replace string
}
var redactionRules = []redactionRule{
{pattern: regexp.MustCompile(`(?i)(["']?authorization["']?\s*[:=]\s*["']?)[^"'\r\n,}]+(["']?)`), replace: `${1}[REDACTED]${2}`},
{pattern: regexp.MustCompile(`(?i)(\b["']?(?:token|access[_-]?token|api[_-]?(?:key|token)|jwt[_-]?secret(?:[_-]?key)?|jwt[_-]?token|pat|agent[_-]?secret(?:[_-]?key)?|client[_-]?secret|handshake[_-]?secret|revert[_-]?handshake[_-]?secret|password|credential|transfer[_-]?token)["']?\s*[:=]\s*["']?)[^"'\s,;&}]+(["']?)`), replace: `${1}[REDACTED]${2}`},
{pattern: regexp.MustCompile(`(?i)([?&](?:token|access_token|api_key|jwt|pat|secret|authorization|sig|signature|x-amz-signature)=)[^&#\s]+`), replace: `${1}[REDACTED]`},
{pattern: regexp.MustCompile(`(?i)(/mcp/(?:download|upload)/)[A-Za-z0-9._-]+`), replace: `${1}[REDACTED]`},
{pattern: regexp.MustCompile(`\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b`), replace: `[REDACTED]`},
{pattern: regexp.MustCompile(`\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b`), replace: `[REDACTED]`},
{pattern: regexp.MustCompile(`(?is)(<\s*(?:agent[_-]?secret(?:[_-]?key)?|authorization|transfer[_-]?token|password|credential|config[_-]?secret)\b)[^>]*?(/?)>`), replace: `${1} value="[REDACTED]"${2}>`},
{pattern: regexp.MustCompile(`(?is)(<\s*(?:agent[_-]?secret(?:[_-]?key)?|authorization|transfer[_-]?token|password|credential|config[_-]?secret)\b[^>]*>)[^<]*(</\s*(?:agent[_-]?secret(?:[_-]?key)?|authorization|transfer[_-]?token|password|credential|config[_-]?secret)\s*>)`), replace: `${1}[REDACTED]${2}`},
{pattern: regexp.MustCompile(`(?is)(<\s*(?:agent[_-]?secret(?:[_-]?key)?|authorization|transfer[_-]?token|password|credential|config[_-]?secret)\b[^>]*>)[^<]*$`), replace: `${1}[REDACTED]`},
{pattern: regexp.MustCompile(`(?is)(<\s*(?:agent[_-]?secret(?:[_-]?key)?|authorization|transfer[_-]?token|password|credential|config[_-]?secret)\b)[^>]*$`), replace: `${1} value="[REDACTED]"`},
}
var sensitiveXMLAttribute = regexp.MustCompile(`(?i)(\b(?:agent[_-]?secret(?:[_-]?key)?|authorization|transfer[_-]?token|password|credential|config[_-]?secret)\b\s*=\s*)(?:"[^"]*"|'[^']*'|"[^\r\n>]*|'[^'\r\n>]*'|[^\s/>][^/>]*)`)
func Redact(input string) string {
redacted := sensitiveXMLAttribute.ReplaceAllStringFunc(input, func(attribute string) string {
separator := strings.IndexByte(attribute, '=')
if separator < 0 {
return attribute
}
prefix := attribute[:separator+1]
return prefix + "\"[REDACTED]\""
})
for _, rule := range redactionRules {
redacted = rule.pattern.ReplaceAllString(redacted, rule.replace)
}
return redacted
}
@@ -0,0 +1,32 @@
package evidence
import (
"slices"
"testing"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
func TestEvidence_RegistryDefinitionsDriveManifestAndProfiles(t *testing.T) {
manifest := EvidenceFiles()
for _, definition := range contract.ScenarioDefinitions() {
dedicatedName := definition.DedicatedArtifactName()
if dedicatedName != "" && !slices.Contains(manifest, dedicatedName) {
t.Fatalf("dedicated artifact %q absent from manifest", dedicatedName)
}
metadata := validMetadata(t, t.TempDir(), definition.Name)
profile, err := currentProfile(metadata)
if err != nil {
t.Fatalf("profile for %q: %v", definition.Name, err)
}
if profile.DedicatedFile != dedicatedName {
t.Fatalf("scenario %q dedicated file=%q want=%q", definition.Name, profile.DedicatedFile, dedicatedName)
}
if definition.Execution == contract.ScenarioExecutionMetadata && profile.Executable {
t.Fatal("metadata profile unexpectedly executable")
}
if definition.Execution != contract.ScenarioExecutionMetadata && !profile.Executable {
t.Fatalf("scenario %q profile is not executable", definition.Name)
}
}
}
@@ -0,0 +1,105 @@
package evidence
import (
"encoding/json"
"fmt"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
)
type ScenarioResult struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Assertions []Assertion `json:"assertions,omitempty"`
Error string `json:"error,omitempty"`
}
type Assertion struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Details string `json:"details,omitempty"`
}
type Results struct {
Profile string `json:"profile"`
Passed bool `json:"passed"`
Scenarios []ScenarioResult `json:"scenarios"`
}
func (results Results) Validate() error {
if results.Profile == "" || len(results.Scenarios) == 0 {
return fmt.Errorf("results fields are incomplete")
}
if _, err := contract.ProfileByName(results.Profile); err != nil {
return fmt.Errorf("results profile is invalid: %w", err)
}
allPassed := true
seenScenarios := make(map[string]struct{}, len(results.Scenarios))
for _, scenario := range results.Scenarios {
if _, err := contract.NewScenario(scenario.Name); err != nil {
return fmt.Errorf("scenario name is invalid: %w", err)
}
if _, exists := seenScenarios[scenario.Name]; exists {
return fmt.Errorf("scenario name is duplicated")
}
seenScenarios[scenario.Name] = struct{}{}
if len(scenario.Assertions) == 0 {
return fmt.Errorf("scenario must contain assertions")
}
assertionsPassed := true
seenAssertions := make(map[string]struct{}, len(scenario.Assertions))
for _, assertion := range scenario.Assertions {
if assertion.Name == "" {
return fmt.Errorf("assertion name is required")
}
if _, exists := seenAssertions[assertion.Name]; exists {
return fmt.Errorf("assertion name is duplicated")
}
seenAssertions[assertion.Name] = struct{}{}
assertionsPassed = assertionsPassed && assertion.Passed
}
if scenario.Passed != assertionsPassed {
return fmt.Errorf("scenario pass state is inconsistent with assertions")
}
if !scenario.Passed {
if scenario.Error == "" {
return fmt.Errorf("failed scenario error is required")
}
allPassed = false
} else if scenario.Error != "" {
return fmt.Errorf("passed scenario cannot contain an error")
}
}
if results.Passed != allPassed {
return fmt.Errorf("results pass state is inconsistent")
}
return nil
}
func MarshalResults(results Results) ([]byte, error) {
if err := results.Validate(); err != nil {
return nil, err
}
return json.Marshal(results)
}
func (results Results) MarshalJSON() ([]byte, error) {
type resultsWire Results
redacted := resultsWire{Profile: results.Profile, Passed: results.Passed, Scenarios: make([]ScenarioResult, 0, len(results.Scenarios))}
for _, scenario := range results.Scenarios {
assertions := make([]Assertion, 0, len(scenario.Assertions))
for _, assertion := range scenario.Assertions {
assertions = append(assertions, Assertion{Name: assertion.Name, Passed: assertion.Passed, Details: Redact(assertion.Details)})
}
redacted.Scenarios = append(redacted.Scenarios, ScenarioResult{Name: scenario.Name, Passed: scenario.Passed, Assertions: assertions, Error: Redact(scenario.Error)})
}
return json.Marshal(redacted)
}
func scenarioResultNames(scenarios []ScenarioResult) []string {
names := make([]string, 0, len(scenarios))
for _, scenario := range scenarios {
names = append(names, scenario.Name)
}
return names
}