diff --git a/internal/core/browserreceiver/receiver.go b/internal/core/browserreceiver/receiver.go index 36b52e6..1f391c9 100644 --- a/internal/core/browserreceiver/receiver.go +++ b/internal/core/browserreceiver/receiver.go @@ -47,6 +47,7 @@ type CapturePayload struct { Page CapturePage `json:"page"` Selection *CaptureSelection `json:"selection,omitempty"` Link *CaptureLink `json:"link,omitempty"` + File *CaptureFile `json:"file,omitempty"` Browser *CaptureBrowser `json:"browser,omitempty"` Context interface{} `json:"context,omitempty"` } @@ -66,6 +67,13 @@ type CaptureLink struct { Text string `json:"text"` } +type CaptureFile struct { + Name string `json:"name"` + Mime string `json:"mime"` + Size int64 `json:"size"` + Text string `json:"text"` +} + type CaptureBrowser struct { Name string `json:"name"` } @@ -209,7 +217,7 @@ func (p CapturePayload) Validate() error { if strings.TrimSpace(p.CapturedAt) == "" { return fmt.Errorf("capturedAt is required") } - if p.Kind != "page" && p.Kind != "selection" && p.Kind != "link" { + if p.Kind != "page" && p.Kind != "selection" && p.Kind != "link" && p.Kind != "file" { return fmt.Errorf("unsupported kind") } if strings.TrimSpace(p.Page.URL) == "" { @@ -221,6 +229,12 @@ func (p CapturePayload) Validate() error { if p.Kind == "link" && (p.Link == nil || strings.TrimSpace(p.Link.URL) == "") { return fmt.Errorf("link.url is required") } + if p.Kind == "file" && (p.File == nil || strings.TrimSpace(p.File.Name) == "") { + return fmt.Errorf("file.name is required") + } + if p.Kind == "file" && (p.File == nil || p.File.Text == "") { + return fmt.Errorf("file.text is required") + } return nil } @@ -250,6 +264,11 @@ func (p CapturePayload) EventPayload() map[string]interface{} { result["url"] = linkURL result["title"] = strings.TrimSpace(p.Link.Text) result["domain"] = captureDomain(linkURL, "") + case "file": + result["fileName"] = strings.TrimSpace(p.File.Name) + result["fileMime"] = strings.TrimSpace(p.File.Mime) + result["fileSize"] = p.File.Size + result["fileText"] = p.File.Text } return result } diff --git a/internal/core/browserreceiver/receiver_test.go b/internal/core/browserreceiver/receiver_test.go index 27e658e..453437c 100644 --- a/internal/core/browserreceiver/receiver_test.go +++ b/internal/core/browserreceiver/receiver_test.go @@ -85,6 +85,69 @@ func TestReceiverAcceptsSelectionCaptureAndPublishesEvent(t *testing.T) { } } +func TestReceiverAcceptsFileCaptureAndPublishesEvent(t *testing.T) { + bus := events.NewBus() + received := make(chan events.Event, 1) + bus.Subscribe("browser.capture.file", func(event events.Event) { + received <- event + }) + + receiver := New(bus) + body := `{ + "schemaVersion": 1, + "captureId": "capture-file", + "capturedAt": "2026-06-29T02:00:00.000Z", + "source": "verstak-browser-extension", + "kind": "file", + "page": { + "url": "https://example.com/files", + "title": "Example Files", + "domain": "example.com" + }, + "file": { + "name": "notes.txt", + "mime": "text/plain", + "size": 11, + "text": "hello file" + }, + "browser": { + "name": "Firefox" + } + }` + + req := httptest.NewRequest(http.MethodPost, "/api/browser-inbox/v1/captures", bytes.NewBufferString(body)) + rec := httptest.NewRecorder() + + receiver.ServeHTTP(rec, req) + + if rec.Code != http.StatusAccepted { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusAccepted, rec.Body.String()) + } + event := <-received + if event.Name != "browser.capture.file" { + t.Fatalf("event name = %q, want browser.capture.file", event.Name) + } + payload, ok := event.Payload.(map[string]interface{}) + if !ok { + t.Fatalf("event payload type = %T, want map[string]interface{}", event.Payload) + } + if payload["kind"] != "file" { + t.Fatalf("payload kind = %v, want file", payload["kind"]) + } + if payload["fileName"] != "notes.txt" { + t.Fatalf("payload fileName = %v, want notes.txt", payload["fileName"]) + } + if payload["fileMime"] != "text/plain" { + t.Fatalf("payload fileMime = %v, want text/plain", payload["fileMime"]) + } + if payload["fileSize"] != int64(11) { + t.Fatalf("payload fileSize = %v, want 11", payload["fileSize"]) + } + if payload["fileText"] != "hello file" { + t.Fatalf("payload fileText = %v, want hello file", payload["fileText"]) + } +} + func TestReceiverAnnotatesCaptureWithCurrentWorkspace(t *testing.T) { bus := events.NewBus() received := make(chan events.Event, 1)