Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/15-milestones.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ signed AEP evidence instead.
- [x] `wasmagent/edge/`: Low-latency WasmAgent edge runtime supporting offline evidence buffering and eventual ledger synchronization
- Owner: `WasmAgent/wasmagent` (runtime tier; owns the edge runtime). Reference surface landed in this hub (`wasmagent/edge/edge.ts`) and is tracked by `tests/e2e/wasmagent_edge_runtime_test.go`; the production edge binary and ledger sync service live in the sibling repo.
- [ ] `agent-golden-path/multi-agent/`: Multi-agent procurement federation workload (buyer copilot ↔ supplier copilot) under signed AEP contracts
- [ ] `wasmagent-ops/resilience/`: Automated circuit breaker and transactional rollback mechanism triggered on policy violation events
- [x] `wasmagent-ops/resilience/`: Automated circuit breaker and transactional rollback mechanism triggered on policy violation events
- Owner: `WasmAgent/wasmagent-ops` (internal-tool tier; owns the ops tooling). Reference surface landed in this hub (`wasmagent-ops/resilience/resilience.ts`) and is tracked by `tests/e2e/wasmagent_ops_resilience_test.go`; the production resilience daemon lives in the sibling repo.
- [ ] `docs/federation-spec.md`: Complete cross-domain agent federation protocol and ZK attestation architecture specification
- [ ] `tests/mesh/`: End-to-end integration test suite validating multi-agent attestation, ZK evidence verification, and real-time revocation (`npm run test:mesh`)

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions tests/e2e/wasmagent_ops_resilience_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package e2e

import (
"os"
"path/filepath"
"strings"
"testing"
)

// TestWasmagentOpsResilience validates the wasmagent-ops/resilience reference
// surface for the Milestone 6 bullet:
//
// wasmagent-ops/resilience/: Automated circuit breaker and transactional
// rollback mechanism triggered on policy violation events
//
// It checks that:
// - wasmagent-ops/resilience/resilience.ts ships a PolicyCircuitBreaker with
// fail-fast operation gating (recordViolation / allowOperation /
// recordSuccess with closed → open → half_open recovery), a
// TransactionalRollbackManager with begin/addStep/commit/rollback and
// RollbackRecord production, and a ResilienceCoordinator wiring both
// behind a single onPolicyViolation entry point.
// - wasmagent-ops/resilience/resilience.test.ts exercises circuit tripping,
// fail-fast rejection, half-open recovery, transactional rollback, and
// coordinator integration.
// - The Milestone 6 bullet in docs/15-milestones.md is marked complete so the
// hub roadmap tracks the shipped surface.
func TestWasmagentOpsResilience(t *testing.T) {
resilienceDir := filepath.Join("..", "..", "wasmagent-ops", "resilience")

// 1. Reference implementation must ship the resilience surface.
driverPath := filepath.Join(resilienceDir, "resilience.ts")
driverSource, err := os.ReadFile(driverPath)
if err != nil {
t.Fatalf("wasmagent-ops/resilience/resilience.ts is missing: %v", err)
}
for _, fragment := range []string{
"export class PolicyCircuitBreaker",
"export class TransactionalRollbackManager",
"export class ResilienceCoordinator",
"export class CircuitBreakerOpenError",
"export class TransactionNotFoundError",
"export interface PolicyViolationEvent",
"export interface CircuitStateSnapshot",
"export interface Transaction",
"export interface RollbackRecord",
"export type CircuitState",
"recordViolation(",
"allowOperation(",
"recordSuccess(",
"begin(",
"commit(",
"rollback(",
"onPolicyViolation(",
} {
if !strings.Contains(string(driverSource), fragment) {
t.Errorf("wasmagent-ops/resilience resilience.ts is missing required capability %q", fragment)
}
}

// 2. Reference tests must cover circuit tripping, fail-fast rejection,
// half-open recovery, transactional rollback, and coordinator integration.
testPath := filepath.Join(resilienceDir, "resilience.test.ts")
testSource, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("wasmagent-ops/resilience coverage is missing: %v", err)
}
for _, scenario := range []string{
"trips the circuit breaker after repeated policy violations",
"fails fast while the circuit is open",
"recovers through the half-open trial window after the cooldown elapses",
"rolls back a transaction triggered by a policy violation",
"coordinates circuit tripping and transactional rollback from a single violation",
} {
if !strings.Contains(string(testSource), scenario) {
t.Errorf("wasmagent-ops/resilience test is missing scenario %q", scenario)
}
}

