Files
CLIProxyAPI/internal/pluginhost/http_operation_bridge_test.go
Luis Pater 855a722349 feat(pluginhost): add host http operation bridge and lifecycle scoping
- 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
2026-09-24 06:08:51 +08:00

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)
}