Files
new-api/controller/plugin_protocol_test.go
CaIon 6c22550ea3 feat(task): resolve channel-mapped aliases and case variants for plugin models
Channel model_mapping keys exposed in a channel's model list now act as
first-class aliases for task-plugin models across the whole line:

- Derived alias view (model/task_model_alias.go): built from enabled
  channels' model_mapping, chain-following with cycle detection, declared
  names always win, cross-plugin conflicts dropped. Rebuilt on channel
  cache refresh, registry generation change, and a 60s TTL.
- Request path: PinTaskPluginEndpoint resolves declared-name case folds
  and mapping aliases before endpoint lookup (never rewriting the body
  until the endpoint is claimed), pins with MappedModel, and the decode
  contract accepts alias echoes without loosening model ownership for
  normal pins. Legacy /v1/tasks submit folds case variants the same way.
  Fixes aliases on POST /v1/responses silently falling through to the
  main relay against task channels.
- Mapping order: ModelMappedHelper now runs before the plugin submit
  hook builds and caches the upstream body, so channel model_mapping
  actually reaches the upstream request. Plugins receive the mapped
  name as ctx.upstreamModel in both decode and submit contexts.
- Billing: identity stays the origin name; when the alias has no tiered
  expression, the selected channel's mapping tail expression applies.
  Pricing page and billing-expr smoke tests resolve aliases to the
  owning plugin's usage schema.
- Case folding: ASCII-only fold with exact-match priority; same-plugin
  and cross-plugin fold collisions rejected at registration.
- Plugins: model-keyed rate tables, req_key derivation, and combo
  validation in doubao/kling/jimeng/hailuo/vidu/sunoapi now key on
  ctx.upstreamModel || ctx.model; render/echo paths keep ctx.model.
2026-08-30 19:13:51 +08:00

1582 lines
59 KiB
Go

