Files
mcp/governance/pending_observation_test.go
zeekayandHanzo Dev 6b380d62a4 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers: any, max and range over int. omitzero is not applied, since it
changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:15:04 -07:00

144 lines
5.2 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package governance
import (
"context"
"math/big"
"testing"
"github.com/luxfi/mcp"
)
// TestPendingOperationsAnnotatesDeadlineAndTruncation is the MEDIUM-5 test. It proves:
// - a still-Open task whose voting window has CLOSED is returned with deadlinePassed=true
// (so the LLM knows the window is over even though settle was never called), while a
// fresh Open task reads deadlinePassed=false;
// - when more tasks exist than the scan window inspects, truncated=true and scannedFrom
// marks the lowest id seen (the list is honestly flagged as incomplete).
func TestPendingOperationsAnnotatesDeadlineAndTruncation(t *testing.T) {
ops := genKeys(t, 1)
_, env := newEVMChain(t, ops)
srv := env.mcpServer()
for _, k := range ops {
env.registerOperator(k)
}
spec := specHash("pending-ops-spec")
// Task 0: open it, then advance PAST its 1h window without settling -> still Open,
// but deadlinePassed must be true.
t0 := env.openThought(spec, "knob-0", 1)
env.c.advanceSeconds(2 * 3600)
// Task 1: open it fresh (deadline in the future) -> Open, deadlinePassed false.
t1 := env.openThought(spec, "knob-1", 1)
res := callTool(t, srv, toolPendingOperations, map[string]any{})
pending := res["pending"].([]any)
// Both tasks are Open, so both appear. Index them by taskId.
byID := map[string]map[string]any{}
for _, p := range pending {
m := p.(map[string]any)
byID[m["taskId"].(string)] = m
}
p0, ok := byID[t0.String()]
if !ok {
t.Fatalf("task 0 missing from pending: %v", pending)
}
p1, ok := byID[t1.String()]
if !ok {
t.Fatalf("task 1 missing from pending: %v", pending)
}
if p0["deadlinePassed"] != true {
t.Fatalf("task 0 deadlinePassed=%v, want true (window closed, unsettled)", p0["deadlinePassed"])
}
if p1["deadlinePassed"] != false {
t.Fatalf("task 1 deadlinePassed=%v, want false (fresh window)", p1["deadlinePassed"])
}
// With a tiny limit and only the tail scanned, truncation must be reported honestly.
// Open several more tasks so taskCount exceeds a limit=1 window.
for range 3 {
env.openThought(spec, "knob-extra", 1)
}
// Pass limit as float64 to mirror JSON-decoded args (the real transport path).
small := callTool(t, srv, toolPendingOperations, map[string]any{"limit": float64(1)})
// We asked for at most 1 result; with many open tasks at the tail it returns 1.
if got := len(small["pending"].([]any)); got != 1 {
t.Fatalf("limit=1 returned %d pending, want 1", got)
}
if small["truncated"] != true {
t.Fatalf("truncated=%v, want true (older tasks below the window were not inspected)", small["truncated"])
}
// scannedFrom must be > 0 (we did not reach task 0).
sf, ok := new(big.Int).SetString(small["scannedFrom"].(string), 10)
if !ok || sf.Sign() <= 0 {
t.Fatalf("scannedFrom=%v, want a positive lowest-scanned id", small["scannedFrom"])
}
}
// TestPendingOperationsNotTruncatedWhenAllScanned proves truncated=false when the scan
// reaches task 0 (the window covers everything), so the flag is meaningful (not always on).
func TestPendingOperationsNotTruncatedWhenAllScanned(t *testing.T) {
ops := genKeys(t, 1)
_, env := newEVMChain(t, ops)
srv := env.mcpServer()
for _, k := range ops {
env.registerOperator(k)
}
spec := specHash("pending-small-spec")
// Just two tasks; the default scan floor (64) covers them to task 0.
env.openThought(spec, "a", 1)
env.openThought(spec, "b", 1)
res := callTool(t, srv, toolPendingOperations, map[string]any{})
if res["truncated"] != false {
t.Fatalf("truncated=%v, want false (scan reached task 0)", res["truncated"])
}
if res["scannedFrom"] != "0" {
t.Fatalf("scannedFrom=%v, want 0 (inspected down to genesis task)", res["scannedFrom"])
}
}
// TestObservationVerifyRejectsChainMismatch is the LOW test: Verify must reject an
// observation taken on a DIFFERENT chain id, even when block number/hash would match.
func TestObservationVerifyRejectsChainMismatch(t *testing.T) {
ops := genKeys(t, 1)
_, env := newEVMChain(t, ops)
obs, err := mcp.NewObservation(context.Background(), env.c, toolChainState, nil)
if err != nil {
t.Fatalf("NewObservation: %v", err)
}
// Sanity: it verifies on its own chain.
if fresh, err := obs.Verify(context.Background(), env.c, 0); err != nil || !fresh {
t.Fatalf("fresh observation rejected on its own chain: fresh=%v err=%v", fresh, err)
}
// Forge a wrong chain id on the observation; Verify must reject as a chain mismatch.
wrong := &mcp.ChainObservation{
ChainID: new(big.Int).Add(obs.ChainID, big.NewInt(1)),
BlockNumber: obs.BlockNumber,
BlockHash: obs.BlockHash,
Timestamp: obs.Timestamp,
Tool: toolChainState,
}
fresh, err := wrong.Verify(context.Background(), env.c, 1000)
if fresh {
t.Fatal("observation with mismatched chainId passed Verify")
}
if err == nil {
t.Fatal("expected a chain-mismatch error, got nil")
}
t.Logf("chain mismatch correctly rejected: %v", err)
// A nil chainId is also a mismatch (cannot be trusted).
nilCID := &mcp.ChainObservation{BlockNumber: obs.BlockNumber, BlockHash: obs.BlockHash, Tool: toolChainState}
if fresh, _ := nilCID.Verify(context.Background(), env.c, 1000); fresh {
t.Fatal("observation with nil chainId passed Verify")
}
}