Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/manifest_test.go
T

136 lines
5.0 KiB
Go

package main
// The manifest says what the code does (novox/hq ADR 0459 §1, §2; issue 1426): the seat it claims and the
// verbs it serves, and that it invokes mail-store's read-only verbs alone — never one that changes the
// mailbox (ADR 0472: mesh-mail-reader is granted reading only, and its manifest invokes no send verb).
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"
)
type seat struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
}
type manifest struct {
Module string `json:"module"`
RunsAs string `json:"runs-as"`
Claims []seat `json:"claims"`
Invokes []string `json:"invokes"`
Consumes []string `json:"consumes"`
Tools []string `json:"tools"`
Requires []string `json:"requires"`
Contributes map[string]map[string]any `json:"contributes"`
Resources []map[string]any `json:"resources"`
}
func readManifest(t *testing.T) (manifest, map[string]map[string]any) {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
byID := map[string]map[string]any{}
for _, r := range m.Resources {
byID[r["id"].(string)] = r
}
return m, byID
}
// 1426/AC2: runs-as mesh-mail-reader, claims mail-reading (scope mesh) with the six verbs, and invokes
// seat:mail-store.read and seat:mail-store.accounts but no send, send-status or any verb that changes the
// mailbox.
func TestTheManifestClaimsMailReadingAndInvokesReadOnly(t *testing.T) {
m, _ := readManifest(t)
if m.Module != Module {
t.Fatalf("module %q, want %q", m.Module, Module)
}
if m.RunsAs != Module {
t.Fatalf("runs-as %q, want %q", m.RunsAs, Module)
}
wantClaims := []seat{
{Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status"}},
}
if !reflect.DeepEqual(m.Claims, wantClaims) {
t.Fatalf("claims %+v, want %+v", m.Claims, wantClaims)
}
// mail-store.read and mail-store.accounts only (1426/AC2); claude_code_judge/judged (1520), messenger_check
// and operator-channel.tell (1522) are its other invokes, added as each landed — never send or send-status.
wantInvokes := []string{
"seat:mail-store.read", "seat:mail-store.accounts",
"claude-code.claude_code_judge", "claude-code.claude_code_judged",
"messenger.messenger_check", "seat:operator-channel.tell",
}
if !reflect.DeepEqual(m.Invokes, wantInvokes) {
t.Fatalf("invokes %v, want exactly %v (no send, send-status, or any verb that changes the mailbox)", m.Invokes, wantInvokes)
}
for _, forbidden := range []string{"seat:mail-store.send", "seat:mail-store.send-status"} {
for _, inv := range m.Invokes {
if inv == forbidden {
t.Fatalf("invokes names %s, which changes the mailbox (ADR 0472)", forbidden)
}
}
}
}
// 1426/AC3 (the manifest's own half): the manifest's tools match the bundle's Tools(), in the same order.
func TestTheManifestListsTheSameToolsAsTheBundle(t *testing.T) {
m, _ := readManifest(t)
var names []string
for _, tool := range (&Bundle{}).Tools() {
names = append(names, tool.Name)
}
if !reflect.DeepEqual(m.Tools, names) {
t.Fatalf("manifest tools %v, the code serves %v", m.Tools, names)
}
}
// 1517/AC2: the manifest requires postgres-database, binds it, renders database-url and runs a prepare
// process (run-once) with DATABASE_URL_FILE, as mesh-mail does.
func TestTheManifestRequiresItsOwnDatabase(t *testing.T) {
m, byID := readManifest(t)
if !reflect.DeepEqual(m.Requires, []string{"postgres-database"}) {
t.Fatalf("requires %v, want [postgres-database]", m.Requires)
}
if m.Contributes["postgres-database"]["name"] != "mail-reader" {
t.Fatalf("contributes postgres-database %v", m.Contributes["postgres-database"])
}
databaseURL := byID["database-url"]
if databaseURL == nil || databaseURL["type"] != "file" || databaseURL["template"] != "files/database-url" {
t.Fatalf("database-url resource: %v", databaseURL)
}
prepare := byID["prepare"]
if prepare == nil || prepare["type"] != "process" || prepare["run-once"] != true {
t.Fatalf("prepare resource: %v", prepare)
}
run, _ := prepare["run"].([]any)
if len(run) != 2 || run[1] != "prepare" {
t.Fatalf("prepare's run: %v", prepare["run"])
}
env, _ := prepare["env"].(map[string]any)
if env["DATABASE_URL_FILE"] != databaseURL["path"] {
t.Fatalf("prepare's DATABASE_URL_FILE is %v, database-url's own path is %v", env["DATABASE_URL_FILE"], databaseURL["path"])
}
}
// 1518/AC5: the manifest consumes mesh-mail.mail.*.message.added and runs-as mesh-mail-reader.
func TestTheManifestConsumesMeshMailsAddedEvent(t *testing.T) {
m, _ := readManifest(t)
if !reflect.DeepEqual(m.Consumes, []string{AddedPattern}) {
t.Fatalf("consumes %v, want [%s]", m.Consumes, AddedPattern)
}
if m.RunsAs != Module {
t.Fatalf("runs-as %q, want %q", m.RunsAs, Module)
}
}