operator-channel: the bench shapes of channel and intake (Message, Button, Sender, Choice, LinkAsked, Failed, Standing, Code, ProofAnswer) in the definition, with a test against mesh-sdk's channel wire (C-12; ADR 0634 §1; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:45:30 +01:00
parent bd49b184a2
commit 6d61992183
4 changed files with 508 additions and 0 deletions
@@ -0,0 +1,58 @@
package operatorchannelclient
import (
"bytes"
"encoding/json"
"testing"
"time"
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/channel"
)
// TestTheBenchShapesAreTheChannelsWire: each shape the benches channel and intake carry, written by mesh-sdk's channel
// package, reads into the contract's type with no field dropped and reads back as the same wire.
func TestTheBenchShapesAreTheChannelsWire(t *testing.T) {
at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)
who := channel.Sender{Identity: "42", Display: "Op", Authenticated: true}
cases := []struct {
name string
sdk any
into any
}{
{"message", channel.Message{Handle: "h1", Title: "t", Body: "b", Buttons: []channel.Button{{Label: "Approve", Ticket: "tk"}},
Note: "n", Urgent: true, Silent: true, Reply: "r", To: "42", Secret: true}, &seat.Message{}},
{"a send", channel.Message{Handle: "h2", Title: "t", Body: "b"}, &seat.Message{}},
{"choice", channel.Choice{ID: "c1", Ticket: "tk", Handle: "h1", Sender: who, At: at}, &seat.Choice{}},
{"link", channel.LinkAsked{ID: "l1", Sender: who, At: at}, &seat.LinkAsked{}},
{"failed", channel.Failed{Handle: "h1", Why: "blocked", Permanent: true, At: at}, &seat.Failed{}},
{"standing", channel.Standing{Ready: true, Why: "w", EditsSilently: true, At: at}, &seat.Standing{}},
{"code", channel.Code{Sender: who, Code: "123456", Purpose: "approve"}, &seat.Code{}},
{"proof answer", channel.ProofAnswer{Accepted: true, Words: "ok"}, &seat.ProofAnswer{}},
}
for _, c := range cases {
wire, err := json.Marshal(c.sdk)
if err != nil {
t.Fatal(err)
}
dec := json.NewDecoder(bytes.NewReader(wire))
dec.DisallowUnknownFields()
if err := dec.Decode(c.into); err != nil {
t.Errorf("%s: %v", c.name, err)
continue
}
back, _ := json.Marshal(c.into)
var got, want any
_ = json.Unmarshal(back, &got)
_ = json.Unmarshal(wire, &want)
if !jsonEqual(got, want) {
t.Errorf("%s: reads back as %s, want %s", c.name, back, wire)
}
}
}
func jsonEqual(a, b any) bool {
x, _ := json.Marshal(a)
y, _ := json.Marshal(b)
return bytes.Equal(x, y)
}
@@ -149,6 +149,36 @@ type Ask struct {
WholeDetails string `json:"whole-details,omitempty"`
}
// Button is one answer a channel offers: its label and the router's one-time ticket for it.
type Button struct {
// the button's label
Label string `json:"label"`
// the router's one-time ticket for it
Ticket string `json:"ticket"`
}
// Choice is a button chosen on a channel, said on the bench intake as the event `choice`.
type Choice struct {
// when, RFC 3339
At string `json:"at"`
// the message's handle
Handle string `json:"handle,omitempty"`
// the channel's own for this arrival, unique, so the router acts on it once
ID string `json:"id"`
Sender Sender `json:"sender"`
// the ticket of the button
Ticket string `json:"ticket"`
}
// Code is a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept.
type Code struct {
// the code
Code string `json:"code"`
// what it is for
Purpose string `json:"purpose"`
Sender Sender `json:"sender"`
}
// ConditionActionsItem is a type of the contract.
type ConditionActionsItem struct {
Arguments map[string]string `json:"arguments,omitempty"`
@@ -197,6 +227,51 @@ type Condition struct {
V int64 `json:"v"`
}
// Failed is a message a channel could not deliver, said on the bench intake as the event `failed`.
type Failed struct {
// when, RFC 3339
At string `json:"at"`
// the message's handle
Handle string `json:"handle"`
// trying again cannot help
Permanent bool `json:"permanent,omitempty"`
// why
Why string `json:"why"`
}
// LinkAsked is somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`.
type LinkAsked struct {
// when, RFC 3339
At string `json:"at"`
// the channel's own for this arrival
ID string `json:"id"`
Sender Sender `json:"sender"`
}
// Message is the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given.
type Message struct {
// its body
Body string `json:"body"`
// the answers it offers (none for a send)
Buttons []Button `json:"buttons,omitempty"`
// the router's name for it, the same across a show and its edits
Handle string `json:"handle"`
// its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details
Note string `json:"note,omitempty"`
// the choice or link request this answers, by its id: the channel shows it where that was made
Reply string `json:"reply,omitempty"`
// the words carry something shown once (a link's code): the channel keeps no copy
Secret bool `json:"secret,omitempty"`
// shown without notifying
Silent bool `json:"silent,omitempty"`
// its title
Title string `json:"title"`
// the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked
To string `json:"to,omitempty"`
// urgent
Urgent bool `json:"urgent,omitempty"`
}
// Option is a type of the contract.
type Option struct {
// the digest of exactly what the asker performs when it is chosen
@@ -225,6 +300,36 @@ type Person struct {
Who string `json:"who"`
}
// ProofAnswer is the router's answer to a proof.
type ProofAnswer struct {
// whether it was taken
Accepted bool `json:"accepted"`
// words to say to the person
Words string `json:"words"`
}
// Sender is who a channel's service says sent something; the router alone judges whether that is the operator.
type Sender struct {
// whether the service authenticated it
Authenticated bool `json:"authenticated"`
// the name it shows
Display string `json:"display,omitempty"`
// the account
Identity string `json:"identity"`
}
// Standing is what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`.
type Standing struct {
// when, RFC 3339
At string `json:"at"`
// its edits notify nobody
EditsSilently bool `json:"edits-silently,omitempty"`
// it can send now
Ready bool `json:"ready"`
// why not
Why string `json:"why,omitempty"`
}
// Warrant is a type of the contract.
type Warrant struct {
// what it was about
@@ -563,6 +563,246 @@
"subject",
"state"
]
},
"Button": {
"type": "object",
"description": "one answer a channel offers: its label and the router's one-time ticket for it",
"properties": {
"label": {
"type": "string",
"description": "the button's label"
},
"ticket": {
"type": "string",
"description": "the router's one-time ticket for it"
}
},
"required": [
"label",
"ticket"
]
},
"Message": {
"type": "object",
"description": "the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given",
"properties": {
"handle": {
"type": "string",
"description": "the router's name for it, the same across a show and its edits"
},
"title": {
"type": "string",
"description": "its title"
},
"body": {
"type": "string",
"description": "its body"
},
"buttons": {
"type": "array",
"items": {
"$ref": "#/defs/Button"
},
"description": "the answers it offers (none for a send)"
},
"note": {
"type": "string",
"description": "its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details"
},
"urgent": {
"type": "boolean",
"description": "urgent"
},
"silent": {
"type": "boolean",
"description": "shown without notifying"
},
"reply": {
"type": "string",
"description": "the choice or link request this answers, by its id: the channel shows it where that was made"
},
"to": {
"type": "string",
"description": "the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked"
},
"secret": {
"type": "boolean",
"description": "the words carry something shown once (a link's code): the channel keeps no copy"
}
},
"required": [
"handle",
"title",
"body"
]
},
"Sender": {
"type": "object",
"description": "who a channel's service says sent something; the router alone judges whether that is the operator",
"properties": {
"identity": {
"type": "string",
"description": "the account"
},
"display": {
"type": "string",
"description": "the name it shows"
},
"authenticated": {
"type": "boolean",
"description": "whether the service authenticated it"
}
},
"required": [
"identity",
"authenticated"
]
},
"Choice": {
"type": "object",
"description": "a button chosen on a channel, said on the bench intake as the event `choice`",
"properties": {
"id": {
"type": "string",
"description": "the channel's own for this arrival, unique, so the router acts on it once"
},
"ticket": {
"type": "string",
"description": "the ticket of the button"
},
"handle": {
"type": "string",
"description": "the message's handle"
},
"sender": {
"$ref": "#/defs/Sender"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"id",
"ticket",
"sender",
"at"
]
},
"LinkAsked": {
"type": "object",
"description": "somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`",
"properties": {
"id": {
"type": "string",
"description": "the channel's own for this arrival"
},
"sender": {
"$ref": "#/defs/Sender"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"id",
"sender",
"at"
]
},
"Failed": {
"type": "object",
"description": "a message a channel could not deliver, said on the bench intake as the event `failed`",
"properties": {
"handle": {
"type": "string",
"description": "the message's handle"
},
"why": {
"type": "string",
"description": "why"
},
"permanent": {
"type": "boolean",
"description": "trying again cannot help"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"handle",
"why",
"at"
]
},
"Standing": {
"type": "object",
"description": "what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`",
"properties": {
"ready": {
"type": "boolean",
"description": "it can send now"
},
"why": {
"type": "string",
"description": "why not"
},
"edits-silently": {
"type": "boolean",
"description": "its edits notify nobody"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"ready",
"at"
]
},
"Code": {
"type": "object",
"description": "a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept",
"properties": {
"sender": {
"$ref": "#/defs/Sender"
},
"code": {
"type": "string",
"description": "the code"
},
"purpose": {
"type": "string",
"description": "what it is for"
}
},
"required": [
"sender",
"code",
"purpose"
]
},
"ProofAnswer": {
"type": "object",
"description": "the router's answer to a proof",
"properties": {
"accepted": {
"type": "boolean",
"description": "whether it was taken"
},
"words": {
"type": "string",
"description": "words to say to the person"
}
},
"required": [
"accepted",
"words"
]
}
}
}
@@ -108,6 +108,36 @@ export interface Ask {
"whole-details"?: string;
}
/** one answer a channel offers: its label and the router's one-time ticket for it */
export interface Button {
/** the button's label */
"label": string;
/** the router's one-time ticket for it */
"ticket": string;
}
/** a button chosen on a channel, said on the bench intake as the event `choice` */
export interface Choice {
/** when, RFC 3339 */
"at": string;
/** the message's handle */
"handle"?: string;
/** the channel's own for this arrival, unique, so the router acts on it once */
"id": string;
"sender": Sender;
/** the ticket of the button */
"ticket": string;
}
/** a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept */
export interface Code {
/** the code */
"code": string;
/** what it is for */
"purpose": string;
"sender": Sender;
}
export interface ConditionActionsItem {
"arguments"?: Record<string, string>;
/** label */
@@ -153,6 +183,51 @@ export interface Condition {
"v": number;
}
/** a message a channel could not deliver, said on the bench intake as the event `failed` */
export interface Failed {
/** when, RFC 3339 */
"at": string;
/** the message's handle */
"handle": string;
/** trying again cannot help */
"permanent"?: boolean;
/** why */
"why": string;
}
/** somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link` */
export interface LinkAsked {
/** when, RFC 3339 */
"at": string;
/** the channel's own for this arrival */
"id": string;
"sender": Sender;
}
/** the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given */
export interface Message {
/** its body */
"body": string;
/** the answers it offers (none for a send) */
"buttons"?: Button[];
/** the router's name for it, the same across a show and its edits */
"handle": string;
/** its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details */
"note"?: string;
/** the choice or link request this answers, by its id: the channel shows it where that was made */
"reply"?: string;
/** the words carry something shown once (a link's code): the channel keeps no copy */
"secret"?: boolean;
/** shown without notifying */
"silent"?: boolean;
/** its title */
"title": string;
/** the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked */
"to"?: string;
/** urgent */
"urgent"?: boolean;
}
export interface Option {
/** the digest of exactly what the asker performs when it is chosen */
"binds"?: string;
@@ -179,6 +254,36 @@ export interface Person {
"who": string;
}
/** the router's answer to a proof */
export interface ProofAnswer {
/** whether it was taken */
"accepted": boolean;
/** words to say to the person */
"words": string;
}
/** who a channel's service says sent something; the router alone judges whether that is the operator */
export interface Sender {
/** whether the service authenticated it */
"authenticated": boolean;
/** the name it shows */
"display"?: string;
/** the account */
"identity": string;
}
/** what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing` */
export interface Standing {
/** when, RFC 3339 */
"at": string;
/** its edits notify nobody */
"edits-silently"?: boolean;
/** it can send now */
"ready": boolean;
/** why not */
"why"?: string;
}
export interface Warrant {
/** what it was about */
"about"?: string;