package jsplugin import ( "context" "fmt" "net/http" "testing" "time" "github.com/QuantumNous/new-api/constant" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRegistryIndexesNativeRoutesAndProtocolsInOneGeneration(t *testing.T) { registry := NewRegistry() plugin, err := CompilePlugin(routingTestPluginSource( "routing-alpha", 50, `["video-alpha"]`, `routes: [ {method: "POST", path: "/vendor/videos", type: "submit", action: "generate", decode: "decodeVideo", render: "videoCreated"}, {method: "GET", path: "/vendor/videos/:task_id", type: "query", render: "videoStatus"} ], protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, `export const native = { decodeVideo: function(ctx) { return {kind: "submit", model: "video-alpha", requestBody: ctx.body.value}; }, videoCreated: function(ctx, task) { return task; }, videoStatus: function(ctx, task) { return task; } }; export const protocols = {openai_responses: { decodeRequest: function(ctx) { return {kind: "submit", model: ctx.model, requestBody: ctx.body.value}; }, renderEvents: function() { return {events: [], state: null, done: false}; }, renderFinal: function(ctx, task) { return task; } }};`, ), Options{}) require.NoError(t, err) require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{plugin})) generation := registry.Generation() assert.Equal(t, uint64(1), generation.Number) byKey, ok := generation.Get("routing-alpha") require.True(t, ok) assert.Same(t, plugin, byKey) byType, ok := generation.GetByChannelType(50) require.True(t, ok) assert.Same(t, plugin, byType) byModel, ok := generation.GetByModel("video-alpha") require.True(t, ok) assert.Same(t, plugin, byModel) route, ok := generation.LookupDeclaredRoute("get", "/vendor/videos/:id") require.True(t, ok) assert.Equal(t, "routing-alpha", route.Plugin.Meta.Key) assert.Equal(t, "task_id", route.Route.TaskIDParam) endpoint, ok := generation.LookupEndpoint("POST", "/v1/responses", "video-alpha") require.True(t, ok) assert.Equal(t, "openai_responses", endpoint.Protocol) assert.Equal(t, []ProtocolBinding{endpoint}, generation.LookupEndpointCandidates("POST", "/v1/responses", "video-alpha")) } func TestRegistryIndexesSharedEndpointCandidatesForDistinctLegacyProviders(t *testing.T) { registry := NewRegistry() gemini := mustCompileRoutingPlugin(t, "gemini-provider", 24, `["shared-video"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) vertex := mustCompileRoutingPlugin(t, "vertex-provider", 41, `["shared-video"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{vertex, gemini})) candidates := registry.Generation().LookupEndpointCandidates("POST", "/v1/responses", "shared-video") require.Len(t, candidates, 2) assert.Equal(t, "gemini-provider", candidates[0].Plugin.Meta.Key) assert.Equal(t, "vertex-provider", candidates[1].Plugin.Meta.Key) assert.Empty(t, registry.RoutingErrors()) } func TestSupportsRegisteredHostProtocols(t *testing.T) { assert.True(t, SupportsHostProtocol("openai_responses")) assert.True(t, SupportsHostProtocol("openai_video")) assert.False(t, SupportsHostProtocol("plugin_owned_wire")) } func TestLookupHostProtocolOperationExcludesRetrieveWithoutModelField(t *testing.T) { _, _, ok := LookupHostProtocolOperation(http.MethodGet, "/v1/responses/:response_id") assert.False(t, ok) _, _, ok = LookupHostProtocolOperation(http.MethodPost, "/v1/responses") assert.True(t, ok) } func TestPerProtocolModelsNarrowEndpointBindings(t *testing.T) { registry := NewRegistry() plugin := mustCompileRoutingPlugin(t, "narrow-protocol", 50, `["gpt-5.5", "gpt-5.6"]`, `protocols: [{name: "openai_responses", models: ["gpt-5.5"], supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{plugin})) generation := registry.Generation() bound, ok := generation.LookupEndpoint("POST", "/v1/responses", "gpt-5.5") require.True(t, ok) assert.Same(t, plugin, bound.Plugin) _, ok = generation.LookupEndpoint("POST", "/v1/responses", "gpt-5.6") assert.False(t, ok, "model outside the protocol claim must fall through to the Go relay") assert.Empty(t, generation.LookupEndpointCandidates("POST", "/v1/responses", "gpt-5.6")) } func TestPerProtocolModelsAllowDisjointPluginsOnSharedProtocol(t *testing.T) { registry := NewRegistry() left := mustCompileRoutingPlugin(t, "disjoint-left", 0, `["model-a"]`, `protocols: [{name: "openai_responses", models: ["model-a"], supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) right := mustCompileRoutingPlugin(t, "disjoint-right", 0, `["model-b"]`, `protocols: [{name: "openai_responses", models: ["model-b"], supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{left, right})) generation := registry.Generation() assert.Empty(t, registry.RoutingErrors()) boundA, ok := generation.LookupEndpoint("POST", "/v1/responses", "model-a") require.True(t, ok) assert.Same(t, left, boundA.Plugin) boundB, ok := generation.LookupEndpoint("POST", "/v1/responses", "model-b") require.True(t, ok) assert.Same(t, right, boundB.Plugin) } func TestPreflightRoutingConflict(t *testing.T) { first := mustCompileRoutingPlugin(t, "preflight-first", 90, `["model-a"]`, "", "") second := mustCompileRoutingPlugin(t, "preflight-second", 90, `["model-b"]`, "", "") replacement := mustCompileRoutingPlugin(t, "preflight-first", 90, `["model-a-v2"]`, "", "") require.NoError(t, PreflightRoutingConflict(nil, first), "a lone candidate against a nil generation must be admitted") registry := NewRegistry() require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{first})) current := registry.Generation() err := PreflightRoutingConflict(current, second) require.Error(t, err) assert.Contains(t, err.Error(), "preflight-first") assert.Contains(t, err.Error(), "channelType 90 conflicts") require.NoError(t, PreflightRoutingConflict(current, replacement), "same-key re-upload must replace rather than self-conflict") left := mustCompileRoutingPlugin(t, "preflight-left", 0, `["shared-model"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) right := mustCompileRoutingPlugin(t, "preflight-right", 0, `["shared-model"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) protocolRegistry := NewRegistry() require.NoError(t, protocolRegistry.ReplaceOverrides([]*LoadedPlugin{left})) protocolErr := PreflightRoutingConflict(protocolRegistry.Generation(), right) require.Error(t, protocolErr) assert.Contains(t, protocolErr.Error(), "preflight-left") assert.Contains(t, protocolErr.Error(), `model "shared-model" conflicts`) } func TestPerProtocolModelsStillConflictOnOverlap(t *testing.T) { registry := NewRegistry() first := mustCompileRoutingPlugin(t, "overlap-first", 0, `["shared-model"]`, `protocols: [{name: "openai_responses", models: ["shared-model"], supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) second := mustCompileRoutingPlugin(t, "overlap-second", 0, `["shared-model"]`, `protocols: [{name: "openai_responses", models: ["shared-model"], supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")) require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{first, second})) routingErrors := registry.RoutingErrors() require.Len(t, routingErrors, 1, "one of two overlapping claims must be rejected from the generation") for _, message := range routingErrors { assert.Contains(t, message, "conflicts") } binding, ok := registry.Generation().LookupEndpoint("POST", "/v1/responses", "shared-model") require.True(t, ok) candidates := registry.Generation().LookupEndpointCandidates("POST", "/v1/responses", "shared-model") assert.Len(t, candidates, 1) assert.NotNil(t, binding.Plugin) } func TestProtocolClaimDecodeAndValidation(t *testing.T) { tests := []struct { name string models string metaFields string exports string errContains string wantProtocols []ProtocolClaim }{ { name: "string and object entries mix", models: `["gpt-5.5", "gpt-5.6"]`, metaFields: `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}, {name: "openai_video", models: ["gpt-5.5"]}],`, exports: `export const protocols = { openai_responses: { decodeRequest: function(ctx) { return ctx; }, renderEvents: function() { return {events: [], state: null, done: false}; }, renderFinal: function(ctx, task) { return task; } }, openai_video: { decodeRequest: function(ctx) { return ctx; }, render: function(ctx, task) { return task; } } }; export function listArtifacts() { return []; } export function buildContentRequest() { return {}; }`, wantProtocols: []ProtocolClaim{ {Name: "openai_responses", Supports: []string{"stream", "sync", "background"}, objectForm: true}, {Name: "openai_video", Models: []string{"gpt-5.5"}, objectForm: true}, }, }, { name: "absent protocols is fine", models: `["gpt-5.5"]`, wantProtocols: []ProtocolClaim{}, }, { name: "object with unknown field", models: `["gpt-5.5"]`, metaFields: `protocols: [{name: "openai_responses", models: ["gpt-5.5"], extra: true}],`, errContains: `unknown field "extra"`, }, { name: "model outside meta models", models: `["gpt-5.5"]`, metaFields: `protocols: [{name: "openai_responses", models: ["gpt-9.9"], supports: ["stream", "sync", "background"]}],`, errContains: "is not declared in plugin meta models", }, { name: "duplicate models in claim", models: `["gpt-5.5"]`, metaFields: `protocols: [{name: "openai_responses", models: ["gpt-5.5", "gpt-5.5"], supports: ["stream", "sync", "background"]}],`, errContains: "models must be unique", }, { name: "blank model in claim", models: `["gpt-5.5"]`, metaFields: `protocols: [{name: "openai_responses", models: [" "], supports: ["stream", "sync", "background"]}],`, errContains: "non-empty canonical names", }, { name: "empty models rejected", models: `["gpt-5.5"]`, metaFields: `protocols: [{name: "openai_responses", models: []}],`, errContains: "models must contain at least one model", }, { name: "explicit null protocols", models: `["gpt-5.5"]`, metaFields: `protocols: null,`, errContains: "must be an array", }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { exports := testCase.exports if exports == "" && testCase.errContains != "" { exports = routingProtocolExport("openai_responses") } plugin, err := CompilePlugin( routingTestPluginSource("claim-decode", 0, testCase.models, testCase.metaFields, exports), Options{}, ) if testCase.errContains != "" { require.ErrorContains(t, err, testCase.errContains) return } require.NoError(t, err) require.Equal(t, testCase.wantProtocols, plugin.Meta.Protocols) }) } } func TestRouteModelsDecodeAndValidation(t *testing.T) { routeExports := `export const native = { decodeJob: function(ctx) { return {kind: "submit", model: "gpt-5.5", requestBody: ctx.body.value}; }, jobCreated: function(ctx, task) { return task; }, jobStatus: function(ctx, task) { return task; } };` tests := []struct { name string models string metaFields string errContains string }{ { name: "submit route with models", models: `["gpt-5.5", "gpt-5.6"]`, metaFields: `routes: [{method: "POST", path: "/v1/batch", type: "submit", models: ["gpt-5.5"], decode: "decodeJob", render: "jobCreated"}],`, }, { name: "query route rejects models", models: `["gpt-5.5"]`, metaFields: `routes: [{method: "GET", path: "/v1/batch/:task_id", type: "query", models: ["gpt-5.5"], render: "jobStatus"}],`, errContains: "must not declare models", }, { name: "model outside meta models", models: `["gpt-5.5"]`, metaFields: `routes: [{method: "POST", path: "/v1/batch", type: "submit", models: ["gpt-9.9"], decode: "decodeJob", render: "jobCreated"}],`, errContains: "is not declared in plugin meta models", }, { name: "duplicate models", models: `["gpt-5.5"]`, metaFields: `routes: [{method: "POST", path: "/v1/batch", type: "submit", models: ["gpt-5.5", "gpt-5.5"], decode: "decodeJob", render: "jobCreated"}],`, errContains: "models must be unique", }, { name: "blank model entry", models: `["gpt-5.5"]`, metaFields: `routes: [{method: "POST", path: "/v1/batch", type: "submit", models: [" "], decode: "decodeJob", render: "jobCreated"}],`, errContains: "non-empty canonical names", }, { name: "empty models rejected", models: `["gpt-5.5"]`, metaFields: `routes: [{method: "POST", path: "/v1/batch", type: "submit", models: [], decode: "decodeJob", render: "jobCreated"}],`, errContains: "models must contain at least one model", }, { name: "query route empty models rejected", models: `["gpt-5.5"]`, metaFields: `routes: [{method: "GET", path: "/v1/batch/:task_id", type: "query", models: [], render: "jobStatus"}],`, errContains: "models must contain at least one model", }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { plugin, err := CompilePlugin( routingTestPluginSource("route-models", 0, testCase.models, testCase.metaFields, routeExports), Options{}, ) if testCase.errContains != "" { require.ErrorContains(t, err, testCase.errContains) return } require.NoError(t, err) require.Len(t, plugin.Meta.Routes, 1) assert.Equal(t, []string{"gpt-5.5"}, plugin.Meta.Routes[0].Models) }) } } func TestRouteRequestContextClonesFormAndMultipartValuesPerDecoder(t *testing.T) { tests := []struct { name string body any mutate func(map[string]any) }{ { name: "form fields", body: map[string]any{"kind": "form", "fields": map[string][]string{"prompt": {"original"}}}, mutate: func(value map[string]any) { value["body"].(map[string]any)["fields"].(map[string][]string)["prompt"][0] = "mutated" }, }, { name: "multipart files", body: map[string]any{"kind": "multipart", "files": []map[string]any{{"ref": "request_file:image", "filename": "safe.png"}}}, mutate: func(value map[string]any) { value["body"].(map[string]any)["files"].([]map[string]any)[0]["filename"] = "mutated.png" }, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { request := RouteRequestContext{Body: testCase.body} first := request.JSValue() testCase.mutate(first) second := request.JSValue() assert.NotEqual(t, first, second) }) } } func TestRegistryNoOpMutationsKeepCurrentGeneration(t *testing.T) { registry := NewRegistry() plugin := mustCompileRoutingPlugin(t, "stable-generation", 0, `["model"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{plugin})) current := registry.Generation() require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{plugin})) assert.Same(t, current, registry.Generation()) registry.SetOverrideEnabled(true) assert.Same(t, current, registry.Generation()) require.NoError(t, registry.Unregister("missing")) assert.Same(t, current, registry.Generation()) } func TestAdjacentNodeGenerationsKeepPinnedPluginExecutable(t *testing.T) { compileVersion := func(version string) *LoadedPlugin { source := fmt.Sprintf(` export const meta = { apiVersion: 1, key: "adjacent-node", name: "Adjacent", version: %q, author: {name: "Test"}, models: ["model"], fetchMode: "per_task" }; export function buildSubmitRequest() { return {version: %q}; } export function parseSubmitResponse() { return {}; } export function buildQueryRequest() { return {}; } export function parseTaskResult() { return {}; } `, version, version) plugin, err := CompilePlugin(source, Options{}) require.NoError(t, err) return plugin } nodeA := NewRegistry() nodeB := NewRegistry() nodeAV1 := compileVersion("1.0.0") nodeBV1 := compileVersion("1.0.0") require.NoError(t, nodeA.ReplaceOverrides([]*LoadedPlugin{nodeAV1})) require.NoError(t, nodeB.ReplaceOverrides([]*LoadedPlugin{nodeBV1})) nodeAV2 := compileVersion("2.0.0") require.NoError(t, nodeA.ReplaceOverrides([]*LoadedPlugin{nodeAV2})) assert.Equal(t, nodeB.Generation().Number+1, nodeA.Generation().Number) pinned := PinnedPlugin{Generation: nodeB.Generation(), Plugin: nodeBV1} nodeBV2 := compileVersion("2.0.0") require.NoError(t, nodeB.ReplaceOverrides([]*LoadedPlugin{nodeBV2})) oldResult, err := pinned.Plugin.Engine.Call(context.Background(), "buildSubmitRequest") require.NoError(t, err) oldObject, ok := oldResult.(map[string]any) require.True(t, ok) assert.Equal(t, "1.0.0", oldObject["version"]) current, ok := nodeB.Get("adjacent-node") require.True(t, ok) newResult, err := current.Engine.Call(context.Background(), "buildSubmitRequest") require.NoError(t, err) newObject, ok := newResult.(map[string]any) require.True(t, ok) assert.Equal(t, "2.0.0", newObject["version"]) assert.Equal(t, uint64(1), pinned.Generation.Number) assert.Equal(t, uint64(2), nodeB.Generation().Number) } func TestRegistryReportsPartialAndFailedRebuildOutcomes(t *testing.T) { registry := NewRegistry() alpha := mustCompileRoutingPlugin(t, "outcome-alpha", 601, `["alpha"]`, "", "") beta := mustCompileRoutingPlugin(t, "outcome-beta", 601, `["beta"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{alpha, beta})) partial := registry.LastRebuildOutcome() assert.Equal(t, "partial", partial.Status) assert.Equal(t, registry.Generation().Number, partial.Generation) assert.Empty(t, partial.Error) err := registry.SetGenerationPreparer(func(_, _ *RoutingGeneration) (PreparedRoutingGeneration, error) { return PreparedRoutingGeneration{}, fmt.Errorf("runtime rebuild unavailable") }) require.ErrorContains(t, err, "runtime rebuild unavailable") failed := registry.LastRebuildOutcome() assert.Equal(t, "failed", failed.Status) assert.Equal(t, registry.Generation().Number, failed.Generation) assert.Contains(t, failed.Error, "runtime rebuild unavailable") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{alpha})) success := registry.LastRebuildOutcome() assert.Equal(t, "success", success.Status) assert.Equal(t, registry.Generation().Number, success.Generation) assert.Empty(t, success.Error) } func TestRegistryExcludesConflictingPluginWithoutBlockingGeneration(t *testing.T) { tests := []struct { name string first *LoadedPlugin second *LoadedPlugin expectedError string }{ { name: "legacy channel type", first: mustCompileRoutingPlugin(t, "channel-alpha", 50, `["alpha"]`, "", ""), second: mustCompileRoutingPlugin(t, "channel-beta", 50, `["beta"]`, "", ""), expectedError: "channelType 50 conflicts", }, { name: "overlapping channelTypes entry", first: mustCompileRoutingPlugin(t, "channel-alpha", 0, `["alpha"]`, `channelTypes: [55, 1],`, ""), second: mustCompileRoutingPlugin(t, "channel-beta", 0, `["beta"]`, `channelTypes: [1],`, ""), expectedError: "channelType 1 conflicts", }, { name: "route shape", first: mustCompileRoutingPlugin(t, "route-alpha", 0, `["alpha"]`, `routes: [{method: "GET", path: "/vendor/jobs/:task_id", type: "query", render: "status"}],`, `export const native = {status: function(ctx, task) { return task; }};`), second: mustCompileRoutingPlugin(t, "route-beta", 0, `["beta"]`, `routes: [{method: "GET", path: "/vendor/jobs/:id", type: "query", render: "status", taskIdParam: "id"}],`, `export const native = {status: function(ctx, task) { return task; }};`), expectedError: "route GET /vendor/jobs/:id conflicts", }, { name: "endpoint model ownership", first: mustCompileRoutingPlugin(t, "endpoint-alpha", 0, `["shared-model"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")), second: mustCompileRoutingPlugin(t, "endpoint-beta", 0, `["shared-model"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, routingProtocolExport("openai_responses")), expectedError: `model "shared-model" conflicts`, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { registry := NewRegistry() require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{testCase.first})) before := registry.Generation() err := registry.ReplaceOverrides([]*LoadedPlugin{testCase.first, testCase.second}) require.NoError(t, err) assert.Equal(t, before.Number+1, registry.Generation().Number) _, exists := registry.Get(testCase.second.Meta.Key) assert.False(t, exists) assert.Contains(t, registry.RoutingErrors()[testCase.second.Meta.Key], testCase.expectedError) }) } } func TestRegistryRetainsIncumbentWhenUpdatedPluginConflicts(t *testing.T) { registry := NewRegistry() incumbent := mustCompileRoutingPlugin(t, "incumbent", 70, `["model-v1"]`, "", "") other := mustCompileRoutingPlugin(t, "other", 71, `["other-model"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{incumbent, other})) conflictingUpdate := mustCompileRoutingPlugin(t, "incumbent", 71, `["model-v2"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{conflictingUpdate, other})) active, ok := registry.Get("incumbent") require.True(t, ok) assert.Same(t, incumbent, active) assert.Same(t, conflictingUpdate, registry.OverridePlugins()["incumbent"]) assert.Same(t, incumbent, registry.ActiveOverridePlugins()["incumbent"]) require.Contains(t, registry.RoutingErrors(), "incumbent") fixedUpdate := mustCompileRoutingPlugin(t, "incumbent", 72, `["model-v2"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{fixedUpdate, other})) active, ok = registry.Get("incumbent") require.True(t, ok) assert.Same(t, fixedUpdate, active) assert.NotContains(t, registry.RoutingErrors(), "incumbent") } func TestRegistryAdmitsNewConflictsInDeterministicKeyOrder(t *testing.T) { registry := NewRegistry() alpha := mustCompileRoutingPlugin(t, "alpha", 80, `["alpha-model"]`, "", "") beta := mustCompileRoutingPlugin(t, "beta", 80, `["beta-model"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{beta, alpha})) active, ok := registry.Get("alpha") require.True(t, ok) assert.Same(t, alpha, active) _, betaActive := registry.Get("beta") assert.False(t, betaActive) assert.Contains(t, registry.RoutingErrors()["beta"], "channelType 80 conflicts") } func TestRegistryPublishesHealthyUpdateAlongsideRejectedUpdate(t *testing.T) { registry := NewRegistry() healthyV1 := mustCompileRoutingPlugin(t, "healthy", 81, `["healthy-v1"]`, "", "") offenderV1 := mustCompileRoutingPlugin(t, "offender", 82, `["offender-v1"]`, "", "") owner := mustCompileRoutingPlugin(t, "owner", 83, `["owner"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{healthyV1, offenderV1, owner})) healthyV2 := mustCompileRoutingPlugin(t, "healthy", 84, `["healthy-v2"]`, "", "") offenderV2 := mustCompileRoutingPlugin(t, "offender", 83, `["offender-v2"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{healthyV2, offenderV2, owner})) activeHealthy, ok := registry.Get("healthy") require.True(t, ok) assert.Same(t, healthyV2, activeHealthy) activeOffender, ok := registry.Get("offender") require.True(t, ok) assert.Same(t, offenderV1, activeOffender) assert.Contains(t, registry.RoutingErrors()["offender"], "channelType 83 conflicts") } func TestRegistryAdmitsInterdependentUpdatesAsOneGeneration(t *testing.T) { tests := []struct { name string firstType int secondType int }{ {name: "one update frees the type needed by another", firstType: 102, secondType: 103}, {name: "two plugins swap types", firstType: 102, secondType: 101}, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { registry := NewRegistry() firstV1 := mustCompileRoutingPlugin(t, "dependent-first", 101, `["first-v1"]`, "", "") secondV1 := mustCompileRoutingPlugin(t, "dependent-second", 102, `["second-v1"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{firstV1, secondV1})) firstV2 := mustCompileRoutingPlugin(t, "dependent-first", testCase.firstType, `["first-v2"]`, "", "") secondV2 := mustCompileRoutingPlugin(t, "dependent-second", testCase.secondType, `["second-v2"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{firstV2, secondV2})) activeFirst, ok := registry.Get("dependent-first") require.True(t, ok) assert.Same(t, firstV2, activeFirst) activeSecond, ok := registry.Get("dependent-second") require.True(t, ok) assert.Same(t, secondV2, activeSecond) assert.Empty(t, registry.RoutingErrors()) }) } } func TestRejectedUpdateDoesNotRestoreIncumbentAheadOfHealthyPeer(t *testing.T) { registry := NewRegistry() alphaV1 := mustCompileRoutingPlugin(t, "fallback-alpha", 111, `["alpha-v1"]`, "", "") betaV1 := mustCompileRoutingPlugin(t, "fallback-beta", 112, `["beta-v1"]`, "", "") owner := mustCompileRoutingPlugin(t, "fallback-owner", 113, `["owner"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{alphaV1, betaV1, owner})) alphaV2 := mustCompileRoutingPlugin(t, "fallback-alpha", 113, `["alpha-v2"]`, "", "") betaV2 := mustCompileRoutingPlugin(t, "fallback-beta", 111, `["beta-v2"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{alphaV2, betaV2, owner})) _, alphaActive := registry.Get("fallback-alpha") assert.False(t, alphaActive) activeBeta, ok := registry.Get("fallback-beta") require.True(t, ok) assert.Same(t, betaV2, activeBeta) activeOwner, ok := registry.Get("fallback-owner") require.True(t, ok) assert.Same(t, owner, activeOwner) assert.Contains(t, registry.RoutingErrors()["fallback-alpha"], "channelType 113 conflicts") assert.NotContains(t, registry.RoutingErrors(), "fallback-beta") } func TestRemovingOverrideNeverRetainsRemovedIncumbent(t *testing.T) { registry := NewRegistry() override := mustCompileRoutingPlugin(t, "fallback", 92, `["override"]`, "", "") owner := mustCompileRoutingPlugin(t, "owner", 91, `["owner"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{override, owner})) factory, err := registry.RegisterFactory(routingTestPluginSource("fallback", 91, `["factory"]`, "", ""), Options{}) require.NoError(t, err) require.NoError(t, registry.Unregister("fallback")) _, fallbackActive := registry.Get("fallback") assert.False(t, fallbackActive) ownerActive, ok := registry.Get("owner") require.True(t, ok) assert.Same(t, owner, ownerActive) assert.NotContains(t, registry.OverridePlugins(), "fallback") assert.NotContains(t, registry.ActiveOverridePlugins(), "fallback") assert.Contains(t, registry.RoutingErrors()["fallback"], "channelType 91 conflicts") require.NoError(t, registry.Unregister("owner")) fallbackPlugin, ok := registry.Get("fallback") require.True(t, ok) assert.Same(t, factory, fallbackPlugin) } func TestRejectedNewOverrideRetainsFactoryIncumbent(t *testing.T) { registry := NewRegistry() factorySource := routingTestPluginSource("factory-fallback", 121, `["factory"]`, "", "") factory, err := registry.RegisterFactory(factorySource, Options{}) require.NoError(t, err) owner := mustCompileRoutingPlugin(t, "factory-owner", 122, `["owner"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{owner})) conflictingOverride := mustCompileRoutingPlugin(t, "factory-fallback", 122, `["override"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{conflictingOverride, owner})) active, ok := registry.Get("factory-fallback") require.True(t, ok) assert.Same(t, factory, active) assert.Same(t, conflictingOverride, registry.OverridePlugins()["factory-fallback"]) assert.NotContains(t, registry.ActiveOverridePlugins(), "factory-fallback") assert.Contains(t, registry.RoutingErrors()["factory-fallback"], "channelType 122 conflicts") } func TestDisablingOverridesPublishesFactoryInsteadOfRetainingOverride(t *testing.T) { registry := NewRegistry() factorySource := routingTestPluginSource("switchable", 93, `["factory"]`, "", "") factory, err := registry.RegisterFactory(factorySource, Options{}) require.NoError(t, err) override := mustCompileRoutingPlugin(t, "switchable", 94, `["override"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{override})) registry.SetOverrideEnabled(false) active, ok := registry.Get("switchable") require.True(t, ok) assert.Same(t, factory, active) assert.Same(t, override, registry.OverridePlugins()["switchable"]) assert.Empty(t, registry.ActiveOverridePlugins()) } func TestGenericChannelTypesDoNotCreateLegacyIdentityConflicts(t *testing.T) { for _, channelType := range []int{0, constant.ChannelTypeTaskPlugin} { t.Run(fmt.Sprintf("channel_%d", channelType), func(t *testing.T) { registry := NewRegistry() first := mustCompileRoutingPlugin(t, "generic-alpha", channelType, `["alpha"]`, "", "") second := mustCompileRoutingPlugin(t, "generic-beta", channelType, `["beta"]`, "", "") require.NoError(t, registry.ReplaceOverrides([]*LoadedPlugin{first, second})) _, found := registry.GetByChannelType(channelType) assert.False(t, found) }) } } func TestDeclarativeRouteValidationProtectsNamespacesAndCanonicalSyntax(t *testing.T) { tests := []struct { name string route Route expectedError string }{ { name: "core API", route: Route{Method: "POST", Path: "/api/plugin", Type: RouteTypeSubmit}, expectedError: "reserved namespace /api", }, { name: "generic task management", route: Route{Method: "GET", Path: "/v1/tasks/:task_id", Type: RouteTypeQuery, Render: "native"}, expectedError: "reserved namespace /v1/tasks", }, { name: "SPA root", route: Route{Method: "POST", Path: "/console/jobs", Type: RouteTypeSubmit}, expectedError: "reserved namespace /console", }, { name: "dynamic root can claim reserved subtree", route: Route{Method: "POST", Path: "/:root/jobs", Type: RouteTypeSubmit}, expectedError: "intersects reserved namespace", }, { name: "dynamic v1 namespace can claim task management", route: Route{Method: "POST", Path: "/v1/:namespace", Type: RouteTypeSubmit}, expectedError: "reserved namespace /v1/tasks", }, { name: "root catch-all can claim reserved subtree", route: Route{Method: "POST", Path: "/*rest", Type: RouteTypeSubmit}, expectedError: "intersects reserved namespace", }, { name: "repeated slash", route: Route{Method: "POST", Path: "/vendor//jobs", Type: RouteTypeSubmit}, expectedError: "empty segments", }, { name: "noncanonical method", route: Route{Method: " post ", Path: "/vendor/jobs", Type: RouteTypeSubmit}, expectedError: "canonical uppercase", }, { name: "non-terminal catch-all", route: Route{Method: "POST", Path: "/vendor/*rest/jobs", Type: RouteTypeSubmit}, expectedError: "invalid catch-all", }, { name: "query id mismatch", route: Route{Method: "GET", Path: "/vendor/jobs/:id", Type: RouteTypeQuery, Render: "native"}, expectedError: "must contain :task_id", }, { name: "query declares decoder", route: Route{Method: "GET", Path: "/vendor/jobs/:task_id", Type: RouteTypeQuery, Decode: "decode", Render: "show"}, expectedError: "must not declare decode", }, { name: "submit missing presenter", route: Route{Method: "POST", Path: "/vendor/jobs", Type: RouteTypeSubmit, Decode: "decode"}, expectedError: "must declare decode and render", }, { name: "dynamic missing decoder", route: Route{Method: "POST", Path: "/vendor/query", Type: RouteTypeDynamic, Render: "show"}, expectedError: "must declare decode and render", }, { name: "dynamic missing presenter", route: Route{Method: "POST", Path: "/vendor/query", Type: RouteTypeDynamic, Decode: "decode"}, expectedError: "must declare decode and render", }, { name: "query declares action", route: Route{Method: "GET", Path: "/vendor/jobs/:task_id", Type: RouteTypeQuery, Render: "show", Action: "retrieve"}, expectedError: "must not declare action", }, { name: "submit declares task id parameter", route: Route{Method: "POST", Path: "/vendor/jobs", Type: RouteTypeSubmit, Decode: "decode", Render: "created", TaskIDParam: "task_id"}, expectedError: "must not declare taskIdParam", }, { name: "dynamic declares task id parameter", route: Route{Method: "POST", Path: "/vendor/query", Type: RouteTypeDynamic, Decode: "decode", Render: "show", TaskIDParam: "task_id"}, expectedError: "must not declare taskIdParam", }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { meta := Meta{APIVersion: 1, Key: "validation", Name: "Validation", Version: "1.0.0", Author: AuthorMeta{Name: "Test"}, Models: []string{"model"}, FetchMode: "per_task", Routes: []Route{testCase.route}} require.ErrorContains(t, ValidateV1Meta(meta), testCase.expectedError) }) } } func TestRemovedEndpointsAndProtocolHooksAreValidatedAtCompileTime(t *testing.T) { removed := routingTestPluginSource( "removed-endpoint", 0, `["model"]`, `endpoints: [{method: "POST", path: "/v1/chat/completions", protocol: "chat"}],`, routingProtocolExport("chat"), ) _, err := CompilePlugin(removed, Options{}) require.ErrorContains(t, err, "endpoints is no longer supported") unknown := routingTestPluginSource("unknown-protocol", 0, `["model"]`, `protocols: ["chat"],`, ``) _, err = CompilePlugin(unknown, Options{}) require.ErrorContains(t, err, `protocol "chat" is unknown`) missingHook := routingTestPluginSource( "bad-protocol", 0, `["model"]`, `protocols: ["openai_responses"],`, `export const protocols = {openai_responses: { decodeRequest: function() { return {}; }, renderEvents: function() { return {events: [], done: false}; } }};`, ) _, err = CompilePlugin(missingHook, Options{}) require.ErrorContains(t, err, `plugin bad-protocol protocol "openai_responses" must declare supports; replace the bare string with {name: "openai_responses", supports: [...]} choosing from "stream", "sync", "background"`) for _, removedExport := range []string{"renderers", "renderError", "resolveRequest"} { t.Run("removed export "+removedExport, func(t *testing.T) { source := routingTestPluginSource("removed-export", 0, `["model"]`, "", `export const `+removedExport+` = {};`) _, compileErr := CompilePlugin(source, Options{}) require.ErrorContains(t, compileErr, `export "`+removedExport+`" is no longer supported`) }) } t.Run("removed route renderer", func(t *testing.T) { source := routingTestPluginSource( "removed-route-renderer", 0, `["model"]`, `routes: [{method:"GET",path:"/vendor/:task_id",type:"query",render:"show",renderer:"legacy"}],`, `export const native = {show: function(ctx, task) { return task; }};`, ) _, compileErr := CompilePlugin(source, Options{}) require.ErrorContains(t, compileErr, "field renderer is no longer supported") }) } func TestRegistryRejectsPrototypeInheritedHooks(t *testing.T) { for _, member := range []string{"constructor", "toString", "__proto__"} { t.Run("native "+member, func(t *testing.T) { source := routingTestPluginSource( "inherited-renderer", 0, `["model"]`, fmt.Sprintf(`routes: [{method: "GET", path: "/vendor/jobs/:task_id", type: "query", render: %q}],`, member), `const nativePrototype = { constructor: function(task) { return task; }, toString: function(task) { return task; }, ["__proto__"]: function(task) { return task; }, }; export const native = Object.create(nativePrototype);`, ) _, err := CompilePlugin(source, Options{}) require.ErrorContains(t, err, fmt.Sprintf(`references missing native render %q`, member)) }) } t.Run("protocol object", func(t *testing.T) { source := routingTestPluginSource( "inherited-protocol", 0, `["model"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, `const protocol = { decodeRequest: function(ctx) { return ctx; }, renderEvents: function() { return {events: [], state: null, done: false}; }, renderFinal: function(ctx, task) { return task; }, }; export const protocols = Object.create({openai_responses: protocol});`, ) _, err := CompilePlugin(source, Options{}) require.ErrorContains(t, err, `protocol "openai_responses" is missing hook "decodeRequest"`) }) t.Run("protocol hook", func(t *testing.T) { source := routingTestPluginSource( "inherited-protocol-hook", 0, `["model"]`, `protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}],`, `const protocolPrototype = { decodeRequest: function(ctx) { return ctx; }, renderEvents: function() { return {events: [], state: null, done: false}; }, renderFinal: function(ctx, task) { return task; }, }; export const protocols = {openai_responses: Object.create(protocolPrototype)};`, ) _, err := CompilePlugin(source, Options{}) require.ErrorContains(t, err, `protocol "openai_responses" is missing hook "decodeRequest"`) }) } func TestMetaDecoderRejectsLossyOrUnknownRoutingFields(t *testing.T) { tests := []struct { name string metaFields string expectedError string }{ { name: "fractional channel type", metaFields: `channelTypes: [50.5],`, expectedError: "channelTypes element 1 must be an integer", }, { name: "non-string model", metaFields: `models: ["model", 2],`, expectedError: "models must be an array of strings", }, { name: "unknown route field", metaFields: `routes: [{method: "POST", path: "/vendor/jobs", type: "submit", auth: false}],`, expectedError: `unknown field "auth"`, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { source := fmt.Sprintf(` export const meta = { apiVersion: 1, key: "strict-meta", name: "Strict", version: "1.0.0", author: {name: "Test"}, models: ["model"], fetchMode: "per_task", %s }; export function buildSubmitRequest() { return {}; } export function parseSubmitResponse() { return {}; } export function buildQueryRequest() { return {}; } export function parseTaskResult() { return {}; } export function resolveRequest() { return {kind: "submit", model: "model"}; } `, testCase.metaFields) _, err := CompilePlugin(source, Options{}) require.ErrorContains(t, err, testCase.expectedError) }) } } func TestProtocolHookInspectionIsDeadlineBounded(t *testing.T) { source := ` export const meta = { apiVersion: 1, key: "getter-timeout", name: "Getter", version: "1.0.0", author: {name: "Test"}, models: ["model"], fetchMode: "per_task", protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}], }; export function buildSubmitRequest() { return {}; } export function parseSubmitResponse() { return {}; } export function buildQueryRequest() { return {}; } export function parseTaskResult() { return {}; } export const protocols = { get openai_responses() { while (true) {} } }; ` _, err := CompilePlugin(source, Options{Timeout: 20 * time.Millisecond}) require.ErrorContains(t, err, "inspection interrupted") } func TestReservedNamespaceChecksUseSegmentBoundaries(t *testing.T) { meta := Meta{ APIVersion: 1, Key: "apiary", Name: "Apiary", Version: "1.0.0", Author: AuthorMeta{Name: "Test"}, Models: []string{"model"}, FetchMode: "per_task", Routes: []Route{{Method: "POST", Path: "/apiary/jobs", Type: RouteTypeSubmit, Decode: "decode", Render: "render"}}, } require.NoError(t, ValidateV1Meta(meta)) } func TestResolveRouteActionPrefersResolvedAction(t *testing.T) { route := Route{Action: "manifest-action"} assert.Equal(t, "hook-action", ResolveRouteAction(route, "hook-action")) assert.Equal(t, "manifest-action", ResolveRouteAction(route, "")) } func mustCompileRoutingPlugin(t *testing.T, key string, channelType int, models, metaFields, exports string) *LoadedPlugin { t.Helper() plugin, err := CompilePlugin(routingTestPluginSource(key, channelType, models, metaFields, exports), Options{}) require.NoError(t, err) return plugin } func routingTestPluginSource(key string, channelType int, models, metaFields, exports string) string { channelTypesField := "" if channelType > 0 && channelType != constant.ChannelTypeTaskPlugin { channelTypesField = fmt.Sprintf("channelTypes: [%d],", channelType) } return fmt.Sprintf(` export const meta = { apiVersion: 1, key: %q, name: %q, version: "1.0.0", author: {name: "Test"}, %s models: %s, fetchMode: "per_task", %s }; export function buildSubmitRequest() { return {}; } export function parseSubmitResponse() { return {}; } export function buildQueryRequest() { return {}; } export function parseTaskResult() { return {}; } %s `, key, key, channelTypesField, models, metaFields, exports) } func routingProtocolExport(name string) string { return fmt.Sprintf(`export const protocols = {%s: { decodeRequest: function(ctx) { return ctx; }, renderEvents: function() { return {events: [], state: null, done: false}; }, renderFinal: function(ctx, task) { return task; } }};`, name) }