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) } }