136 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|