Testing Your Implementation
Test an AGH Network participant with fixtures, a local echo peer, NATS integration checks, and conformance evidence.
- Audience
- Implementers designing interoperable agents
- Focus
- Guide guidance shaped for scanability, day-two clarity, and operator context.
This tutorial turns the sender, transport, and trust code into repeatable tests. You will start with fixture tests, add a tiny local echo peer, and collect enough evidence to make a clear conformance claim.
Normative requirements live in conformance levels. Current AGH source has package tests for its v0 runtime, but it does not yet ship a standalone third-party conformance runner or built-in echo peer.
What you'll build
By the end, you will have a test harness that:
- validates envelope fixtures before transport is involved
- checks v0 NATS route-token and subject mapping
- exercises a local echo peer over JSON envelopes
- verifies a baseline Ed25519 proof fixture
- records what is covered and what is still out of scope
Start with fixture tests
Keep core protocol tests transport-agnostic. A receiver should reject malformed JSON before it opens subscriptions, trust stores, or work queues.
package aghnetwork_test
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"testing"
)
type Envelope struct {
Protocol string `json:"protocol"`
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Kind string `json:"kind"`
Channel string `json:"channel"`
Surface *string `json:"surface,omitempty"`
ThreadID *string `json:"thread_id,omitempty"`
DirectID *string `json:"direct_id,omitempty"`
From string `json:"from"`
To *string `json:"to"`
WorkID *string `json:"work_id,omitempty"`
ReplyTo *string `json:"reply_to,omitempty"`
TS int64 `json:"ts"`
Body json.RawMessage `json:"body"`
Proof map[string]any `json:"proof"`
}
func TestCoreSenderFixture(t *testing.T) {
raw := []byte(`{
"protocol": "agh-network/v0",
"id": "msg_fixture_01",
"workspace_id": "ws_alpha",
"kind": "say",
"channel": "builders",
"surface": "thread",
"thread_id": "thread_fixture_01",
"from": "sender.demo",
"to": null,
"ts": 1775606300,
"body": {"text": "fixture hello"},
"proof": null
}`)
var env Envelope
if err := json.Unmarshal(raw, &env); err != nil {
t.Fatalf("decode envelope: %v", err)
}
if env.Protocol != "agh-network/v0" {
t.Fatalf("protocol = %q", env.Protocol)
}
if env.Kind != "say" {
t.Fatalf("kind = %q", env.Kind)
}
if env.Surface == nil || *env.Surface != "thread" {
t.Fatalf("conversation say must carry surface=thread")
}
if env.ThreadID == nil || *env.ThreadID == "" {
t.Fatal("thread surface must carry a non-empty thread_id")
}
var body struct {
Text string `json:"text"`
}
if err := json.Unmarshal(env.Body, &body); err != nil {
t.Fatalf("decode body: %v", err)
}
if body.Text == "" {
t.Fatal("body.text is required")
}
}
func TestNATSSubjectMapping(t *testing.T) {
if got, want := broadcastSubject("ws_alpha", "builders"), "agh.network.v0.ws_alpha.builders.broadcast"; got != want {
t.Fatalf("broadcast subject = %q, want %q", got, want)
}
if got, want := directSubject("ws_alpha", "builders", "echo.demo"), "agh.network.v0.ws_alpha.builders.peer.96b61db360703e491489caf5fbbd45bc"; got != want {
t.Fatalf("direct subject = %q, want %q", got, want)
}
}
func TestLocalEchoPeer(t *testing.T) {
to := "echo.demo"
surface := "thread"
threadID := "thread_echo_demo"
request := Envelope{
Protocol: "agh-network/v0",
ID: "msg_say_work_01",
WorkspaceID: "ws_alpha",
Kind: "say",
Channel: "builders",
Surface: &surface,
ThreadID: &threadID,
From: "sender.demo",
To: &to,
WorkID: ptr("work_echo_01"),
TS: 1775606300,
Body: json.RawMessage(`{"text":"ping","intent":"request"}`),
Proof: nil,
}
response, err := echoPeer(request)
if err != nil {
t.Fatalf("echo peer: %v", err)
}
if response.Kind != "receipt" {
t.Fatalf("response kind = %q", response.Kind)
}
if response.ReplyTo == nil || *response.ReplyTo != request.ID {
t.Fatalf("response reply_to = %v", response.ReplyTo)
}
if response.WorkID == nil || *response.WorkID != "work_echo_01" {
t.Fatalf("response work_id = %v", response.WorkID)
}
if response.Surface == nil || *response.Surface != "thread" {
t.Fatalf("response surface = %v", response.Surface)
}
if response.ThreadID == nil || *response.ThreadID != threadID {
t.Fatalf("response thread_id = %v", response.ThreadID)
}
}
func echoPeer(request Envelope) (Envelope, error) {
body := map[string]any{
"for_id": request.ID,
"status": "accepted",
}
encoded, err := json.Marshal(body)
if err != nil {
return Envelope{}, err
}
return Envelope{
Protocol: "agh-network/v0",
ID: "msg_receipt_01",
WorkspaceID: request.WorkspaceID,
Kind: "receipt",
Channel: request.Channel,
Surface: request.Surface,
ThreadID: request.ThreadID,
DirectID: request.DirectID,
From: "echo.demo",
To: &request.From,
WorkID: request.WorkID,
ReplyTo: &request.ID,
TS: request.TS + 1,
Body: encoded,
Proof: nil,
}, nil
}
func broadcastSubject(workspaceID string, channel string) string {
return "agh.network.v0." + workspaceID + "." + channel + ".broadcast"
}
func directSubject(workspaceID string, channel string, peerID string) string {
return "agh.network.v0." + workspaceID + "." + channel + ".peer." + routeToken(peerID)
}
func routeToken(peerID string) string {
sum := sha256.Sum256([]byte(peerID))
return hex.EncodeToString(sum[:16])
}
func ptr(value string) *string {
return &value
}Language-agnostic pseudocode:
test core sender fixture:
decode envelope JSON
require protocol == "agh-network/v0"
require kind == "say"
require surface == "thread"
require thread_id is set
require body.text is not blank
test nats subject mapping:
broadcast_subject("ws_alpha", "builders") must equal "agh.network.v0.ws_alpha.builders.broadcast"
direct_subject("ws_alpha", "builders", "echo.demo") must use sha256(peer_id)[0:32]
test local echo peer:
build say request with surface=thread, thread_id, work_id
pass request to echo_peer
require response.kind == "receipt"
require response.surface == request.surface
require response.thread_id == request.thread_id
require response.work_id == request.work_id
require response.reply_to == request.idAdd integration tests after fixtures pass
Once fixture tests are stable, add integration checks in this order:
| Layer | What to test | Failure to catch |
|---|---|---|
| JSON fixtures | Required fields, kind-specific bodies, malformed body shapes, conversation surface pairing, rejection of kind:"direct" and interaction_id. | Accepting invalid envelopes or legacy fields. |
| Subject mapping | Broadcast and direct subject derivation. | Routing to an alias or display name instead of canonical peer ID. |
| NATS loopback | Publish, subscribe, request-reply, disconnect/reconnect. | Confusing transport acknowledgement with protocol acceptance. |
| Trust fixture | Known public key, fingerprint, signature, and tamper case. | Verifying non-canonical bytes or skipping from fingerprint checks. |
| Work lifecycle | say, capability, receipt, and trace transitions sharing one work_id inside one conversation container. | Closing work too early, accepting cross-container continuations, or accepting late terminal updates. |
Use the worked Ed25519 example as a golden trust fixture. It is deterministic and should fail if your verifier signs or verifies the wrong bytes.
Compare with AGH's current runtime tests
When you are working inside the AGH repository, these commands exercise the current v0 implementation:
go test ./internal/networkgo test ./internal/cli -run 'TestCLINetwork'Those tests are useful implementation evidence. They do not replace a public third-party conformance runner, because they import AGH internals and assume the daemon-owned runtime.
Prepare conformance evidence
Before you claim support, write down the level you are testing against:
| Claim | Evidence to collect |
|---|---|
| Core Sender | Valid envelope fixtures for every kind you emit. |
| Core Receiver | Rejection fixtures for malformed, expired, duplicate, and unsupported messages. |
| NATS Peer | Subject mapping tests plus live NATS loopback tests. |
| Trust | Golden signed envelope, tamper rejection, proof-stripping rejection. |
| Full | Core, NATS, and Trust evidence together. |
Publish the fixtures, command output, implementation version, and known gaps with the claim. If a feature is intentionally missing, say so explicitly instead of widening the claim.
Verify it works
Run the fixture tests:
go test ./...Expected result:
ok example.com/agh-network-guide ...Then run your NATS loopback program from the previous tutorial and your trust verifier:
go run ./nats-transport.go
go run ./trust-verification.goAt this point you have a complete tutorial implementation path:
- Minimal sender proves the envelope shape.
- NATS transport proves message exchange.
- Trust verification proves cryptographic identity.
- This page turns the behavior into repeatable evidence.