mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-24 00:24:23 +08:00
- Introduce `hostHTTPOperationBridge` to manage, claim, and cancel host HTTP operations with scoped cleanups - Scope HTTP operations and streams to specific plugin instances to isolate lifecycles and prevent cross-plugin access - Add RPC handlers for `MethodHostHTTPOperationOpen` and `MethodHostHTTPCancel` - Link operation cancellation and resource cleanup to host callback contexts and stream termination Closes: #6085
96 lines
3.1 KiB
Go
96 lines
3.1 KiB
Go
package pluginhost
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHostHTTPOperationParentCancellationCleansUp(t *testing.T) {
|
|
bridge := newHostHTTPOperationBridge()
|
|
parent, cancelParent := context.WithCancel(context.Background())
|
|
operationID, entry, opened := bridge.open("plugin", nil, "", parent)
|
|
if !opened {
|
|
t.Fatal("failed to open operation")
|
|
}
|
|
cleanupDone := make(chan struct{})
|
|
if !bridge.setCleanup("plugin", operationID, entry, func() { close(cleanupDone) }) {
|
|
t.Fatal("failed to attach cleanup")
|
|
}
|
|
|
|
cancelParent()
|
|
select {
|
|
case <-cleanupDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("parent cancellation did not clean up the operation")
|
|
}
|
|
if _, claimed := bridge.claim("plugin", nil, operationID, ""); claimed {
|
|
t.Fatal("operation remained registered after parent cancellation")
|
|
}
|
|
}
|
|
|
|
func TestHostHTTPOperationCancelIsPluginScoped(t *testing.T) {
|
|
bridge := newHostHTTPOperationBridge()
|
|
operationID, entry, opened := bridge.open("plugin-a", nil, "", context.Background())
|
|
if !opened {
|
|
t.Fatal("failed to open operation")
|
|
}
|
|
bridge.cancel("plugin-b", nil, operationID)
|
|
if entry.ctx.Err() != nil {
|
|
t.Fatalf("foreign plugin canceled operation: %v", entry.ctx.Err())
|
|
}
|
|
bridge.cancel("plugin-a", nil, operationID)
|
|
if entry.ctx.Err() != context.Canceled {
|
|
t.Fatalf("operation context error = %v, want context.Canceled", entry.ctx.Err())
|
|
}
|
|
}
|
|
|
|
func TestHostHTTPOperationFinishDoesNotRemoveReusedID(t *testing.T) {
|
|
bridge := newHostHTTPOperationBridge()
|
|
oldID, oldEntry, openedOld := bridge.open("plugin", nil, "", context.Background())
|
|
if !openedOld {
|
|
t.Fatal("failed to open first operation")
|
|
}
|
|
bridge.cancel("plugin", nil, oldID)
|
|
if oldEntry.ctx.Err() != context.Canceled {
|
|
t.Fatalf("first operation context error = %v, want context.Canceled", oldEntry.ctx.Err())
|
|
}
|
|
newID, newEntry, openedNew := bridge.open("plugin", nil, "", context.Background())
|
|
if !openedNew {
|
|
t.Fatal("failed to open second operation")
|
|
}
|
|
if oldID == newID {
|
|
t.Fatalf("operation ID was reused: %q", oldID)
|
|
}
|
|
bridge.finish("plugin", oldID, oldEntry)
|
|
bridge.cancel("plugin", nil, newID)
|
|
if newEntry.ctx.Err() != context.Canceled {
|
|
t.Fatalf("new operation context error = %v, want context.Canceled", newEntry.ctx.Err())
|
|
}
|
|
}
|
|
|
|
func TestHostHTTPOperationCloseInstanceBlocksLateOpen(t *testing.T) {
|
|
bridge := newHostHTTPOperationBridge()
|
|
oldInstance := &hostCallbackInstance{}
|
|
operationID, entry, opened := bridge.open("plugin", oldInstance, "", context.Background())
|
|
if !opened {
|
|
t.Fatal("failed to open operation")
|
|
}
|
|
|
|
bridge.closeInstance("plugin", oldInstance)
|
|
if entry.ctx.Err() != context.Canceled {
|
|
t.Fatalf("operation context error = %v, want context.Canceled", entry.ctx.Err())
|
|
}
|
|
if _, _, openedLate := bridge.open("plugin", oldInstance, "", context.Background()); openedLate {
|
|
t.Fatal("opened operation for a closed callback instance")
|
|
}
|
|
|
|
newInstance := &hostCallbackInstance{}
|
|
newOperationID, _, openedNew := bridge.open("plugin", newInstance, "", context.Background())
|
|
if !openedNew {
|
|
t.Fatal("new callback instance could not open an operation")
|
|
}
|
|
bridge.cancel("plugin", newInstance, newOperationID)
|
|
bridge.finish("plugin", operationID, entry)
|
|
}
|