Restore release-blocking backend contracts

This commit is contained in:
rcourtman
2026-07-13 21:51:33 +01:00
parent 252754ee4f
commit 71a3b6ebcd
15 changed files with 223 additions and 107 deletions
@@ -3644,13 +3644,16 @@ may keep first-party model-facing descriptions, but it must not re-declare
required arguments such as `resolution_note` or dismissal `note` when the
canonical Pulse Intelligence manifest and API contract mark those fields
optional.
Legacy native Assistant utility provider aliases for `run_command`, `fetch_url`,
and `set_resource_url`, plus their provider JSON schemas, are owned by
Legacy native Assistant utility provider aliases for `fetch_url` and
`set_resource_url`, plus their provider JSON schemas, are owned by
`agentcapabilities.LegacyAssistantUtilityProviderTools`. The older native
Assistant service may continue to expose those compatibility aliases while the
execution migration proceeds, but it must consume the shared provider projection
and shared argument constants rather than carrying inline schema maps or local
tool-input string keys.
tool-input string keys. The legacy `run_command` alias is retired: the Assistant
must not offer it, and a fabricated invocation must fail closed before any
command dispatch. Raw command execution remains available only through the
governed capability and action-lifecycle path.
Native Assistant registry tools that operate on Patrol finding lifecycle state
must also consume the same `agentcapabilities` argument vocabulary for
`finding_id`, `resolution_note`, `reason`, and `note` instead of repeating
@@ -5855,6 +5858,15 @@ biometric claims never satisfy an MFA floor. Until the core step-up verifier
accepts action-bound cryptographic evidence, the honest runtime outcome is
step-up unavailable, not MFA approved.
The legacy Patrol approval bridge must project stored approvals into that same
server-owned model before a lifecycle decision is appended. It binds a scoped
service requester, the deciding human `ActorBinding`, and method/session
`ApprovalEvidence` to the exact action, plan hash, outcome, organization, and
decision time. A zero-version legacy approval requirement is upgraded to the
current canonical floor before evaluation; missing or inconsistent authority
fails closed. A dry-run plan is never executable and must persist refusal
without minting decision approval authority.
Patrol action proposals persist the exact bounded policy authorities consulted
at planning time through the canonical unified-resource `policyDecision`
object. Capability, tenant Patrol, and resource operator factors retain typed
@@ -5907,6 +5919,10 @@ exercised through an exact empty-parameter proposal, shared planning/policy and
human approval, durable typed dispatch, canonical terminal audit, and finding
resolution for both workflows. Finding evidence stays bounded and contains no
command, path, package selector, raw APT output, stderr, or reboot authority.
The APT verifier claims only canonical APT finding keys. It must return
unhandled for CPU, memory, disk, and every other non-APT key so the owning
deterministic verifier can evaluate that finding; APT resource lookup failure
must never intercept unrelated verification.
Fake-only callback-loss tests reopen the server-side action store and consume
Task 07 terminal receipts without resending either typed mutation, then
@@ -945,6 +945,12 @@ so the Kubernetes platform-page Configuration tab exercises the same
RBAC summary-count contract live agents use. The
`TestKubernetesDemoClustersTellDistinctStories` test in
`internal/mock/demo_scenarios_test.go` guards that distribution.
Generic Kubernetes fixture synthesis must also preserve at least one ready,
schedulable node whenever a cluster has nodes. Randomized readiness and cordon
stories may degrade the remaining nodes, but they must not accidentally create
a total outage that erases running-pod metrics or makes the demo and its proof
nondeterministic; explicit curated outage scenarios remain the owner of
cluster-wide unavailability.
Bumps to those defaults must keep the
curated demo scenario's per-node hostname seasoning in
`demo_scenarios.go` aligned (today: pve1..pve6 with regional labels,
@@ -1819,7 +1819,10 @@ and malformed evidence remain explicitly inconclusive. Legacy `Success` and
Canonical evidence is bounded before redaction and digested as SHA-256 over
the canonical redacted envelope. Redaction deep-copies nested evidence and
fails closed to `redaction_contract_violation`; it never preserves invalid or
unredacted input. A present but malformed stored V2 also fails closed to
unredacted input. Result normalization is copy-on-normalize across evidence,
verification, and nested compensation, so callers may safely normalize shared
immutable snapshots without mutating their stored input or racing other
readers. A present but malformed stored V2 also fails closed to
`result_v2_invalid`; legacy booleans can never override it. Compensation truth
never rewrites the primary result and carries declared trigger, durable
attempt/step identity, timing, nested execution and verification, and restored
+3
View File
@@ -188,6 +188,9 @@ func formatAPTBytes(value int64) string {
}
func verifyAPTWorkflowFinding(state patrolRuntimeState, findingKey, resourceID string, now time.Time) (bool, bool, error) {
if findingKey != aptHostUpdateFindingKey && findingKey != aptPackageCacheFindingKey {
return false, false, nil
}
if state.readState == nil {
state = state.withDerivedProviders()
}
+20 -74
View File
@@ -142,17 +142,13 @@ func TestService_GetTools(t *testing.T) {
t.Error("Expected tools")
}
// Verify some common tools are present
foundRunCommand := false
// The retired raw-command alias must not be projected even when the legacy
// control level is configured to allow governed actions.
for _, tool := range tools {
if tool.Name == agentcapabilities.LegacyAssistantRunCommandToolName {
foundRunCommand = true
break
t.Fatal("retired run_command alias was projected to the provider")
}
}
if !foundRunCommand {
t.Error("Expected run_command tool to be available")
}
}
func TestService_GetToolsUsesSharedLegacyUtilitySchemas(t *testing.T) {
@@ -815,10 +811,15 @@ func TestService_GetModelForRequest(t *testing.T) {
}
func TestService_ExecuteTool_RunCommand(t *testing.T) {
dispatched := false
mockAgentServer := &mockAgentServer{
agents: []agentexec.ConnectedAgent{
{Hostname: "node-1", AgentID: "agent-1"},
},
executeFunc: func(context.Context, string, agentexec.ExecuteCommandPayload) (*agentexec.CommandResultPayload, error) {
dispatched = true
return &agentexec.CommandResultPayload{Success: true}, nil
},
}
svc := NewService(nil, mockAgentServer)
svc.cfg = &config.AIConfig{ControlLevel: config.ControlLevelControlled}
@@ -841,57 +842,12 @@ func TestService_ExecuteTool_RunCommand(t *testing.T) {
Context: map[string]interface{}{"node": "node-1"},
}
// 1. Allowed command
result, exec := svc.executeTool(ctx, req, tc)
if !exec.Success {
t.Errorf("Expected success, got failure: %s", result)
if exec.Success || !strings.Contains(result, "Invocation blocked") {
t.Fatalf("retired run_command did not fail closed: success=%v result=%q", exec.Success, result)
}
// 2. Blocked command
mockPolicy.decision = agentexec.PolicyBlock
result, exec = svc.executeTool(ctx, req, tc)
if exec.Success {
t.Error("Expected failure for blocked command")
}
if !strings.Contains(result, "blocked") {
t.Errorf("Expected blocked message, got %s", result)
}
// 3. Approval required (non-autonomous)
mockPolicy.decision = agentexec.PolicyRequireApproval
svc.cfg = &config.AIConfig{ControlLevel: config.ControlLevelControlled}
result, exec = svc.executeTool(ctx, req, tc)
if !exec.Success {
t.Errorf("Expected success (not an error to need approval), got: %s", result)
}
if !strings.Contains(result, "APPROVAL_REQUIRED") {
t.Errorf("Expected APPROVAL_REQUIRED message, got %s", result)
}
// 4. Request-scoped approval mode must clamp an autonomous default.
autonomousMode := false
svc.cfg = &config.AIConfig{ControlLevel: config.ControlLevelAutonomous}
req.AutonomousMode = &autonomousMode
result, exec = svc.executeTool(ctx, req, tc)
if !exec.Success {
t.Errorf("Expected success (not an error to need approval), got: %s", result)
}
if !strings.Contains(result, "APPROVAL_REQUIRED") {
t.Errorf("Expected request-scoped approval mode to require approval, got %s", result)
}
// 5. Approval-bound handoffs require approval even for policy-allowed commands.
mockPolicy.decision = agentexec.PolicyAllow
req.RequireCommandApproval = true
result, exec = svc.executeTool(ctx, req, tc)
if !exec.Success {
t.Errorf("Expected success (not an error to need approval), got: %s", result)
}
if !strings.Contains(result, "APPROVAL_REQUIRED") {
t.Errorf("Expected approval-bound handoff to require approval, got %s", result)
}
if !strings.Contains(result, "handoff requires operator approval") {
t.Errorf("Expected handoff approval reason, got %s", result)
if dispatched {
t.Fatal("retired run_command reached the agent transport")
}
}
@@ -1282,25 +1238,16 @@ func TestService_ExecuteTool_RunCommand_WithTargetHost(t *testing.T) {
}
result, exec := svc.executeTool(context.Background(), req, tc)
if !exec.Success {
t.Errorf("Expected success, got failure: %s", result)
}
if exec.Input != "[target-node] uptime" {
t.Errorf("Expected input to show target-node, got: %s", exec.Input)
if exec.Success || !strings.Contains(result, "Invocation blocked") {
t.Fatalf("retired run_command did not fail closed: success=%v result=%q", exec.Success, result)
}
changes, err := canonicalStore.GetRecentChanges("vm-100", time.Time{}, 10)
if err != nil {
t.Fatalf("GetRecentChanges: %v", err)
}
if len(changes) != 1 {
t.Fatalf("expected 1 canonical command change, got %d", len(changes))
}
if changes[0].Kind != unifiedresources.ChangeCommandExecuted {
t.Fatalf("Kind = %q, want %q", changes[0].Kind, unifiedresources.ChangeCommandExecuted)
}
if got := changes[0].Metadata["alert_identifier"]; got != "alert-123" {
t.Fatalf("alert_identifier = %#v, want alert-123", got)
if len(changes) != 0 {
t.Fatalf("retired run_command wrote canonical change history: %#v", changes)
}
}
@@ -3244,12 +3191,11 @@ func TestService_FindingContextCommandRouting(t *testing.T) {
result, exec := svc.executeTool(context.Background(), req, tc)
if !exec.Success {
t.Errorf("Command execution failed: %s", result)
if exec.Success || !strings.Contains(result, "Invocation blocked") {
t.Fatalf("retired run_command did not fail closed: success=%v result=%q", exec.Success, result)
}
// Verify it routed to the correct agent (minipc, not pve-node)
if routedToAgent != "agent-minipc" {
t.Errorf("Expected command to route to agent-minipc, but went to: %s", routedToAgent)
if routedToAgent != "" {
t.Fatalf("retired run_command reached agent routing: %s", routedToAgent)
}
}
+2 -2
View File
@@ -1184,8 +1184,8 @@ func TestService_ExecuteTool(t *testing.T) {
}
output, exec := svc.executeTool(ctx, req, tc)
if !containsString(output, "agent server not available") {
t.Errorf("Expected agent server error, got: %s", output)
if !containsString(output, "Invocation blocked") {
t.Errorf("expected retired run_command to fail closed, got: %s", output)
}
if exec.Success {
t.Error("Expected failure")
+79 -22
View File
@@ -84,7 +84,7 @@ func (e *PulseToolExecutor) executeCommandWithAudit(
if planFromApproval {
plan = mergeApprovedActionPlan(*approvalReq.Plan, plan)
}
approvalRecords := approvalRecordsForID(approvalID)
approvalRecords := approvalRecordsForID(approvalID, &plan)
record := unifiedresources.ActionAuditRecord{
ID: actionID,
@@ -293,7 +293,7 @@ func (e *PulseToolExecutor) executeNativeActionWithAudit(
if planFromApproval {
plan = mergeApprovedActionPlan(*approvalReq.Plan, plan)
}
approvalRecords := approvalRecordsForID(approvalID)
approvalRecords := approvalRecordsForID(approvalID, &plan)
record := unifiedresources.ActionAuditRecord{
ID: actionID,
@@ -371,6 +371,15 @@ func (e *PulseToolExecutor) recordActionExecutionStart(record unifiedresources.A
actor = approvalAuditActor
}
if planFromApproval {
if record.Plan.ApprovalPolicy == unifiedresources.ApprovalDryRun {
refusalRecord := record
if e != nil && e.actionAuditStore != nil {
if current, found, err := e.actionAuditStore.GetActionAudit(record.ID); err == nil && found {
refusalRecord = current
}
}
return e.recordActionExecutionRefusal(refusalRecord, unifiedresources.ErrActionDryRunOnly, actor, now)
}
var err error
approvalRecord := record
record, err = e.ensureApprovalDecisionBeforeExecution(record, approvalID, actor, now)
@@ -490,7 +499,7 @@ func (e *PulseToolExecutor) ensureApprovalDecisionBeforeExecution(record unified
return current, nil
}
approvalRecord := actionApprovalRecordForExecution(approvalID, actor, now, unifiedresources.OutcomeApproved)
approvalRecord := actionApprovalRecordForExecution(current, approvalID, actor, now, unifiedresources.OutcomeApproved)
updated, event, err := unifiedresources.ApplyActionDecision(current, approvalRecord, approvalRecord.Timestamp)
if err != nil {
log.Warn().Err(err).Str("action_id", record.ID).Msg("failed to normalize approval decision before execution")
@@ -503,11 +512,11 @@ func (e *PulseToolExecutor) ensureApprovalDecisionBeforeExecution(record unified
return updated, nil
}
func actionApprovalRecordForExecution(approvalID, actor string, now time.Time, fallbackOutcome unifiedresources.ApprovalOutcome) unifiedresources.ActionApprovalRecord {
func actionApprovalRecordForExecution(action unifiedresources.ActionAuditRecord, approvalID, actor string, now time.Time, fallbackOutcome unifiedresources.ApprovalOutcome) unifiedresources.ActionApprovalRecord {
if fallbackOutcome == "" {
fallbackOutcome = unifiedresources.OutcomeApproved
}
records := approvalRecordsForID(approvalID)
records := approvalRecordsForID(approvalID, &action.Plan)
if len(records) > 0 {
record := records[len(records)-1]
if record.Outcome == "" {
@@ -521,11 +530,15 @@ func actionApprovalRecordForExecution(approvalID, actor string, now time.Time, f
}
return record
}
actorBinding := legacyApprovalActorBinding(approvalID, actor, action.Request.Actor.OrgID)
evidence := legacyApprovalEvidence(actorBinding, &action.Plan, fallbackOutcome, now)
return unifiedresources.ActionApprovalRecord{
Actor: actor,
Method: unifiedresources.MethodAPI,
Timestamp: now,
Outcome: fallbackOutcome,
Actor: actorBinding.SubjectID,
ActorBinding: actorBinding,
Method: evidence.Method,
Timestamp: now,
Outcome: fallbackOutcome,
Evidence: &evidence,
}
}
@@ -617,7 +630,7 @@ func RecordApprovalDecision(store unifiedresources.ResourceStore, approvalID str
message = string(state)
}
record := actionAuditRecordFromApproval(req, state, actor)
record.Approvals = approvalRecordsForID(req.ID)
record.Approvals = approvalRecordsForID(req.ID, req.Plan)
if recordApprovalDecisionAtomically(store, req.ID, record, actor) {
return
}
@@ -665,7 +678,7 @@ func recordApprovalDecisionAtomically(store unifiedresources.ResourceStore, appr
if record.State == unifiedresources.ActionStateRejected {
outcome = unifiedresources.OutcomeRejected
}
approvalRecord := actionApprovalRecordForExecution(approvalID, actor, time.Now().UTC(), outcome)
approvalRecord := actionApprovalRecordForExecution(current, approvalID, actor, time.Now().UTC(), outcome)
approvalRecord.Outcome = outcome
updated, event, err := unifiedresources.ApplyActionDecision(current, approvalRecord, approvalRecord.Timestamp)
if err != nil {
@@ -803,9 +816,15 @@ func mergeApprovedActionPlan(approved unifiedresources.ActionPlan, fallback unif
return approved
}
func actionAuditRecordFromApproval(req *approval.ApprovalRequest, state unifiedresources.ActionState, actor string) unifiedresources.ActionAuditRecord {
func actionAuditRecordFromApproval(req *approval.ApprovalRequest, state unifiedresources.ActionState, _ string) unifiedresources.ActionAuditRecord {
now := time.Now().UTC()
plan := *req.Plan
if plan.ApprovalPolicy == "" {
plan.ApprovalPolicy = unifiedresources.ApprovalAdmin
}
if plan.ApprovalRequirement.Version == 0 {
plan.ApprovalRequirement = unifiedresources.ApprovalRequirementForFloor(plan.ApprovalPolicy)
}
createdAt := plan.PlannedAt
if createdAt.IsZero() {
createdAt = now
@@ -824,6 +843,13 @@ func actionAuditRecordFromApproval(req *approval.ApprovalRequest, state unifiedr
if orgID := strings.TrimSpace(req.OrgID); orgID != "" {
params["orgId"] = orgID
}
requestedBy := approval.RequesterForRequest(req)
requestActor := unifiedresources.ActionActor{
SubjectID: requestedBy,
Kind: unifiedresources.ActionActorService,
CredentialID: "approval-request:" + strings.TrimSpace(req.ID),
OrgID: approval.NormalizeOrgID(req.OrgID),
}
return unifiedresources.ActionAuditRecord{
ID: plan.ActionID,
CreatedAt: createdAt,
@@ -835,7 +861,8 @@ func actionAuditRecordFromApproval(req *approval.ApprovalRequest, state unifiedr
CapabilityName: approvalCapabilityForTargetType(req.TargetType),
Params: params,
Reason: strings.TrimSpace(req.Context),
RequestedBy: actor,
RequestedBy: requestedBy,
Actor: requestActor,
},
Plan: plan,
}
@@ -989,7 +1016,7 @@ func actionAuditPreflight(resourceID, reason string, generatedAt time.Time) *uni
}, unifiedresources.ActionRequest{ResourceID: resourceID, Reason: reason}, unifiedresources.ActionPlan{PlannedAt: generatedAt, Message: reason})
}
func approvalRecordsForID(approvalID string) []unifiedresources.ActionApprovalRecord {
func approvalRecordsForID(approvalID string, plan *unifiedresources.ActionPlan) []unifiedresources.ActionApprovalRecord {
approvalID = strings.TrimSpace(approvalID)
if approvalID == "" {
return nil
@@ -1003,19 +1030,49 @@ func approvalRecordsForID(approvalID string) []unifiedresources.ActionApprovalRe
return nil
}
record := unifiedresources.ActionApprovalRecord{
Actor: strings.TrimSpace(req.DecidedBy),
Method: unifiedresources.MethodAPI,
Timestamp: approvalTimestamp(req),
Outcome: unifiedresources.OutcomeApproved,
Reason: strings.TrimSpace(req.Context),
}
outcome := unifiedresources.OutcomeApproved
if req.Status == approval.StatusDenied {
record.Outcome = unifiedresources.OutcomeRejected
outcome = unifiedresources.OutcomeRejected
}
actorBinding := legacyApprovalActorBinding(approvalID, req.DecidedBy, req.OrgID)
evidence := legacyApprovalEvidence(actorBinding, plan, outcome, approvalTimestamp(req))
record := unifiedresources.ActionApprovalRecord{
Actor: actorBinding.SubjectID,
ActorBinding: actorBinding,
Method: evidence.Method,
Timestamp: evidence.IssuedAt,
Outcome: outcome,
Reason: strings.TrimSpace(req.Context),
Evidence: &evidence,
}
return []unifiedresources.ActionApprovalRecord{record}
}
func legacyApprovalActorBinding(approvalID, subject, orgID string) unifiedresources.ActionActor {
return unifiedresources.ActionActor{
SubjectID: strings.TrimSpace(subject),
Kind: unifiedresources.ActionActorUser,
CredentialID: "approval-session:" + strings.TrimSpace(approvalID),
OrgID: approval.NormalizeOrgID(orgID),
}
}
func legacyApprovalEvidence(actor unifiedresources.ActionActor, plan *unifiedresources.ActionPlan, outcome unifiedresources.ApprovalOutcome, issuedAt time.Time) unifiedresources.ApprovalEvidence {
evidence := unifiedresources.ApprovalEvidence{
Version: 1,
Method: unifiedresources.MethodSession,
Actor: actor,
OrgID: actor.OrgID,
Outcome: outcome,
IssuedAt: issuedAt.UTC(),
}
if plan != nil {
evidence.ActionID = strings.TrimSpace(plan.ActionID)
evidence.PlanHash = strings.TrimSpace(plan.PlanHash)
}
return evidence
}
func approvalTimestamp(req *approval.ApprovalRequest) time.Time {
if req == nil || req.DecidedAt == nil {
return time.Now().UTC()
@@ -461,7 +461,7 @@ func TestRecordApprovalDecisionDoesNotRegressExecutingAudit(t *testing.T) {
}
executor := NewPulseToolExecutor(ExecutorConfig{ActionAuditStore: actionStore})
record := actionAuditRecordFromApproval(req, unifiedresources.ActionStateExecuting, "pulse_control")
record.Approvals = approvalRecordsForID(req.ID)
record.Approvals = approvalRecordsForID(req.ID, req.Plan)
if err := actionStore.RecordActionAudit(record); err != nil {
t.Fatalf("RecordActionAudit: %v", err)
}
@@ -1104,7 +1104,11 @@ func TestExecuteCommandWithAuditRefusesApprovedDryRunOnlyAndExpiredPlans(t *test
if !strings.HasPrefix(audit.Result.ErrorMessage, tc.wantPrefix) {
t.Fatalf("ErrorMessage = %q, want prefix %q", audit.Result.ErrorMessage, tc.wantPrefix)
}
if len(audit.Approvals) != 1 || audit.Approvals[0].Outcome != unifiedresources.OutcomeApproved {
if errors.Is(tc.wantErr, unifiedresources.ErrActionDryRunOnly) {
if len(audit.Approvals) != 0 {
t.Fatalf("dry-run-only refusal minted executable approval authority: %#v", audit.Approvals)
}
} else if len(audit.Approvals) != 1 || audit.Approvals[0].Outcome != unifiedresources.OutcomeApproved {
t.Fatalf("expected approved audit record to be preserved, got %#v", audit.Approvals)
}
+7 -3
View File
@@ -367,7 +367,7 @@ func TestHandleListPendingActionsReturnsOnlyCanonicalDecisionQueue(t *testing.T)
}
func TestHandleListAndDetailActionsProjectCanonicalResourcePresentation(t *testing.T) {
now := time.Date(2026, 7, 13, 15, 0, 0, 0, time.UTC)
now := time.Now().UTC().Truncate(time.Second)
h := newActionTestResourceHandlers(t, &config.Config{DataPath: t.TempDir()})
h.SetStateProvider(resourceUnifiedSeedProvider{
snapshot: models.StateSnapshot{LastUpdate: now},
@@ -386,11 +386,15 @@ func TestHandleListAndDetailActionsProjectCanonicalResourcePresentation(t *testi
}
record := unified.ActionAuditRecord{
ID: "act-resource-presentation", CreatedAt: now, UpdatedAt: now, State: unified.ActionStatePending,
Request: unified.ActionRequest{RequestID: "req-resource-presentation", ResourceID: "vm:42", CapabilityName: "restart", Reason: "Recover checkout", RequestedBy: "pulse_patrol"},
Request: unified.ActionRequest{
RequestID: "req-resource-presentation", ResourceID: "vm:42", CapabilityName: "restart", Reason: "Recover checkout", RequestedBy: "pulse_patrol",
Actor: unified.ActionActor{SubjectID: "pulse_patrol", Kind: unified.ActionActorService, CredentialID: "service:test-patrol", OrgID: "default"},
},
Plan: unified.ActionPlan{
ActionID: "act-resource-presentation", RequestID: "req-resource-presentation",
Allowed: true, RequiresApproval: true, ApprovalPolicy: unified.ApprovalAdmin,
PlannedAt: now, ExpiresAt: now.Add(4 * time.Hour), PlanHash: "sha256:resource-presentation",
ApprovalRequirement: unified.ApprovalRequirementForFloor(unified.ApprovalAdmin),
PlannedAt: now, ExpiresAt: now.Add(4 * time.Hour), PlanHash: "sha256:resource-presentation",
},
}
if err := store.RecordActionAudit(record); err != nil {
+16
View File
@@ -2132,6 +2132,22 @@ func generateKubernetesNodes(clusterID string, count int) []models.KubernetesNod
nodes[idx].Ready = false
}
// A non-empty demo cluster must retain at least one node that can run
// workloads. Independent readiness and scheduling rolls can otherwise
// produce a fully unavailable estate, making pod placement and the metric
// story nondeterministic even when no outage scenario was requested.
hasSchedulableReadyNode := false
for _, node := range nodes {
if node.Ready && !node.Unschedulable {
hasSchedulableReadyNode = true
break
}
}
if !hasSchedulableReadyNode {
nodes[0].Ready = true
nodes[0].Unschedulable = false
}
return nodes
}
+18
View File
@@ -380,6 +380,24 @@ func TestBuildFixtureStatePopulatesKubernetesUsageMetrics(t *testing.T) {
}
}
func TestGenerateKubernetesNodesAlwaysRetainsSchedulableReadyNode(t *testing.T) {
for count := 1; count <= 8; count++ {
for iteration := 0; iteration < 100; iteration++ {
nodes := generateKubernetesNodes("fixture-cluster", count)
hasSchedulableReadyNode := false
for _, node := range nodes {
if node.Ready && !node.Unschedulable {
hasSchedulableReadyNode = true
break
}
}
if !hasSchedulableReadyNode {
t.Fatalf("node count %d iteration %d produced no schedulable ready node: %+v", count, iteration, nodes)
}
}
}
}
func TestBuildFixtureStateIncludesKubernetesDeploymentAPIMetadata(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
@@ -33,6 +33,7 @@ var infrastructureRouteCatalog = map[string]routeClassification{
"/api/updates/plan": {},
"/api/updates/history": {},
"/api/updates/history/entry": {},
"/api/updates/release-notes": {},
"/api/ai/run-command": {MutationID: "legacy.api.run-command"},
"/api/ai/remediation/plans": {},
"/api/ai/remediation/plan": {},
@@ -493,6 +493,10 @@ func normalizeRestoredStateTruth(truth ActionRestoredStateTruth) (ActionRestored
}
func NormalizeActionResultV2(result ActionResultV2) (ActionResultV2, error) {
// Normalization is routinely applied to shared store and mock-fixture
// snapshots. Clone every nested truth surface before canonicalizing it so
// digest calculation and UTC normalization never mutate the caller's copy.
result = cloneActionResultV2(result)
if result.Version == 0 {
result.Version = ActionResultV2Version
}
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
)
@@ -26,6 +27,40 @@ func actionResultTestEvidence(class ActionEvidenceClass) []ActionEvidence {
}}
}
func TestNormalizeActionResultV2DoesNotMutateSharedInput(t *testing.T) {
input := ActionResultV2{
Version: ActionResultV2Version,
Execution: ActionExecutionTruth{Status: ActionExecutionSucceeded},
Verification: ActionVerificationTruth{Status: ActionVerificationConfirmed, EvidenceClass: ActionEvidenceIndependent, Evidence: actionResultTestEvidence(ActionEvidenceIndependent)},
Compensation: actionResultTestCompensation(),
}
const callers = 16
errs := make(chan error, callers)
var wg sync.WaitGroup
for range callers {
wg.Add(1)
go func() {
defer wg.Done()
normalized, err := NormalizeActionResultV2(input)
if err == nil && normalized.Verification.Evidence[0].Digest == "" {
err = errors.New("normalized evidence is missing its digest")
}
errs <- err
}()
}
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatal(err)
}
}
if input.Verification.Evidence[0].Digest != "" {
t.Fatalf("normalization mutated shared input digest: %q", input.Verification.Evidence[0].Digest)
}
}
func actionResultTestCompensation() ActionCompensationTruth {
return ActionCompensationTruth{Support: ActionCompensationUnavailable, Status: ActionCompensationNotAvailable}
}
@@ -245,6 +245,9 @@ func TestCanonicalActionPlanConstructionCannotBypassPolicyProvenancePlanner(t *t
// audit history remain readable but are not canonical action producers.
"../api/router_routes_ai_relay.go": true,
"../ai/tools/action_audit.go": true,
// Graph-owned mock records are immutable presentation fixtures. They do
// not admit, approve, or dispatch executable actions.
"../mock/action_fixtures.go": true,
}
pattern := regexp.MustCompile(`(?s)\bActionPlan\s*\{\s*[A-Za-z_][A-Za-z0-9_]*\s*:`)
err := filepath.Walk("..", func(path string, info os.FileInfo, err error) error {