Files
new-api/controller/plugin_protocol_test.go
T

1556 lines
58 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, 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 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
}