mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-01 19:41:57 +00:00
246 lines
9.4 KiB
Go
246 lines
9.4 KiB
Go
package jsplugin
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
responsesDecodeOnly = `export const protocols = {openai_responses: {
|
|
decodeRequest: function(ctx) { return ctx; }
|
|
}};`
|
|
responsesDecodeEvents = `export const protocols = {openai_responses: {
|
|
decodeRequest: function(ctx) { return ctx; },
|
|
renderEvents: function() { return {events: [], state: null, done: false}; }
|
|
}};`
|
|
responsesDecodeFinal = `export const protocols = {openai_responses: {
|
|
decodeRequest: function(ctx) { return ctx; },
|
|
renderFinal: function(ctx, task) { return task; }
|
|
}};`
|
|
responsesDecodeBoth = `export const protocols = {openai_responses: {
|
|
decodeRequest: function(ctx) { return ctx; },
|
|
renderEvents: function() { return {events: [], state: null, done: false}; },
|
|
renderFinal: function(ctx, task) { return task; }
|
|
}};`
|
|
videoProtocolExport = `export const protocols = {openai_video: {
|
|
decodeRequest: function(ctx) { return ctx; },
|
|
render: function(ctx, task) { return task; }
|
|
}};
|
|
export function listArtifacts() { return []; }
|
|
export function buildContentRequest() { return {}; }`
|
|
)
|
|
|
|
func TestProtocolSupportsLoadErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
protocols string
|
|
exports string
|
|
err string
|
|
}{
|
|
{
|
|
name: "bare string",
|
|
protocols: `["openai_responses"]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" must declare supports; replace the bare string with {name: "openai_responses", supports: [...]} choosing from "stream", "sync", "background"`,
|
|
},
|
|
{
|
|
name: "object without supports",
|
|
protocols: `[{name: "openai_responses"}]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" must declare supports; add supports: [...] choosing from "stream", "sync", "background"`,
|
|
},
|
|
{
|
|
name: "supports sync but only renderEvents",
|
|
protocols: `[{name: "openai_responses", supports: ["sync"]}]`,
|
|
exports: responsesDecodeEvents,
|
|
err: `plugin acme protocol "openai_responses" supports "sync" but does not export protocols.openai_responses.renderFinal; implement it or declare supports: ["stream"]`,
|
|
},
|
|
{
|
|
name: "supports sync with only decodeRequest",
|
|
protocols: `[{name: "openai_responses", supports: ["sync"]}]`,
|
|
exports: responsesDecodeOnly,
|
|
err: `plugin acme protocol "openai_responses" supports "sync" but does not export protocols.openai_responses.renderFinal; implement it`,
|
|
},
|
|
{
|
|
name: "supports stream but also exports renderFinal",
|
|
protocols: `[{name: "openai_responses", supports: ["stream"]}]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" exports protocols.openai_responses.renderFinal but no supported mode uses it; add "sync" or "background" to supports or remove the hook`,
|
|
},
|
|
{
|
|
name: "supports sync and background but also exports renderEvents",
|
|
protocols: `[{name: "openai_responses", supports: ["sync", "background"]}]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" exports protocols.openai_responses.renderEvents but no supported mode uses it; add "stream" to supports or remove the hook`,
|
|
},
|
|
{
|
|
name: "empty supports",
|
|
protocols: `[{name: "openai_responses", supports: []}]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" supports must contain at least one of "stream", "sync", "background"`,
|
|
},
|
|
{
|
|
name: "duplicate supports",
|
|
protocols: `[{name: "openai_responses", supports: ["stream", "stream"]}]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" supports must be unique`,
|
|
},
|
|
{
|
|
name: "retrieve is not a mode",
|
|
protocols: `[{name: "openai_responses", supports: ["retrieve"]}]`,
|
|
exports: responsesDecodeBoth,
|
|
err: `plugin acme protocol "openai_responses" has no mode "retrieve"; retrieval of a created response is always available and is never declared`,
|
|
},
|
|
{
|
|
name: "openai_video forbids supports",
|
|
protocols: `[{name: "openai_video", supports: ["stream"]}]`,
|
|
exports: videoProtocolExport,
|
|
err: `plugin acme protocol "openai_video" does not define modes; supports is not allowed`,
|
|
},
|
|
{
|
|
name: "unknown protocol forbids supports",
|
|
protocols: `[{name: "openai_custom", supports: ["stream"]}]`,
|
|
err: `plugin acme protocol "openai_custom" does not define modes; supports is not allowed`,
|
|
},
|
|
}
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
_, err := compileProtocolPlugin(t, "acme", `["model"]`, testCase.protocols, testCase.exports)
|
|
require.ErrorContains(t, err, testCase.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProtocolSupportsHappyPaths(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
models string
|
|
protocols string
|
|
exports string
|
|
wantProtocols []ProtocolClaim
|
|
}{
|
|
{
|
|
name: "stream only with renderEvents",
|
|
models: `["model"]`,
|
|
protocols: `[{name: "openai_responses", supports: ["stream"]}]`,
|
|
exports: responsesDecodeEvents,
|
|
wantProtocols: []ProtocolClaim{
|
|
{Name: "openai_responses", Supports: []string{"stream"}, objectForm: true},
|
|
},
|
|
},
|
|
{
|
|
name: "sync and background with renderFinal",
|
|
models: `["model"]`,
|
|
protocols: `[{name: "openai_responses", supports: ["sync", "background"]}]`,
|
|
exports: responsesDecodeFinal,
|
|
wantProtocols: []ProtocolClaim{
|
|
{Name: "openai_responses", Supports: []string{"sync", "background"}, objectForm: true},
|
|
},
|
|
},
|
|
{
|
|
name: "all modes normalize to table order",
|
|
models: `["model"]`,
|
|
protocols: `[{name: "openai_responses", supports: ["background", "stream", "sync"]}]`,
|
|
exports: responsesDecodeBoth,
|
|
wantProtocols: []ProtocolClaim{
|
|
{Name: "openai_responses", Supports: []string{"stream", "sync", "background"}, objectForm: true},
|
|
},
|
|
},
|
|
{
|
|
name: "openai_video object without supports",
|
|
models: `["gpt-5.5", "gpt-5.6"]`,
|
|
protocols: `[{name: "openai_video", models: ["gpt-5.5"]}]`,
|
|
exports: videoProtocolExport,
|
|
wantProtocols: []ProtocolClaim{
|
|
{Name: "openai_video", Models: []string{"gpt-5.5"}, objectForm: true},
|
|
},
|
|
},
|
|
{
|
|
name: "bare openai_video",
|
|
models: `["model"]`,
|
|
protocols: `["openai_video"]`,
|
|
exports: videoProtocolExport,
|
|
wantProtocols: []ProtocolClaim{
|
|
{Name: "openai_video"},
|
|
},
|
|
},
|
|
}
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
plugin, err := compileProtocolPlugin(t, "acme", testCase.models, testCase.protocols, testCase.exports)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testCase.wantProtocols, plugin.Meta.Protocols)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCloneMetaDeepCopiesSupports(t *testing.T) {
|
|
registry := NewRegistry()
|
|
_, err := registry.Register(protocolPluginSource(
|
|
"acme",
|
|
`["model"]`,
|
|
`[{name: "openai_responses", supports: ["stream", "sync", "background"]}]`,
|
|
responsesDecodeBoth,
|
|
), Options{})
|
|
require.NoError(t, err)
|
|
|
|
snapshot := registry.Snapshot()
|
|
require.Len(t, snapshot.Override, 1)
|
|
require.Len(t, snapshot.Override[0].Protocols, 1)
|
|
snapshot.Override[0].Protocols[0].Supports[0] = "mutated"
|
|
|
|
plugin, ok := registry.Get("acme")
|
|
require.True(t, ok)
|
|
assert.Equal(t, []string{"stream", "sync", "background"}, plugin.Meta.Protocols[0].Supports)
|
|
}
|
|
|
|
func TestMetaProtocolSupports(t *testing.T) {
|
|
streamOnly, err := compileProtocolPlugin(t, "acme", `["model"]`,
|
|
`[{name: "openai_responses", supports: ["stream"]}]`, responsesDecodeEvents)
|
|
require.NoError(t, err)
|
|
assert.True(t, streamOnly.Meta.ProtocolSupports("openai_responses", "stream"))
|
|
assert.False(t, streamOnly.Meta.ProtocolSupports("openai_responses", "sync"))
|
|
assert.False(t, streamOnly.Meta.ProtocolSupports("openai_responses", "background"))
|
|
assert.False(t, streamOnly.Meta.ProtocolSupports("openai_responses", "retrieve"))
|
|
assert.False(t, streamOnly.Meta.ProtocolSupports("openai_video", "stream"))
|
|
assert.False(t, streamOnly.Meta.ProtocolSupports("missing", "stream"))
|
|
|
|
video, err := compileProtocolPlugin(t, "acme-video", `["model"]`, `["openai_video"]`, videoProtocolExport)
|
|
require.NoError(t, err)
|
|
assert.False(t, video.Meta.ProtocolSupports("openai_video", "stream"))
|
|
}
|
|
|
|
func TestProtocolClaimMarshalEmitsSupportsInTableOrder(t *testing.T) {
|
|
plugin, err := compileProtocolPlugin(t, "acme", `["model"]`,
|
|
`[{name: "openai_responses", supports: ["background", "sync", "stream"]}]`, responsesDecodeBoth)
|
|
require.NoError(t, err)
|
|
encoded, err := common.Marshal(plugin.Meta.Protocols[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, `{"name":"openai_responses","supports":["stream","sync","background"]}`, string(encoded))
|
|
}
|
|
|
|
func compileProtocolPlugin(t *testing.T, key, models, protocols, exports string) (*LoadedPlugin, error) {
|
|
t.Helper()
|
|
return CompilePlugin(protocolPluginSource(key, models, protocols, exports), Options{})
|
|
}
|
|
|
|
func protocolPluginSource(key, models, protocols, exports string) string {
|
|
return fmt.Sprintf(`
|
|
export const meta = {
|
|
apiVersion: 1, key: %q, name: %q, version: "1.0.0",
|
|
author: {name: "Test"},
|
|
models: %s, fetchMode: "per_task",
|
|
protocols: %s,
|
|
};
|
|
export function buildSubmitRequest() { return {}; }
|
|
export function parseSubmitResponse() { return {}; }
|
|
export function buildQueryRequest() { return {}; }
|
|
export function parseTaskResult() { return {}; }
|
|
%s
|
|
`, key, key, models, protocols, exports)
|
|
}
|