package launch import ( "encoding/json" "os" "path/filepath" "strings" "sync" "testing" "time" ) // puttingBundle asks the capability words, and once they are answered with put-sessions sends the one chunk of a put // session, as mesh-sdk's bundle does; it then ends, which closes its channel. const puttingBundle = `#!/bin/sh while IFS= read -r line; do id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\)[,}].*/\1/p') case "$line" in *'"method":"initialize"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;; *'"method":"tools/list"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id" printf '{"jsonrpc":"2.0","id":"w1","method":"mesh/capabilities","params":{}}\n' ;; *'"id":"w1"'**put-sessions*) printf '{"jsonrpc":"2.0","id":"p1","method":"mesh/objects.put-chunk","params":{"objects":"o","name":"n","call":"c","seq":1,"last":true,"data":"aGk="}}\n' ;; *'"id":"p1"'*) exit 0 ;; esac done ` type sessionBus struct { subscribing mu sync.Mutex chunks []string dropped chan struct{} } func (b *sessionBus) CapabilityWords() []string { return []string{"put-sessions"} } func (b *sessionBus) BusState(method string, params json.RawMessage) (json.RawMessage, error) { b.mu.Lock() b.chunks = append(b.chunks, method+" "+string(params)) b.mu.Unlock() return json.RawMessage(`{"name":"n"}`), nil } func (b *sessionBus) DropSessions() { close(b.dropped) } // Design 47 G §576, §518: a bundle reads the runner's capability words with mesh/capabilities, then runs its put session // through the runner's put-chunk, and the sessions of its channel are dropped when the channel closes. func TestABundleReadsTheCapabilityWordsRunsAPutSessionAndItsChannelsClosingDropsThem(t *testing.T) { dir := t.TempDir() entry := filepath.Join(dir, "bundle") if err := os.WriteFile(entry, []byte(puttingBundle), 0o755); err != nil { t.Fatal(err) } b := &sessionBus{dropped: make(chan struct{})} l, err := Start("putter", entry, os.Environ(), b, func(string, ...any) {}) if err != nil { t.Fatal(err) } t.Cleanup(l.Stop) select { case <-b.dropped: case <-time.After(5 * time.Second): t.Fatal("the bundle's sessions were not dropped when its channel closed") } b.mu.Lock() defer b.mu.Unlock() if len(b.chunks) != 1 || !strings.HasPrefix(b.chunks[0], "mesh/objects.put-chunk ") || !strings.Contains(b.chunks[0], `"seq":1`) { t.Fatalf("the runner took %v", b.chunks) } }