package launch import ( "encoding/json" "errors" "os" "path/filepath" "sync" "testing" "time" ) // A bundle that answers `mesh/event` as $ANSWER says: an error, a result, or an answer with neither. const answeringBundle = `#!/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":"s1","method":"mesh/subscribe","params":{"pattern":"#"}}\n' ;; *'"method":"mesh/event"'*) case "$ANSWER" in error) printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32000,"message":"Unexpected end of JSON input"}}\n' "$id" ;; result) printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;; bare) printf '{"jsonrpc":"2.0","id":%s}\n' "$id" ;; esac ;; esac done ` type subscribing struct { mu sync.Mutex deliver func(json.RawMessage) error } func (b *subscribing) Publish(json.RawMessage) error { return nil } func (b *subscribing) Ask(json.RawMessage) (json.RawMessage, error) { return nil, nil } func (b *subscribing) State(string, json.RawMessage) (json.RawMessage, error) { return nil, nil } func (b *subscribing) Watch(json.RawMessage, func(json.RawMessage) error) (func(), error) { return func() {}, nil } func (b *subscribing) Subscribe(d func(json.RawMessage) error) error { b.mu.Lock() defer b.mu.Unlock() b.deliver = d return nil } func deliverTo(t *testing.T, answer string) error { t.Helper() entry := filepath.Join(t.TempDir(), "bundle") if err := os.WriteFile(entry, []byte(answeringBundle), 0o755); err != nil { t.Fatal(err) } b := &subscribing{} l, err := Start("plex", entry, append(os.Environ(), "ANSWER="+answer), b, t.Logf) if err != nil { t.Fatal(err) } t.Cleanup(l.Stop) for i := 0; ; i++ { b.mu.Lock() d := b.deliver b.mu.Unlock() if d != nil { return d(json.RawMessage(`{"key":"radarr.download.completed","body":{"title":"x"}}`)) } if i > 100 { t.Fatal("the bundle never subscribed") } time.Sleep(20 * time.Millisecond) } } // An event is taken by any answer that is not an error, and not taken by an error, which reaches the // runtime as the bundle's own words — not as something the runtime failed to read (hq issue 276). func TestAnEventIsTakenByAnyAnswerThatIsNotAnError(t *testing.T) { for _, answer := range []string{"result", "bare"} { if err := deliverTo(t, answer); err != nil { t.Errorf("an answer %q did not take the event: %v", answer, err) } } err := deliverTo(t, "error") var refused *Refused if !errors.As(err, &refused) || refused.Method != "mesh/event" || refused.Message != "Unexpected end of JSON input" { t.Fatalf("an error answer was not the bundle's refusal of mesh/event: %#v", err) } }