Ctrl K
ComunidadGuías…

pkg/jsplugin/protocol_supports_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}