// 3. The milestone bullet must be marked complete.
milestones, err := os.ReadFile("../../docs/15-milestones.md")
if err != nil {
t.Fatalf("Failed to read docs/15-milestones.md: %v", err)
}
bulletFound := false
for _, line := range strings.Split(string(milestones), "\n") {
if strings.Contains(line, "`wasmagent-ops/resilience/`") {
bulletFound = true
if !strings.HasPrefix(strings.TrimSpace(line), "- [x]") {
t.Errorf("wasmagent-ops/resilience milestone bullet is not checked: %s", line)
}
}
}
if !bulletFound {
t.Error("wasmagent-ops/resilience milestone bullet not found in docs/15-milestones.md")
}

t.Log("Automated circuit breaker and transactional rollback mechanism validated for wasmagent-ops")
}
192 changes: 192 additions & 0 deletions wasmagent-ops/resilience/resilience.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Automated circuit breaker and transactional rollback mechanism tests.
//
// Exercises fail-fast circuit breaking, half-open recovery, transactional
// rollback, and the combined coordinator for the Milestone 6 reference
// surface:
//
// > `wasmagent-ops/resilience/`: Automated circuit breaker and transactional
// > rollback mechanism triggered on policy violation events

import { describe, expect, it } from "bun:test";

import type { PolicyViolationEvent, TransactionStep } from "./resilience";
import {
CircuitBreakerOpenError,
InvalidViolationEventError,
PolicyCircuitBreaker,
ResilienceCoordinator,
TransactionAlreadyEndedError,
TransactionNotFoundError,
TransactionalRollbackManager,
} from "./resilience";

function violation(overrides: Partial<PolicyViolationEvent> = {}): PolicyViolationEvent {
return {
violationId: "v-1",
agentId: "agent-1",
timestamp: "2026-08-03T00:00:00.000Z",
policyRef: "agentbom.policy.tool-admission",
kind: "policy.denied",
severity: 3,
detail: "tool call not admitted by AgentBOM",
...overrides,
};
}

const step = (operationId: string, name: string): TransactionStep => ({
operationId,
name,
recordedAt: "2026-08-03T00:00:00.000Z",
});

describe("PolicyCircuitBreaker", () => {
it("trips the circuit breaker after repeated policy violations", () => {
const breaker = new PolicyCircuitBreaker("agent-1", { failureThreshold: 3 });
const tripped: string[] = [];
breaker.onTrip((snapshot) => tripped.push(snapshot.state));

breaker.recordViolation(violation({ violationId: "v-1" }));
breaker.recordViolation(violation({ violationId: "v-2" }));
expect(breaker.getState().state).toBe("closed");

breaker.recordViolation(violation({ violationId: "v-3" }));
expect(breaker.getState().state).toBe("open");
expect(tripped).toEqual(["open"]);
expect(breaker.getState().consecutiveViolations).toBe(3);
});

it("fails fast while the circuit is open", () => {
const breaker = new PolicyCircuitBreaker("agent-1", { failureThreshold: 2 });
breaker.recordViolation(violation({ violationId: "v-1" }));
breaker.recordViolation(violation({ violationId: "v-2" }));
expect(breaker.getState().state).toBe("open");

expect(() =>
breaker.allowOperation({
operationId: "op-1",
agentId: "agent-1",
name: "tool.call.write-file",
}),
).toThrow(CircuitBreakerOpenError);
});

it("recovers through the half-open trial window after the cooldown elapses", () => {
const breaker = new PolicyCircuitBreaker("agent-1", {
failureThreshold: 2,
cooldownMs: 5_000,
maxTrials: 1,
});
breaker.recordViolation(
violation({ violationId: "v-1", timestamp: "2026-08-03T00:00:00.000Z" }),
);
breaker.recordViolation(
violation({ violationId: "v-2", timestamp: "2026-08-03T00:00:01.000Z" }),
);
expect(breaker.getState().state).toBe("open");

const later = "2026-08-03T00:01:00.000Z";
expect(breaker.getState(later).state).toBe("half_open");

breaker.allowOperation({
operationId: "op-1",
agentId: "agent-1",
name: "tool.call.read",
});
breaker.recordSuccess();
const snapshot = breaker.getState(later);
expect(snapshot.state).toBe("closed");
expect(snapshot.consecutiveViolations).toBe(0);
});

it("trips immediately on a critical-severity violation", () => {
const breaker = new PolicyCircuitBreaker("agent-1", {
failureThreshold: 5,
maxSeverity: 10,
});
breaker.recordViolation(violation({ violationId: "v-1", severity: 10 }));
expect(breaker.getState().state).toBe("open");
});

it("rejects malformed violation events and mismatched agents", () => {
const breaker = new PolicyCircuitBreaker("agent-1");
expect(() => breaker.recordViolation(violation({ violationId: "" }))).toThrow(
InvalidViolationEventError,
);
expect(() =>
breaker.recordViolation(violation({ agentId: "other-agent" })),
).toThrow(InvalidViolationEventError);
});
});

