Skip to content
Merged
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 @@ -69,7 +69,8 @@ signed AEP evidence instead.
- [x] `wasmagent-js/spiffe/`: SPIFFE/SPIRE cryptographic identity driver binding Wasm sandbox workloads to enterprise mTLS credentials
- [x] `trace-pipeline/stream/`: Real-time gRPC telemetry and event ingestion pipeline for instant posture drift detection and passport revocation
- Owner: `WasmAgent/trace-pipeline` (evidence-pipeline tier; owns evidence admission / training-data pipeline). Reference surface landed in this hub (`trace-pipeline/stream/stream.ts`) and is tracked by `tests/e2e/trace_pipeline_stream_test.go`; the production gRPC service port lives in the sibling repo.
- [ ] `wasmagent/edge/`: Low-latency WasmAgent edge runtime supporting offline evidence buffering and eventual ledger synchronization
- [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
- [ ] `docs/federation-spec.md`: Complete cross-domain agent federation protocol and ZK attestation architecture specification
Expand Down
89 changes: 89 additions & 0 deletions tests/e2e/wasmagent_edge_runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package e2e

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

// TestWasmagentEdgeRuntime validates the wasmagent/edge reference surface for
// the Milestone 6 bullet:
//
// wasmagent/edge/: Low-latency WasmAgent edge runtime supporting offline
// evidence buffering and eventual ledger synchronization
//
// It checks that:
// - wasmagent/edge/edge.ts ships an EdgeEvidenceRuntime exposing low-latency
// agent step execution (executeStep), offline evidence buffering
// (recordEvidence / getBufferStats), and eventual ledger synchronization
// (flushBuffered / LedgerTransport.isOnline / LedgerSyncResult).
// - wasmagent/edge/edge.test.ts exercises low-latency execution, offline
// evidence buffering, offline flush rejection, and eventual ledger
// synchronization once connectivity returns.
// - The Milestone 6 bullet in docs/15-milestones.md is marked complete so the
// hub roadmap tracks the shipped surface.
func TestWasmagentEdgeRuntime(t *testing.T) {
edgeDir := filepath.Join("..", "..", "wasmagent", "edge")

// 1. Reference implementation must ship the edge runtime surface.
driverPath := filepath.Join(edgeDir, "edge.ts")
driverSource, err := os.ReadFile(driverPath)
if err != nil {
t.Fatalf("wasmagent/edge/edge.ts is missing: %v", err)
}
for _, fragment := range []string{
"export class EdgeEvidenceRuntime",
"export interface EvidenceEvent",
"export interface OfflineBufferStats",
"export interface LedgerSyncResult",
"export interface LedgerTransport",
"recordEvidence(",
"flushBuffered(",
"getBufferStats(",
"executeStep(",
"isOnline()",
} {
if !strings.Contains(string(driverSource), fragment) {
t.Errorf("wasmagent/edge edge.ts is missing required capability %q", fragment)
}
}

// 2. Reference tests must cover low-latency execution, offline evidence
// buffering, and eventual ledger synchronization.
testPath := filepath.Join(edgeDir, "edge.test.ts")
testSource, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("wasmagent/edge coverage is missing: %v", err)
}
for _, scenario := range []string{
"executes agent steps with low latency while recording evidence",
"buffers evidence while the ledger is offline",
"rejects a flush attempt while the ledger is offline",
"synchronizes buffered evidence to the ledger once connectivity returns",
} {
if !strings.Contains(string(testSource), scenario) {
t.Errorf("wasmagent/edge 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/edge/`") {
bulletFound = true
if !strings.HasPrefix(strings.TrimSpace(line), "- [x]") {
t.Errorf("wasmagent/edge milestone bullet is not checked: %s", line)
}
}
}
if !bulletFound {
t.Error("wasmagent/edge milestone bullet not found in docs/15-milestones.md")
}

t.Log("Low-latency edge runtime with offline evidence buffering validated for wasmagent")
}
138 changes: 138 additions & 0 deletions wasmagent/edge/edge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Low-latency WasmAgent edge runtime tests.
//
// Exercises low-latency agent step execution, offline evidence buffering, and
// eventual ledger synchronization for the Milestone 6 reference surface:
//
// > `wasmagent/edge/`: Low-latency WasmAgent edge runtime supporting offline
// > evidence buffering and eventual ledger synchronization

import { describe, expect, it } from "bun:test";
import {
EdgeEvidenceRuntime,
EvidenceBufferFullError,
InvalidEvidenceEventError,
LedgerSyncError,
type EvidenceEvent,
type LedgerSyncResult,
type LedgerTransport,
} from "./edge";

class FakeLedgerTransport implements LedgerTransport {
readonly ledgerId = "ledger:mesh-1";
private online = false;
readonly appended: EvidenceEvent[][] = [];

isOnline(): boolean {
return this.online;
}

setOnline(online: boolean): void {
this.online = online;
}

append(events: readonly EvidenceEvent[]): Promise<LedgerSyncResult> {
if (!this.online) {
return Promise.reject(new Error("transport offline"));
}
this.appended.push([...events]);
return Promise.resolve({
ledgerId: this.ledgerId,
syncedCount: events.length,
syncedAt: new Date().toISOString(),
});
}
}

function evidence(overrides: Partial<EvidenceEvent> = {}): EvidenceEvent {
return {
evidenceId: "evt-1",
agentId: "edge-agent-1",
timestamp: "2026-08-03T00:00:00.000Z",
kind: "aep.step.executed",
payload: {},
...overrides,
};
}

describe("EdgeEvidenceRuntime", () => {
it("executes agent steps with low latency while recording evidence", () => {
const transport = new FakeLedgerTransport();
const runtime = new EdgeEvidenceRuntime({ agentId: "edge-agent-1", transport });
const startedAt = Date.now();
const result = runtime.executeStep({
stepId: "step-1",
operation: "tool.call.read-file",
input: { path: "/tmp/evidence.jsonl" },
});
expect(result.accepted).toBe(true);
expect(result.evidenceId).toContain("step-1");
expect(Date.now() - startedAt).toBeLessThan(1000);
expect(runtime.getBufferStats().bufferedCount).toBe(1);
});

it("buffers evidence while the ledger is offline", () => {
const transport = new FakeLedgerTransport(); // offline by default
const runtime = new EdgeEvidenceRuntime({ agentId: "edge-agent-1", transport });
runtime.recordEvidence(evidence({ evidenceId: "evt-1" }));
runtime.recordEvidence(evidence({ evidenceId: "evt-2" }));
const stats = runtime.getBufferStats();
expect(stats.bufferedCount).toBe(2);
expect(stats.oldestBufferedAt).toBe("2026-08-03T00:00:00.000Z");
expect(stats.totalBytes).toBeGreaterThan(0);
});

it("rejects a flush attempt while the ledger is offline", async () => {
const transport = new FakeLedgerTransport();
const runtime = new EdgeEvidenceRuntime({ agentId: "edge-agent-1", transport });
runtime.recordEvidence(evidence());
await expect(runtime.flushBuffered()).rejects.toThrow(LedgerSyncError);
expect(runtime.getBufferStats().bufferedCount).toBe(1);
});

it("synchronizes buffered evidence to the ledger once connectivity returns", async () => {
const transport = new FakeLedgerTransport();
const runtime = new EdgeEvidenceRuntime({ agentId: "edge-agent-1", transport });
runtime.recordEvidence(evidence({ evidenceId: "evt-1" }));
runtime.recordEvidence(evidence({ evidenceId: "evt-2" }));
expect(runtime.getBufferStats().bufferedCount).toBe(2);

transport.setOnline(true);
const result = await runtime.flushBuffered();
expect(result.syncedCount).toBe(2);
expect(result.ledgerId).toBe("ledger:mesh-1");
expect(runtime.getBufferStats().bufferedCount).toBe(0);
expect(transport.appended).toHaveLength(1);
});

it("rejects malformed evidence events and a full offline buffer", () => {
const transport = new FakeLedgerTransport();
const runtime = new EdgeEvidenceRuntime({
agentId: "edge-agent-1",
transport,
maxBufferSize: 2,
});
expect(() => runtime.recordEvidence(evidence({ evidenceId: "" }))).toThrow(
InvalidEvidenceEventError,
);
expect(() => runtime.recordEvidence(evidence({ agentId: "other-agent" }))).toThrow(
InvalidEvidenceEventError,
);
runtime.recordEvidence(evidence({ evidenceId: "evt-1" }));
runtime.recordEvidence(evidence({ evidenceId: "evt-2" }));
expect(() => runtime.recordEvidence(evidence({ evidenceId: "evt-3" }))).toThrow(
EvidenceBufferFullError,
);
});

it("notifies drain subscribers when buffered evidence is eventually synced", async () => {
const transport = new FakeLedgerTransport();
const runtime = new EdgeEvidenceRuntime({ agentId: "edge-agent-1", transport });
const drained: number[] = [];
runtime.onBufferDrain((result) => drained.push(result.syncedCount));
runtime.recordEvidence(evidence({ evidenceId: "evt-1" }));
runtime.recordEvidence(evidence({ evidenceId: "evt-2" }));
transport.setOnline(true);
await runtime.flushBuffered();
expect(drained).toEqual([2]);
});
});
Loading
Loading