Add browser inbox receiver

This commit is contained in:
2026-06-27 18:39:01 +08:00
parent fb68c54409
commit a2791c494f
14 changed files with 734 additions and 32 deletions
+21 -7
View File
@@ -30,6 +30,11 @@ import (
"github.com/verstak/verstak-desktop/internal/shell/debug"
)
var newSyncClient = syncsvc.NewClient
var emitFrontendEvent = runtime.EventsEmit
const pluginEventRuntimeName = "verstak:plugin-event"
// App is the main application struct exposed to the Wails frontend.
type App struct {
ctx context.Context
@@ -974,8 +979,8 @@ func (a *App) PublishPluginEvent(pluginID, eventName string, payload map[string]
return ""
}
// SubscribePluginEvent validates subscribe permission for a bundled frontend plugin.
// Actual bundled event dispatch is handled by the frontend plugin host event bus.
// SubscribePluginEvent validates subscribe permission and bridges backend events
// into the bundled frontend plugin host.
func (a *App) SubscribePluginEvent(pluginID, eventName string) string {
if _, err := a.requirePluginAccess(pluginID, "events.subscribe"); err != nil {
return err.Error()
@@ -983,6 +988,15 @@ func (a *App) SubscribePluginEvent(pluginID, eventName string) string {
if eventName == "" {
return "event name is empty"
}
if a.eventBus != nil {
a.eventBus.Subscribe(eventName, func(event events.Event) {
emitFrontendEvent(a.ctx, pluginEventRuntimeName, map[string]interface{}{
"name": event.Name,
"timestamp": event.Timestamp,
"payload": event.Payload,
})
})
}
return ""
}
@@ -1513,7 +1527,7 @@ func (a *App) syncStatus() (*SyncStatusDTO, error) {
dto.UnpushedOps = len(unpushed)
if deviceToken != "" {
client := syncsvc.NewClient(serverURL, "", "", vaultPath)
client := newSyncClient(serverURL, "", "", vaultPath)
client.DeviceToken = deviceToken
if cfg.Sync.DeviceID != "" {
client.DeviceID = cfg.Sync.DeviceID
@@ -1570,7 +1584,7 @@ func (a *App) syncConfigure(serverURL, username, password string) error {
if hostname == "" {
hostname = "unknown"
}
client := syncsvc.NewClient(serverURL, "", "", vaultPath)
client := newSyncClient(serverURL, "", "", vaultPath)
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-desktop/v2")
if err != nil {
return fmt.Errorf("pair: %w", err)
@@ -1613,7 +1627,7 @@ func (a *App) syncDisconnect() error {
cfg := a.appSettings.Get()
if deviceToken != "" {
client := syncsvc.NewClient(cfg.Sync.ServerURL, "", "", vaultPath)
client := newSyncClient(cfg.Sync.ServerURL, "", "", vaultPath)
client.DeviceToken = deviceToken
_ = client.RevokeCurrent()
}
@@ -1647,7 +1661,7 @@ func (a *App) syncTestConnection(serverURL, username, password string) error {
if vaultPath == "" {
vaultPath = "/tmp"
}
client := syncsvc.NewClient(serverURL, "", "", vaultPath)
client := newSyncClient(serverURL, "", "", vaultPath)
return client.TestAuth(serverURL, username, password)
}
@@ -1703,7 +1717,7 @@ func (a *App) syncNow() (map[string]interface{}, error) {
deviceID = cfg.Sync.DeviceID
}
client := syncsvc.NewClient(serverURL, apiKey, deviceID, vaultPath)
client := newSyncClient(serverURL, apiKey, deviceID, vaultPath)
client.DeviceToken = deviceToken
unpushed, err := a.syncSvc.GetUnpushedOps()
+69 -1
View File
@@ -1,7 +1,9 @@
package api
import (
"context"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"os"
@@ -21,6 +23,20 @@ import (
"github.com/verstak/verstak-desktop/internal/core/workspace"
)
func newLocalHTTPTestServer(t *testing.T, handler http.Handler) *httptest.Server {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen local test server: %v", err)
}
server := httptest.NewUnstartedServer(handler)
server.Listener = listener
server.Start()
return server
}
// newTestApp creates an App with a mocked plugin list for testing.
func newTestApp(tmpRoot string) *App {
return &App{
@@ -738,7 +754,7 @@ func TestSyncNowPushesLocalOpsAndAppliesPulledFileOps(t *testing.T) {
CreatedAt string `json:"created_at"`
}
var pushedDeviceID string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := newLocalHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer device-token" {
http.Error(w, "missing auth", http.StatusUnauthorized)
return
@@ -1291,3 +1307,55 @@ func TestPluginBridgeCapabilitiesCommandsAndEventsAreChecked(t *testing.T) {
t.Fatal("expected command permission/ownership error")
}
}
func TestSubscribePluginEventRegistersBackendEventBridge(t *testing.T) {
app := newBridgeTestApp(t)
emitted := make(chan map[string]interface{}, 1)
originalEmit := emitFrontendEvent
emitFrontendEvent = func(_ context.Context, eventName string, data ...interface{}) {
if eventName != pluginEventRuntimeName {
t.Errorf("eventName = %q, want %q", eventName, pluginEventRuntimeName)
}
if len(data) != 1 {
t.Errorf("data length = %d, want 1", len(data))
return
}
payload, ok := data[0].(map[string]interface{})
if !ok {
t.Errorf("data[0] type = %T, want map[string]interface{}", data[0])
return
}
emitted <- payload
}
t.Cleanup(func() {
emitFrontendEvent = originalEmit
})
if errStr := app.SubscribePluginEvent("bridge.plugin", "browser.capture.page"); errStr != "" {
t.Fatalf("SubscribePluginEvent: %s", errStr)
}
if !app.eventBus.HasSubscribers("browser.capture.page") {
t.Fatal("expected backend event bus subscriber")
}
app.eventBus.Publish(events.Event{
Name: "browser.capture.page",
Timestamp: "2026-06-27T00:00:00.000Z",
Payload: map[string]interface{}{"url": "https://example.com"},
})
event := <-emitted
if event["name"] != "browser.capture.page" {
t.Fatalf("event name = %v, want browser.capture.page", event["name"])
}
if event["timestamp"] != "2026-06-27T00:00:00.000Z" {
t.Fatalf("event timestamp = %v, want documented timestamp", event["timestamp"])
}
payload, ok := event["payload"].(map[string]interface{})
if !ok {
t.Fatalf("event payload type = %T, want map[string]interface{}", event["payload"])
}
if payload["url"] != "https://example.com" {
t.Fatalf("payload url = %v, want https://example.com", payload["url"])
}
}