describe("TransactionalRollbackManager", () => {
it("rolls back a transaction triggered by a policy violation", () => {
const manager = new TransactionalRollbackManager();
const txn = manager.begin("agent-1");
manager.addStep(txn.transactionId, step("op-1", "data.write.ledger"));
manager.addStep(txn.transactionId, step("op-2", "data.write.budget"));

const record = manager.rollback(
txn.transactionId,
"policy violation v-1",
"agentbom.policy.tool-admission",
);
expect(record.revertedSteps.map((s) => s.name)).toEqual([
"data.write.ledger",
"data.write.budget",
]);
expect(record.policyRef).toBe("agentbom.policy.tool-admission");
expect(manager.activeTransactions()).toHaveLength(0);
expect(manager.getRollbackLog()).toHaveLength(1);
});

it("commits a transaction and refuses further steps or rollback", () => {
const manager = new TransactionalRollbackManager();
const txn = manager.begin("agent-1");
manager.addStep(txn.transactionId, step("op-1", "network.read"));
const committed = manager.commit(txn.transactionId);
expect(committed.committed).toBe(true);

expect(() => manager.addStep(txn.transactionId, step("op-2", "network.read"))).toThrow(
TransactionAlreadyEndedError,
);
expect(() => manager.rollback(txn.transactionId, "late")).toThrow(
TransactionAlreadyEndedError,
);
});

it("rejects operations on unknown transactions", () => {
const manager = new TransactionalRollbackManager();
expect(() => manager.commit("txn:nope")).toThrow(TransactionNotFoundError);
expect(() => manager.rollback("txn:nope", "unknown")).toThrow(
TransactionNotFoundError,
);
});
});

describe("ResilienceCoordinator", () => {
it("coordinates circuit tripping and transactional rollback from a single violation", () => {
const coordinator = new ResilienceCoordinator({ failureThreshold: 2 });
const txn = coordinator.beginTransaction("agent-1");
coordinator.addStep(txn.transactionId, step("op-1", "data.write"));

coordinator.onPolicyViolation(
violation({ violationId: "v-1", transactionId: txn.transactionId }),
);
expect(coordinator.getCircuitState("agent-1").state).toBe("closed");

coordinator.onPolicyViolation(
violation({ violationId: "v-2", transactionId: txn.transactionId }),
);
expect(coordinator.getCircuitState("agent-1").state).toBe("open");
expect(coordinator.getRollbackLog()).toHaveLength(2);
expect(coordinator.activeTransactions()).toHaveLength(0);

expect(() =>
coordinator.allowOperation({
operationId: "op-2",
agentId: "agent-1",
name: "tool.call.read",
}),
).toThrow(CircuitBreakerOpenError);
});
});
Loading
Loading