Files
pulse/internal/api/docker_container_action_result_test.go
T
2026-07-16 14:36:01 +01:00

139 lines
7.6 KiB
Go

package api
import (
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestDockerContainerExecutionResultDerivesTruthOnlyFromFacts(t *testing.T) {
now := time.Now().UTC()
req := agentexec.DockerContainerLifecyclePayload{Operation: agentexec.DockerContainerOperationRestart}
tests := []struct {
name string
facts agentexec.DockerContainerLifecycleResultPayload
execution unified.ActionExecutionStatus
verification unified.ActionVerificationStatus
class unified.ActionEvidenceClass
}{
{name: "preflight no effect", facts: dockerResultFacts(now, false, false, false, true), execution: unified.ActionExecutionNotRun, verification: unified.ActionVerificationInconclusive, class: unified.ActionEvidenceNone},
{name: "completed confirmed", facts: dockerResultFacts(now, true, true, true, true), execution: unified.ActionExecutionSucceeded, verification: unified.ActionVerificationConfirmed, class: unified.ActionEvidenceAgentAttested},
{name: "completed contradicted", facts: dockerResultFacts(now, true, true, true, false), execution: unified.ActionExecutionSucceeded, verification: unified.ActionVerificationContradicted, class: unified.ActionEvidenceAgentAttested},
{name: "partial unknown", facts: dockerResultFacts(now, true, false, false, false), execution: unified.ActionExecutionInconclusive, verification: unified.ActionVerificationInconclusive, class: unified.ActionEvidenceNone},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := dockerContainerExecutionResult("app-container:fixture", "agent-1", req, tc.facts, nil, now)
if err != nil {
t.Fatal(err)
}
if result.ActionResultV2.Execution.Status != tc.execution || result.ActionResultV2.Verification.Status != tc.verification || result.ActionResultV2.Verification.EvidenceClass != tc.class {
t.Fatalf("truth = %#v", result.ActionResultV2)
}
if result.ActionResultV2.Compensation.Support != unified.ActionCompensationUnavailable {
t.Fatalf("restart acquired rollback: %#v", result.ActionResultV2.Compensation)
}
})
}
}
func TestDockerContainerExecutionResultStaleReadbackIsInconclusive(t *testing.T) {
now := time.Now().UTC()
facts := dockerResultFacts(now.Add(-time.Hour), true, true, true, true)
result, err := dockerContainerExecutionResult("app-container:fixture", "agent-1", agentexec.DockerContainerLifecyclePayload{Operation: agentexec.DockerContainerOperationRestart}, facts, nil, now)
if err != nil {
t.Fatal(err)
}
if result.ActionResultV2.Verification.Status != unified.ActionVerificationInconclusive || result.ActionResultV2.Verification.ReasonCode != "stale_agent_readback" {
t.Fatalf("verification = %#v", result.ActionResultV2.Verification)
}
}
func TestDockerContainerExecutionResultClampsBoundedPositiveAgentClockSkew(t *testing.T) {
receivedAt := time.Now().UTC()
facts := dockerResultFacts(receivedAt.Add(2*time.Second), true, true, true, true)
result, err := dockerContainerExecutionResult("app-container:fixture", "agent-1", agentexec.DockerContainerLifecyclePayload{Operation: agentexec.DockerContainerOperationRestart}, facts, nil, receivedAt)
if err != nil {
t.Fatal(err)
}
verification := result.ActionResultV2.Verification
if verification.Status != unified.ActionVerificationConfirmed || len(verification.Evidence) != 1 {
t.Fatalf("verification = %#v", verification)
}
if !verification.Evidence[0].ObservedAt.Equal(receivedAt) || !verification.Evidence[0].ReceivedAt.Equal(receivedAt) {
t.Fatalf("evidence timestamps = observed %s received %s, want receipt boundary %s", verification.Evidence[0].ObservedAt, verification.Evidence[0].ReceivedAt, receivedAt)
}
}
func TestDockerContainerExecutionResultRejectsExcessivePositiveAgentClockSkew(t *testing.T) {
receivedAt := time.Now().UTC()
facts := dockerResultFacts(receivedAt.Add(6*time.Minute), true, true, true, true)
result, err := dockerContainerExecutionResult("app-container:fixture", "agent-1", agentexec.DockerContainerLifecyclePayload{Operation: agentexec.DockerContainerOperationRestart}, facts, nil, receivedAt)
if err != nil {
t.Fatal(err)
}
verification := result.ActionResultV2.Verification
if verification.Status != unified.ActionVerificationInconclusive || verification.ReasonCode != "stale_agent_readback" || len(verification.Evidence) != 0 {
t.Fatalf("verification = %#v", verification)
}
}
func TestDockerContainerUpdateExecutionResultClampsBoundedPositiveAgentClockSkew(t *testing.T) {
receivedAt := time.Now().UTC()
observedAt := receivedAt.Add(2 * time.Second)
facts := agentexec.DockerContainerUpdateResultPayload{
Operation: agentexec.DockerContainerOperationUpdate, ActionID: "action-update", ExecutionPhase: agentexec.DockerContainerPhaseComplete,
MutationStarted: true, MutationCompleted: true, ReadbackRan: true, NewContainerID: dockerLifecycleTestID,
After: agentexec.DockerContainerLifecycleSnapshot{ContainerID: dockerLifecycleTestID, State: "running", Running: true, ObservedAt: observedAt},
}
result, err := dockerContainerUpdateExecutionResult("app-container:fixture", "agent-1", facts, nil, receivedAt)
if err != nil {
t.Fatal(err)
}
verification := result.ActionResultV2.Verification
if verification.Status != unified.ActionVerificationConfirmed || len(verification.Evidence) != 1 {
t.Fatalf("verification = %#v", verification)
}
if !verification.Evidence[0].ObservedAt.Equal(receivedAt) || !verification.Evidence[0].ReceivedAt.Equal(receivedAt) {
t.Fatalf("evidence timestamps = observed %s received %s, want receipt boundary %s", verification.Evidence[0].ObservedAt, verification.Evidence[0].ReceivedAt, receivedAt)
}
}
func TestDockerContainerExecutionResultClassifiesDistinctDaemonObserverAsIndependent(t *testing.T) {
now := time.Now().UTC()
facts := dockerResultFacts(now, true, true, true, true)
facts.ContainerID = dockerLifecycleTestID
req := agentexec.DockerContainerLifecyclePayload{ActionID: "action-1", Operation: agentexec.DockerContainerOperationRestart}
observation := &dockerContainerPostconditionObservation{
ObserverID: "colima-direct-cli", TrustDomain: "daemon:colima-direct", Method: "docker_api_inspect",
Snapshot: facts.After, ReceivedAt: now,
}
result, err := dockerContainerExecutionResult("app-container:fixture", "agent-1", req, facts, observation, now)
if err != nil {
t.Fatal(err)
}
truth := result.ActionResultV2.Verification
if truth.Status != unified.ActionVerificationConfirmed || truth.EvidenceClass != unified.ActionEvidenceIndependent || len(truth.Evidence) != 1 || truth.Evidence[0].Digest == "" || len(truth.Evidence[0].Refs) != 1 {
t.Fatalf("independent verification = %#v", truth)
}
}
func dockerResultFacts(now time.Time, started, completed, readback, matches bool) agentexec.DockerContainerLifecycleResultPayload {
beforeStart := now.Add(-time.Minute)
afterStart := now
state, running := "running", true
if !matches {
afterStart = beforeStart
state, running = "exited", false
}
return agentexec.DockerContainerLifecycleResultPayload{
ExecutionPhase: agentexec.DockerContainerPhaseComplete, MutationStarted: started, MutationCompleted: completed, ReadbackRan: readback,
Before: agentexec.DockerContainerLifecycleSnapshot{ContainerID: dockerLifecycleTestID, State: "running", Running: true, StartedAt: beforeStart, ObservedAt: now.Add(-time.Second)},
After: agentexec.DockerContainerLifecycleSnapshot{ContainerID: dockerLifecycleTestID, State: state, Running: running, StartedAt: afterStart, ObservedAt: now},
}
}
const dockerLifecycleTestID = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"