package controller
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
pluginruntime "github.com/QuantumNous/new-api/pkg/jsplugin"
"github.com/QuantumNous/new-api/relay"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestServeTaskPluginProtocolWaitsForDurableSubmissionBeforeWriting(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "durable-barrier", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, false)
submitStarted := make(chan struct{})
releaseSubmit := make(chan struct{})
done := make(chan struct{})
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
close(submitStarted)
<-releaseSubmit
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_durable", map[string]any{
"must_not": "be_written",
}), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return nil, false, errors.New("observation failed after durable barrier")
}
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
assert.Empty(t, recorder.Header().Get("Content-Type"))
assert.Empty(t, recorder.Body.String())
assert.False(t, recorder.Flushed)
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "protocol handler did not finish")
}
assert.Equal(t, "text/event-stream", recorder.Header().Get("Content-Type"))
assert.Contains(t, recorder.Body.String(), "event: response.created\n")
assert.NotContains(t, recorder.Body.String(), "must_not")
}
func TestServeTaskPluginProtocolDisconnectDuringSubmissionFinishesDurableWithoutWriting(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "disconnect-during-submit", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
submitStarted := make(chan struct{})
checkSubmissionContext := make(chan struct{})
submissionContextActive := make(chan struct{})
releaseSubmit := make(chan struct{})
observationStarted := make(chan struct{}, 1)
done := make(chan struct{})
deps := pluginProtocolTestDeps()
deps.submit = func(c *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
close(submitStarted)
<-checkSubmissionContext
select {
case <-c.Request.Context().Done():
return nil, service.TaskErrorWrapperLocal(c.Request.Context().Err(), "request_cancelled", http.StatusRequestTimeout)
default:
close(submissionContextActive)
}
<-releaseSubmit
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_disconnect_durable", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
observationStarted <- struct{}{}
return nil, false, errors.New("observation must not start after disconnect")
}
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
cancel()
close(checkSubmissionContext)
select {
case <-submissionContextActive:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission context was canceled with the client")
}
select {
case <-done:
require.FailNow(t, "protocol handler stopped before submission became durable")
default:
}
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "protocol handler did not finish after durable submission")
}
assert.Empty(t, recorder.Header().Get("Content-Type"))
assert.Empty(t, recorder.Body.String())
assert.False(t, recorder.Flushed)
select {
case <-observationStarted:
require.FailNow(t, "protocol observation started after client disconnect")
default:
}
}
func TestServeTaskPluginProtocolDisconnectBeforeDurableBarrierPersistsAndSettlesWithoutRefund(t *testing.T) {
events := make([]string, 0, 3)
database := setupTaskSubmissionDatabase(t, true, &events)
previousLogConsumeEnabled := common.LogConsumeEnabled
common.LogConsumeEnabled = false
t.Cleanup(func() { common.LogConsumeEnabled = previousLogConsumeEnabled })
pinned := compilePluginProtocolTestEndpoint(t, "disconnect-before-durable", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
billing := &taskSubmissionTestBilling{events: &events}
submitStarted := make(chan struct{})
releaseSubmit := make(chan struct{})
observationStarted := make(chan struct{}, 1)
done := make(chan struct{})
deps := pluginProtocolTestDeps()
deps.submit = func(c *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
info.Billing = billing
info.TaskRelayInfo.PublicTaskID = "task_disconnect_persisted"
info.TaskRelayInfo.LockedChannel = &model.Channel{
Id: 1,
Type: constant.ChannelTypeTaskPlugin,
Name: "disconnect-before-durable",
}
info.ChannelMeta = &relaycommon.ChannelMeta{
ChannelId: 1,
ChannelType: constant.ChannelTypeTaskPlugin,
}
return executeTaskSubmissionWith(c, info, func(*gin.Context, *relaycommon.RelayInfo) (*relay.TaskSubmitResult, *dto.TaskError) {
close(submitStarted)
<-releaseSubmit
return &relay.TaskSubmitResult{
UpstreamTaskID: "upstream_disconnect_persisted",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
Quota: 7,
}, nil
})
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
observationStarted <- struct{}{}
return nil, false, errors.New("observation must not start after disconnect")
}
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
cancel()
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "detached submission did not finish")
}
assert.Equal(t, []string{"reserve", "insert", "settle"}, events)
assert.Zero(t, billing.refunds)
var persisted model.Task
require.NoError(t, database.Where("task_id = ?", "task_disconnect_persisted").First(&persisted).Error)
assert.Equal(t, model.TaskStatus(model.TaskStatusNotStart), persisted.Status)
assert.Equal(t, 7, persisted.Quota)
assert.Equal(t, "upstream_disconnect_persisted", persisted.PrivateData.UpstreamTaskID)
assert.Empty(t, recorder.Header().Get("Content-Type"))
assert.Empty(t, recorder.Body.String())
assert.False(t, recorder.Flushed)
select {
case <-observationStarted:
require.FailNow(t, "protocol observation started after client disconnect")
default:
}
}
func TestServeTaskPluginProtocolDisconnectDuringTerminalSettlementStopsOnlyObservation(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "disconnect-terminal-settlement", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
previousDB := model.DB
previousMemoryCache := common.MemoryCacheEnabled
database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, database.AutoMigrate(&model.Channel{}, &model.Task{}))
model.DB = database
common.MemoryCacheEnabled = false
t.Cleanup(func() {
model.DB = previousDB
common.MemoryCacheEnabled = previousMemoryCache
})
baseURL := "https://example.com"
channel := model.Channel{
Type: constant.ChannelTypeTaskPlugin,
Name: "terminal-settlement",
Key: "test-key",
BaseURL: &baseURL,
Status: common.ChannelStatusEnabled,
}
require.NoError(t, database.Create(&channel).Error)
task := model.Task{
TaskID: "task_terminal_disconnect",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
ChannelId: channel.Id,
Quota: 10,
Status: model.TaskStatusSubmitted,
PrivateData: model.TaskPrivateData{
UpstreamTaskID: "upstream-terminal",
},
}
require.NoError(t, database.Create(&task).Error)
c, recorder := newPluginProtocolTestContext(true, true)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
billingEvents := make([]string, 0)
billing := &taskSubmissionTestBilling{events: &billingEvents}
observationStarted := make(chan struct{})
settlementStarted := make(chan struct{})
releaseSettlement := make(chan struct{})
t.Cleanup(func() {
select {
case <-releaseSettlement:
default:
close(releaseSettlement)
}
})
pollingDone := make(chan struct{})
adaptor := &terminalSettlementPollingAdaptor{
started: settlementStarted,
release: releaseSettlement,
}
previousAdaptorFactory := service.GetTaskAdaptorFunc
service.GetTaskAdaptorFunc = func(constant.TaskPlatform) service.TaskPollingAdaptor {
return adaptor
}
t.Cleanup(func() { service.GetTaskAdaptorFunc = previousAdaptorFactory })
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
info.Billing = billing
return &taskSubmissionOutcome{
Result: &relay.TaskSubmitResult{},
Task: &task,
RelayInfo: info,
}, nil
}
deps.loadTask = func(ctx context.Context, _ int, _ constant.TaskPlatform, _ string) (*model.Task, bool, error) {
close(observationStarted)
<-ctx.Done()
return nil, false, ctx.Err()
}
done := make(chan struct{})
go func() {
<-observationStarted
defer close(pollingDone)
service.DispatchPlatformUpdate(
context.Background(),
task.Platform,
map[int][]string{channel.Id: {"upstream-terminal"}},
map[string]*model.Task{"upstream-terminal": &task},
)
}()
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-settlementStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "terminal settlement did not start")
}
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "protocol observation did not stop after terminal disconnect")
}
assert.Equal(t, []string{"response.created"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.Zero(t, billing.refunds)
close(releaseSettlement)
select {
case <-pollingDone:
case <-time.After(2 * time.Second):
require.FailNow(t, "terminal settlement was canceled with the client observation")
}
var persisted model.Task
require.NoError(t, database.Where("task_id = ?", task.TaskID).First(&persisted).Error)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), persisted.Status)
assert.Equal(t, "100%", persisted.Progress)
assert.Equal(t, 10, persisted.Quota)
assert.True(t, adaptor.completed)
assert.Empty(t, billingEvents)
}
type terminalSettlementPollingAdaptor struct {
started chan struct{}
release chan struct{}
completed bool
}
func (a *terminalSettlementPollingAdaptor) Init(*relaycommon.RelayInfo) {}
func (a *terminalSettlementPollingAdaptor) FetchTask(string, string, map[string]any, string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{}`)),
}, nil
}
func (a *terminalSettlementPollingAdaptor) ParseTaskResult([]byte) (*relaycommon.TaskInfo, error) {
return &relaycommon.TaskInfo{
Status: model.TaskStatusSuccess,
Progress: "100%",
}, nil
}
func (a *terminalSettlementPollingAdaptor) AdjustBillingOnComplete(task *model.Task, _ *relaycommon.TaskInfo) int {
close(a.started)
<-a.release
a.completed = true
return task.Quota
}
func TestPluginProtocolBridgeBoundsDatabaseReadBelowHeartbeat(t *testing.T) {
deps := pluginProtocolBridgeDeps{
observationTimeout: time.Minute,
loadTimeout: 10 * time.Second,
tickInterval: time.Second,
heartbeatInterval: 4 * time.Second,
admissionTimeout: time.Second,
}.withDefaults()
assert.Equal(t, 2*time.Second, deps.loadTimeout)
}
func TestServeTaskPluginProtocolPostDurableObservationFailureUsesCanonicalResponse(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "observation-failure", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_observation_failure", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return nil, false, errors.New("database-secret https://database.invalid/?token=hidden")
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task could not be observed.", response.Error.Message)
assert.Equal(t, "queued", response.Metadata["task_status"])
assert.Equal(t, "/v1/responses/resp_observation_failure", response.Metadata["retrieval_path"])
assert.NotContains(t, recorder.Body.String(), "secret")
assert.NotContains(t, recorder.Body.String(), "database.invalid")
}
func TestServeTaskPluginProtocolStreamsPinnedGenerationWithHostFraming(t *testing.T) {
oldPinned := compilePluginProtocolTestEndpoint(t, "generation-pinned", `
export const protocols = {openai_responses: {
renderEvents: function(ctx, task, previousState) {
if (ctx.stream !== true || ctx.body.value.stream !== true) {
throw new Error("host did not preserve parsed stream mode");
}
if (arguments.length === 2) {
return {events: [], state: null, done: false};
}
if (arguments.length !== 3 || previousState !== null) {
throw new Error("explicit null state was not supplied on the next tick");
}
return {events: [{type: "output", data: "old-generation"}], done: true};
},
renderFinal: function() { throw new Error("stream called renderFinal"); }
}};
`)
newPinned := compilePluginProtocolTestEndpoint(t, "generation-pinned", `
export const protocols = {openai_responses: {
renderEvents: function() {
return {events: [{type: "output", data: "new-generation"}], done: true};
},
renderFinal: function() { return "new-generation"; }
}};
`)
require.NotSame(t, oldPinned.Plugin.Engine, newPinned.Plugin.Engine)
c, recorder := newPluginProtocolTestContext(true, true)
c.Set(pluginruntime.ContextKeyRouteRequest, pluginruntime.RouteRequestContext{
Path: "/v1/responses",
Method: http.MethodPost,
RequestBody: map[string]any{"model": "video-model", "stream": false},
})
loadCount := 0
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, oldPinned.Plugin.Meta.Key, "task_generation", map[string]any{
"client_response": "ignored",
}), nil
}
deps.loadTask = func(_ context.Context, userID int, platform constant.TaskPlatform, taskID string) (*model.Task, bool, error) {
loadCount++
assert.Equal(t, 71, userID)
assert.Equal(t, constant.TaskPlatform(oldPinned.Plugin.Meta.Key), platform)
assert.Equal(t, "task_generation", taskID)
status := model.TaskStatus(model.TaskStatusInProgress)
if loadCount == 2 {
status = model.TaskStatus(model.TaskStatusSuccess)
}
return &model.Task{
TaskID: taskID,
UserId: userID,
Platform: platform,
Status: status,
}, true, nil
}
serveTaskPluginProtocol(c, oldPinned, deps)
assert.Equal(t, 2, loadCount)
assert.True(t, recorder.Flushed)
assert.Equal(t, "text/event-stream", recorder.Header().Get("Content-Type"))
assert.Equal(t, []string{
"response.created",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.True(t, strings.HasPrefix(recorder.Body.String(), "event: response.created\ndata: {"))
assert.Contains(t, recorder.Body.String(), `"sequence_number":0`)
assert.Contains(t, recorder.Body.String(), `"sequence_number":7`)
assert.Contains(t, recorder.Body.String(), "old-generation")
assert.NotContains(t, recorder.Body.String(), "new-generation")
}
func TestServeTaskPluginProtocolStreamMissingRenderEventsUsesFailureEnvelope(t *testing.T) {
tests := []struct {
name string
status model.TaskStatus
}{
{name: "success", status: model.TaskStatusSuccess},
{name: "failure", status: model.TaskStatusFailure},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "default-events-"+testCase.name, `
export const protocols = {openai_responses: {
renderFinal: function() { throw new Error("stream must not call renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_default_events"), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return &model.Task{TaskID: "task_default_events", Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key), UserId: 71, Status: testCase.status}, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, []string{"response.created", "response.failed"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.NotContains(t, recorder.Body.String(), "stream must not call")
})
}
}
func TestServeTaskPluginProtocolStreamInjectsHostArtifactCapabilities(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "stream-artifacts", `
export function listArtifacts(task) {
if (task.data.output.video_url !== "https://upstream.invalid/video.mp4?secret=hidden") {
throw new Error("listArtifacts did not receive raw Task.Data");
}
return [{key: "video", type: "video", mimeType: "video/mp4"}];
}
export function buildContentRequest() {
throw new Error("rendering must not resolve provider content");
}
export const protocols = {openai_responses: {
renderEvents: function(ctx, task) {
const artifact = ctx.artifacts && ctx.artifacts.video;
if (!artifact || artifact.key !== "video" || artifact.type !== "video" ||
artifact.mimeType !== "video/mp4") {
throw new Error("host artifact context is invalid");
}
return {events: [{type: "output", data: artifact.url}], done: true};
},
renderFinal: function() { throw new Error("stream called renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_stream_artifact", nil), nil
}
task := &model.Task{
TaskID: "task_stream_artifact",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
task.SetData(map[string]any{
"output": map[string]any{
"video_url": "https://upstream.invalid/video.mp4?secret=hidden",
},
})
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return task, true, nil
}
deps.artifactContentURL = func(taskID, artifactKey string) (string, error) {
assert.Equal(t, "task_stream_artifact", taskID)
assert.Equal(t, "video", artifactKey)
return "https://gateway.example/v1/tasks/task_stream_artifact/artifacts/video/content?access=host-capability", nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), task.Status)
assert.Equal(t, []string{
"response.created",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.Contains(t, recorder.Body.String(), "host-capability")
assert.NotContains(t, recorder.Body.String(), "upstream.invalid")
assert.NotContains(t, recorder.Body.String(), "secret")
}
func TestTaskPluginProtocolHeartbeatDoesNotDispatchEmptySDKEvent(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
require.NoError(t, writeTaskPluginProtocolHeartbeat(c))
assert.Equal(t, ": PING\n", recorder.Body.String())
assert.True(t, recorder.Flushed)
}
func TestServeTaskPluginProtocolNonStreamUsesFinalHookAndHostEnvelope(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "final-response", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("non-stream called renderEvents"); },
renderFinal: function(ctx, task) {
if (ctx.stream !== false || ctx.body.value.stream !== false) {
throw new Error("host did not preserve parsed non-stream mode");
}
return {
id: "plugin-controlled-id",
status: "plugin-controlled-status",
metadata: {plugin_field: "kept", task_id: "plugin-controlled-task"},
output: [{
id: "plugin-controlled-item",
type: "message",
status: "plugin-controlled-item-status",
role: "assistant",
content: [{
id: "plugin-controlled-content",
type: "output_text",
text: task.data.value,
annotations: [],
logprobs: []
}]
}],
custom_field: "kept"
};
}
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
c.Set(pluginruntime.ContextKeyRouteRequest, pluginruntime.RouteRequestContext{
Path: "/v1/responses",
Method: http.MethodPost,
RequestBody: map[string]any{"model": "video-model", "stream": true},
})
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_final", map[string]any{
"client_response_secret": "must-be-ignored",
}), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
task := &model.Task{
TaskID: "task_final",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
task.SetData(map[string]any{"value": "plugin-semantic-result"})
return task, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.NotEqual(t, "text/event-stream", recorder.Header().Get("Content-Type"))
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "resp_final", response.ID)
assert.Equal(t, "response", response.Object)
assert.Equal(t, "completed", response.Status)
assert.Equal(t, "video-model", response.Model)
assert.Equal(t, "task_final", response.Metadata["task_id"])
assert.Equal(t, "kept", response.Metadata["plugin_field"])
require.Len(t, response.Output, 1)
assert.Equal(t, "item_task_final_0", response.Output[0].ID)
assert.Equal(t, "completed", response.Output[0].Status)
require.Len(t, response.Output[0].Content, 1)
assert.Equal(t, "content_task_final_0_0", response.Output[0].Content[0].ID)
assert.Equal(t, "plugin-semantic-result", response.Output[0].Content[0].Text)
assert.Contains(t, recorder.Body.String(), `"custom_field":"kept"`)
assert.NotContains(t, recorder.Body.String(), "plugin-controlled-id")
assert.NotContains(t, recorder.Body.String(), "client_response_secret")
}
func TestServeTaskPluginProtocolNonStreamInjectsHostArtifactCapabilities(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "final-artifacts", `
export function listArtifacts() {
return [{key: "video", type: "video"}];
}
export function buildContentRequest() {
throw new Error("rendering must not resolve provider content");
}
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("non-stream called renderEvents"); },
renderFinal: function(ctx) {
const artifact = ctx.artifacts && ctx.artifacts.video;
if (!artifact || artifact.key !== "video" || artifact.type !== "video" ||
Object.prototype.hasOwnProperty.call(artifact, "mimeType")) {
throw new Error("host artifact context is invalid");
}
return {
output: [{
type: "message",
status: "completed",
role: "assistant",
content: [{
type: "output_text",
text: artifact.url,
annotations: [],
logprobs: []
}]
}]
};
}
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_final_artifact", nil), nil
}
task := &model.Task{
TaskID: "task_final_artifact",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return task, true, nil
}
deps.artifactContentURL = func(taskID, artifactKey string) (string, error) {
assert.Equal(t, "task_final_artifact", taskID)
assert.Equal(t, "video", artifactKey)
return "https://gateway.example/v1/tasks/task_final_artifact/artifacts/video/content?access=host-capability", nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), task.Status)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "completed", response.Status)
require.Len(t, response.Output, 1)
require.Len(t, response.Output[0].Content, 1)
assert.Contains(t, response.Output[0].Content[0].Text, "host-capability")
}
func TestServeTaskPluginProtocolArtifactURLFailureOnlyFailsCurrentRendering(t *testing.T) {
for _, stream := range []bool{false, true} {
t.Run(strconv.FormatBool(stream), func(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "artifact-url-failure-"+strconv.FormatBool(stream), `
export function listArtifacts() {
return [{key: "video", type: "video"}];
}
export function buildContentRequest() {
throw new Error("unused");
}
export const protocols = {openai_responses: {
renderEvents: function() {
return {events: [{type: "output", data: "must-not-render"}], done: true};
},
renderFinal: function() {
return {output: []};
}
}};
`)
c, recorder := newPluginProtocolTestContext(stream, stream)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_capability_failure", nil), nil
}
task := &model.Task{
TaskID: "task_capability_failure",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return task, true, nil
}
deps.artifactContentURL = func(string, string) (string, error) {
return "", errors.New("public address is unavailable")
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), task.Status)
assert.NotContains(t, recorder.Body.String(), "must-not-render")
assert.NotContains(t, recorder.Body.String(), "public address")
if stream {
assert.Equal(t, []string{"response.created", "response.failed"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
} else {
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "completed", response.Metadata["task_status"])
}
})
}
}
func TestServeTaskPluginProtocolNonStreamTaskFailureSkipsFinalHook(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolTestEndpointWithOptions(t, "failed-final", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() {
console.log("renderFinal called");
return {
output: [{id: "secret-id", content: [{text: "plugin-secret"}]}],
secret: "https://secret.invalid/"
};
}
}};
`, pluginruntime.Options{
Log: func(message string) { logs = append(logs, message) },
})
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.artifactContentURL = func(string, string) (string, error) {
require.FailNow(t, "failed tasks must not project artifact URLs")
return "", nil
}
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_failed", map[string]any{
"credential": "client-response-secret",
}), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
task := &model.Task{
TaskID: "task_failed",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
FailReason: "upstream credential at https://secret.invalid/",
}
task.SetData(map[string]any{"secret": "database-secret"})
return task, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task failed.", response.Error.Message)
assert.Empty(t, response.Output)
assert.NotContains(t, recorder.Body.String(), "secret")
assert.NotContains(t, recorder.Body.String(), "credential")
assert.Empty(t, logs)
}
func TestServeTaskPluginProtocolStreamTaskFailureSuppressesPluginAndDatabaseDetails(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "failed-stream", `
export const protocols = {openai_responses: {
renderEvents: function() {
return {
events: [{type: "output", data: "plugin-secret https://plugin.invalid/?key=hidden"}],
done: true
};
},
renderFinal: function() { return "unused-secret"; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, false)
deps := pluginProtocolTestDeps()
deps.artifactContentURL = func(string, string) (string, error) {
require.FailNow(t, "failed tasks must not project artifact URLs")
return "", nil
}
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_stream_failed", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
task := &model.Task{
TaskID: "task_stream_failed",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
FailReason: "database-secret https://database.invalid/?token=hidden",
}
task.SetData(map[string]any{"secret": "private-result"})
return task, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, []string{"response.created", "response.failed"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.Contains(t, recorder.Body.String(), `"code":"server_error"`)
assert.Contains(t, recorder.Body.String(), `"message":"The task failed."`)
assert.Contains(t, recorder.Body.String(), `"task_status":"failed"`)
assert.NotContains(t, recorder.Body.String(), "secret")
assert.NotContains(t, recorder.Body.String(), "invalid")
assert.NotContains(t, recorder.Body.String(), "hidden")
}
func TestServeTaskPluginProtocolRejectsUnsupportedProtocolBeforeSubmission(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "unsupported-protocol", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
pinned.Protocol = "unsupported"
c, recorder := newPluginProtocolTestContext(false, false)
c.Set(pluginruntime.ContextKeyProtocolRequest, pluginruntime.ProtocolRequestContext{
RouteRequestContext: pluginruntime.RouteRequestContext{
Path: "/v1/videos",
Method: http.MethodPost,
RequestBody: map[string]any{"model": "video-model"},
},
Protocol: pinned.Protocol,
})
submitted := false
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
submitted = true
return nil, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.False(t, submitted)
assert.Equal(t, http.StatusNotImplemented, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"code":"task_protocol_not_available"`)
}
func TestServeTaskPluginProtocolRejectsObservationAdmissionBeforeSubmission(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "admission-limit", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
for _, stream := range []bool{false, true} {
t.Run(map[bool]string{false: "non-stream", true: "stream"}[stream], func(t *testing.T) {
c, recorder := newPluginProtocolTestContext(stream, stream)
submitted := false
deps := pluginProtocolTestDeps()
deps.admissions = newPluginProtocolObservationLimiter(pluginProtocolObservationLimits{
global: 0,
perPlugin: 1,
perUser: 1,
perToken: 1,
})
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
submitted = true
return nil, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.False(t, submitted)
assert.Equal(t, http.StatusTooManyRequests, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"code":"rate_limit_exceeded"`)
assert.NotEqual(t, "text/event-stream", recorder.Header().Get("Content-Type"))
})
}
}
func TestServeTaskPluginProtocolBackgroundNonStreamReturnsPendingWithoutObservation(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "background-create", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("background create called renderEvents"); },
renderFinal: function() { throw new Error("background create called renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
setProtocolRequestBackground(c, true)
loadCalls := 0
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_background", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
loadCalls++
return nil, false, errors.New("observation must not start for background create")
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, 0, loadCalls)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "resp_background", response["id"])
assert.Equal(t, "response", response["object"])
assert.Equal(t, "queued", response["status"])
assert.Equal(t, true, response["background"])
assert.Nil(t, response["completed_at"])
assert.Nil(t, response["error"])
assert.Nil(t, response["usage"])
assert.Empty(t, response["output"])
metadata, ok := response["metadata"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "task_background", metadata["task_id"])
assert.Equal(t, "queued", metadata["task_status"])
assert.Equal(t, "/v1/responses/resp_background", metadata["retrieval_path"])
assert.NotEqual(t, "text/event-stream", recorder.Header().Get("Content-Type"))
}
func TestServeTaskPluginProtocolBackgroundStreamEntersObservation(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "background-stream", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [{type: "output", data: "streamed"}], done: true}; },
renderFinal: function() { throw new Error("stream called renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
setProtocolRequestBackground(c, true)
loadCalls := 0
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_background_stream", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
loadCalls++
return &model.Task{
TaskID: "task_background_stream",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Greater(t, loadCalls, 0)
assert.Equal(t, []string{
"response.created",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
}
func TestRetrieveTaskPluginResponsePendingSkipsRenderFinal(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-pending", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve pending called renderEvents"); },
renderFinal: function() {
console.log("renderFinal called");
return {};
}
}};
`, logsAppender(&logs))
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_pending")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_pending",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
PrivateData: model.TaskPrivateData{
ResponsesBackground: true,
},
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "resp_retrieve_pending", response["id"])
assert.Equal(t, "in_progress", response["status"])
assert.Equal(t, "video-model", response["model"])
assert.Equal(t, true, response["background"])
assert.Nil(t, response["completed_at"])
assert.Empty(t, response["output"])
metadata, ok := response["metadata"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "/v1/responses/resp_retrieve_pending", metadata["retrieval_path"])
}
func TestRetrieveTaskPluginResponseEchoesOriginModelName(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-alias-echo", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("pending retrieve called renderEvents"); },
renderFinal: function() { throw new Error("pending retrieve called renderFinal"); }
}};
`, pluginruntime.Options{})
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_alias")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_alias",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
Properties: model.Properties{OriginModelName: "alias-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "alias-model", response["model"])
}
func TestRetrieveTaskPluginResponseSuccessRendersFinal(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-success", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve success called renderEvents"); },
renderFinal: function() {
console.log("renderFinal called");
return {
output: [{
type: "message",
status: "completed",
role: "assistant",
content: [{type: "output_text", text: "retrieved-final", annotations: [], logprobs: []}]
}]
};
}
}};
`, logsAppender(&logs))
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_success")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_success",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
require.NotEmpty(t, logs)
assert.Contains(t, logs[0], "renderFinal called")
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "completed", response.Status)
assert.Equal(t, "resp_retrieve_success", response.ID)
require.Len(t, response.Output, 1)
require.Len(t, response.Output[0].Content, 1)
assert.Equal(t, "retrieved-final", response.Output[0].Content[0].Text)
}
func TestRetrieveTaskPluginResponseStreamOnlySuccessSynthesizesFromEvents(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-stream-only", `
export const protocols = {openai_responses: {
renderEvents: function() {
console.log("renderEvents called");
return {events: [{type: "output", data: "synthesized-retrieve"}], done: true};
},
renderFinal: function() { throw new Error("stream-only retrieve called renderFinal"); }
}};
`, logsAppender(&logs))
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream"}}}
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
require.NotEmpty(t, logs)
assert.Contains(t, logs[0], "renderEvents called")
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "completed", response.Status)
require.NotEmpty(t, response.Output)
}
func TestRetrieveTaskPluginResponseStreamOnlyPendingAndFailureStayHostEnvelopes(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-stream-envelope", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("envelope retrieve called renderEvents"); },
renderFinal: function() { throw new Error("envelope retrieve called renderFinal"); }
}};
`, logsAppender(&logs))
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream"}}}
t.Run("pending", func(t *testing.T) {
logs = logs[:0]
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream_pending")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream_pending",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "in_progress", response["status"])
assert.Empty(t, response["output"])
})
t.Run("failure", func(t *testing.T) {
logs = logs[:0]
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream_failure")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream_failure",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
Properties: model.Properties{OriginModelName: "video-model"},
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "The task failed.", response.Error.Message)
})
}
func TestRetrieveTaskPluginResponseStreamOnlyRenderErrorUsesFailureEnvelope(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-stream-throw", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve boom"); },
renderFinal: function() { throw new Error("stream-only retrieve called renderFinal"); }
}};
`, pluginruntime.Options{})
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream"}}}
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream_throw")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream_throw",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.NotContains(t, recorder.Body.String(), "retrieve boom")
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task could not be observed.", response.Error.Message)
}
func TestRetrieveTaskPluginResponseFailureUsesFailedEnvelope(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-failure", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve failure called renderEvents"); },
renderFinal: function() {
console.log("renderFinal called");
return {};
}
}};
`, logsAppender(&logs))
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_failure")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_failure",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
Properties: model.Properties{OriginModelName: "video-model"},
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task failed.", response.Error.Message)
}
func TestRetrieveTaskPluginResponseNotFound(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-404", `
export const protocols = {openai_responses: {
renderFinal: function() { return {}; }
}};
`, pluginruntime.Options{})
owned := &model.Task{
TaskID: "task_owned",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
Properties: model.Properties{OriginModelName: "video-model"},
}
tests := []struct {
name string
responseID string
userID int
task *model.Task
exists bool
plugin *pluginruntime.LoadedPlugin
claims []pluginruntime.ProtocolClaim
}{
{name: "bad prefix", responseID: "task_owned", userID: 71, task: owned, exists: true, plugin: pinned.Plugin},
{name: "missing", responseID: "resp_missing", userID: 71, exists: false, plugin: pinned.Plugin},
{name: "other user", responseID: "resp_owned", userID: 99, task: owned, exists: false, plugin: pinned.Plugin},
{name: "no plugin", responseID: "resp_owned", userID: 71, task: owned, exists: true},
{name: "plugin does not claim protocol", responseID: "resp_owned", userID: 71, task: owned, exists: true, plugin: pinned.Plugin, claims: []pluginruntime.ProtocolClaim{{Name: "openai_video"}}},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
if testCase.plugin != nil {
if testCase.claims != nil {
testCase.plugin.Meta.Protocols = testCase.claims
} else {
testCase.plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream", "sync", "background"}}}
}
}
c, recorder := newPluginProtocolRetrieveContext(testCase.responseID)
common.SetContextKey(c, constant.ContextKeyUserId, testCase.userID)
deps := pluginProtocolRetrieveDeps(pinned, testCase.task, testCase.exists, nil)
if testCase.plugin == nil {
deps.resolvePlugin = func(constant.TaskPlatform) (*pluginruntime.LoadedPlugin, *pluginruntime.RoutingGeneration, bool) {
return nil, nil, false
}
}
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusNotFound, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"code":"not_found"`)
assert.Contains(t, recorder.Body.String(), "No response found with id '"+testCase.responseID+"'.")
})
}
}
func TestRespondPluginProtocolSubmissionErrorPassesValidationMessage(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "protocol-validation-detail", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return nil, &dto.TaskError{
Code: "invalid_request",
Message: "model is required",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"message":"model is required"`)
assert.Contains(t, recorder.Body.String(), `"code":"invalid_request_error"`)
assert.NotContains(t, recorder.Body.String(), "Invalid task protocol request")
}
func TestRespondPluginProtocolSubmissionErrorKeepsGenericNonValidation400(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "protocol-generic-400", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return nil, &dto.TaskError{
Code: "task_not_exist",
Message: "task_origin_not_exist",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Invalid task protocol request")
assert.NotContains(t, recorder.Body.String(), "task_origin_not_exist")
}
func compilePluginProtocolTestEndpoint(t *testing.T, key, source string) pluginruntime.PinnedEndpoint {
t.Helper()
return compilePluginProtocolTestEndpointWithOptions(t, key, source, pluginruntime.Options{})
}
func compilePluginProtocolTestEndpointWithOptions(
t *testing.T,
key string,
source string,
options pluginruntime.Options,
) pluginruntime.PinnedEndpoint {
t.Helper()
options.Key = key
options.Version = "1.0.0"
options.Concurrency = 1
engine, err := pluginruntime.Compile(source, options)
require.NoError(t, err)
return pluginruntime.PinnedEndpoint{
Generation: &pluginruntime.RoutingGeneration{Number: 41},
Plugin: &pluginruntime.LoadedPlugin{
Meta: pluginruntime.Meta{
Key: key,
Version: "1.0.0",
Protocols: []pluginruntime.ProtocolClaim{{
Name: "openai_responses",
Supports: []string{"stream", "sync", "background"},
}},
},
Engine: engine,
},
Protocol: "openai_responses",
Operation: pluginruntime.HostProtocolOperation{Name: "create", Methods: []string{http.MethodPost}, Path: "/v1/responses", BodyKinds: []pluginruntime.BodyKind{pluginruntime.BodyJSON}, ModelField: "model"},
Model: "video-model",
}
}
func newPluginProtocolTestContext(stream, requestBodyStream bool) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{}`))
common.SetContextKey(c, constant.ContextKeyUserId, 71)
common.SetContextKey(c, constant.ContextKeyTokenId, 81)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Set("resolved_task_model", "video-model")
c.Set(pluginruntime.ContextKeyProtocolRequest, pluginruntime.ProtocolRequestContext{
RouteRequestContext: pluginruntime.RouteRequestContext{
Path: "/v1/responses",
Method: http.MethodPost,
Params: map[string]string{},
Query: map[string][]string{},
Body: map[string]any{
"kind": "json",
"value": map[string]any{"model": "video-model", "stream": requestBodyStream},
},
RequestBody: map[string]any{
"model": "video-model",
"stream": requestBodyStream,
},
},
Protocol: "openai_responses",
Stream: stream,
})
return c, recorder
}
func pluginProtocolTestDeps() pluginProtocolBridgeDeps {
return pluginProtocolBridgeDeps{
now: func() time.Time { return time.Unix(1_710_000_000, 0) },
admissions: newPluginProtocolObservationLimiter(defaultPluginProtocolObservationLimits),
protocolLimits: relay.DefaultPluginProtocolLimits(),
observationTimeout: time.Hour,
tickInterval: time.Nanosecond,
tickJitter: 0,
heartbeatInterval: time.Hour,
admissionTimeout: time.Second,
}
}
func pluginProtocolTestOutcome(
info *relaycommon.RelayInfo,
pluginKey string,
taskID string,
_ ...any,
) *taskSubmissionOutcome {
return &taskSubmissionOutcome{
Result: &relay.TaskSubmitResult{},
Task: &model.Task{
TaskID: taskID,
Platform: constant.TaskPlatform(pluginKey),
UserId: info.UserId,
Status: model.TaskStatusSubmitted,
CreatedAt: 1_710_000_000,
},
RelayInfo: info,
}
}
func pluginProtocolTestSSEEventTypes(body string) []string {
lines := strings.Split(body, "\n")
events := make([]string, 0)
for _, line := range lines {
if after, ok := strings.CutPrefix(line, "event: "); ok {
events = append(events, after)
}
}
return events
}
func setProtocolRequestBackground(c *gin.Context, background bool) {
request := c.MustGet(pluginruntime.ContextKeyProtocolRequest).(pluginruntime.ProtocolRequestContext)
if body, ok := request.Body.(map[string]any); ok {
if value, ok := body["value"].(map[string]any); ok {
value["background"] = background
}
}
}
func compilePluginProtocolRetrieveEndpoint(t *testing.T, key, source string, options pluginruntime.Options) pluginruntime.PinnedEndpoint {
t.Helper()
pinned := compilePluginProtocolTestEndpointWithOptions(t, key, source, options)
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream", "sync", "background"}}}
return pinned
}
func logsAppender(logs *[]string) pluginruntime.Options {
if logs == nil {
return pluginruntime.Options{}
}
return pluginruntime.Options{
Log: func(message string) { *logs = append(*logs, message) },
}
}
func newPluginProtocolRetrieveContext(responseID string) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "/v1/responses/"+responseID, nil)
c.Params = gin.Params{{Key: "response_id", Value: responseID}}
common.SetContextKey(c, constant.ContextKeyUserId, 71)
common.SetContextKey(c, constant.ContextKeyTokenId, 81)
return c, recorder
}
func pluginProtocolRetrieveDeps(pinned pluginruntime.PinnedEndpoint, task *model.Task, exists bool, err error) pluginProtocolBridgeDeps {
deps := pluginProtocolTestDeps()
deps.getByTaskId = func(userId int, taskId string) (*model.Task, bool, error) {
if !exists {
return nil, false, err
}
if task != nil && (userId != task.UserId || taskId != task.TaskID) {
return nil, false, err
}
return task, task != nil, err
}
deps.resolvePlugin = func(constant.TaskPlatform) (*pluginruntime.LoadedPlugin, *pluginruntime.RoutingGeneration, bool) {
if pinned.Plugin == nil {
return nil, nil, false
}
return pinned.Plugin, pinned.Generation, true
}
return deps
}