webctx 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ Source-aware reading also prefers raw JSON, YAML, and OpenAPI files when the URL
30
30
 
31
31
  ## Structured data: discover → inspect → execute
32
32
 
33
- `research` discovers datasets and operations by meaning, website, source, category, or group. `inspect` retrieves exact parameter types, required fields, response shapes, pricing, and copy-ready `execute` arguments. `execute` validates the inputs and credit estimate before calling one or more data operations, and preserves their source-native JSON and pagination.
33
+ `research` discovers datasets and operations by meaning, website, source, category, or group. `inspect` retrieves exact parameter types, required fields, response shapes, pricing, and copy-ready `execute` arguments. `execute` validates inputs and estimated charges before independently querying each operation, preserving native JSON, pagination and partial results when another provider rejects access.
34
34
 
35
35
  ```bash
36
36
  webctx research "podcast conversations about AI agents" --limit 5
@@ -70,6 +70,33 @@ func Run(args []string, stdout, stderr io.Writer) int {
70
70
  _, _ = fmt.Fprintln(stdout, "Usage: webctx map-site <url>")
71
71
  return 1
72
72
  }
73
+ if len(flags) > 0 {
74
+ limit, offset := 0, 0
75
+ if raw := flags["limit"]; raw != "" {
76
+ value, e := strconv.Atoi(raw)
77
+ if e != nil {
78
+ return cliDataError(stderr, dataError("invalid_limit", "--limit must be an integer"))
79
+ }
80
+ limit = value
81
+ }
82
+ if raw := flags["offset"]; raw != "" {
83
+ value, e := strconv.Atoi(raw)
84
+ if e != nil {
85
+ return cliDataError(stderr, dataError("invalid_offset", "--offset must be an integer"))
86
+ }
87
+ offset = value
88
+ }
89
+ result, e := MapSitePage(MapSiteInput{
90
+ URL: input, Limit: limit, Offset: offset, Query: flags["query"],
91
+ PathPrefix: flags["path-prefix"], Language: flags["language"],
92
+ limitProvided: flags["limit"] != "",
93
+ })
94
+ if e != nil {
95
+ return cliDataError(stderr, e)
96
+ }
97
+ writeDataJSON(stdout, result)
98
+ return 0
99
+ }
73
100
  text, err := MapSite(input)
74
101
  if err != nil {
75
102
  _, _ = fmt.Fprintln(stderr, err.Error())
@@ -99,6 +126,7 @@ func Run(args []string, stdout, stderr io.Writer) int {
99
126
  return cliDataError(stderr, dataError("invalid_limit", "--limit requires an integer."))
100
127
  }
101
128
  research.Limit = n
129
+ research.limitProvided = true
102
130
  }
103
131
  if flag := flags["offset"]; flag != "" {
104
132
  n, e := strconv.Atoi(flag)
@@ -170,7 +198,7 @@ func usageText() string {
170
198
  Usage:
171
199
  webctx search <query> [--exclude domain1,domain2] [--keyword phrase]
172
200
  webctx read-link <url> [--question 'what to find']
173
- webctx map-site <url>
201
+ webctx map-site <url> [--query topic] [--limit 30] [--offset 0] [--path-prefix /api] [--language en|all]
174
202
  webctx research [question] [--view sources|groups|operations] [--sources IDs] [--include inputs,output,examples] [--limit N] [--offset N]
175
203
  webctx inspect <operation-or-source-id> [--raw]
176
204
  webctx execute <operation-id> --inputs '{"field":"value"}' [--request-id ID]
@@ -0,0 +1,574 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "net/http"
9
+ "net/url"
10
+ "strconv"
11
+ "strings"
12
+ "testing"
13
+ "time"
14
+ )
15
+
16
+ func TestAuditPerRecordBoundsFromActualOperations(t *testing.T) {
17
+ tests := []struct {
18
+ contract map[string]any
19
+ inputs map[string]any
20
+ credits int
21
+ }{
22
+ {
23
+ map[string]any{"id": "firecrawl-developer-index/search", "creditsCost": 2, "perRecord": true,
24
+ "options": []any{map[string]any{"name": "k", "type": "number", "default": 10}}},
25
+ map[string]any{"query": "Rust async programming", "k": float64(2)}, 4,
26
+ },
27
+ {
28
+ map[string]any{"id": "fullenrich/companies/lookup", "creditsCost": 5, "perRecord": true,
29
+ "response": map[string]any{"paginated": false}, "options": []any{map[string]any{"name": "domain", "type": "string"}}},
30
+ map[string]any{"domain": "firecrawl.dev"}, 5,
31
+ },
32
+ {
33
+ map[string]any{"id": "wikipedia/entries/lookup", "creditsCost": 2, "perRecord": true,
34
+ "options": []any{map[string]any{"name": "ids", "type": "array"}}},
35
+ map[string]any{"ids": []any{"A", "B"}}, 4,
36
+ },
37
+ {
38
+ map[string]any{"id": "fullenrich/contacts/mobile", "creditsCost": 175, "perRecord": true,
39
+ "response": map[string]any{"paginated": true, "about": "One record in results when a mobile number was found, an empty array when not."}},
40
+ map[string]any{"linkedin_url": "https://www.linkedin.com/in/example-test-person/"}, 175,
41
+ },
42
+ {
43
+ map[string]any{"id": "example/search", "creditsCost": 2, "perRecord": true,
44
+ "options": []any{map[string]any{"name": "k", "type": "integer"}, map[string]any{"name": "limit", "type": "integer"}}},
45
+ map[string]any{"limit": 1, "k": 120}, 240,
46
+ },
47
+ }
48
+ for _, tt := range tests {
49
+ got, err := expectedCallCredits(tt.contract, tt.inputs)
50
+ if err != nil || got != tt.credits {
51
+ t.Errorf("%s: got %d (%v), want %d", tt.contract["id"], got, err, tt.credits)
52
+ }
53
+ }
54
+ }
55
+
56
+ func TestFilteredCataloguePaginationBeyondFirstHundred(t *testing.T) {
57
+ previous := http.DefaultClient
58
+ t.Cleanup(func() { http.DefaultClient = previous })
59
+ t.Setenv("FIRECRAWL_API_KEY", "test-filtered-pagination")
60
+ const total = 215
61
+ var upstreamOffsets []int
62
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
63
+ if req.URL.Path != "/v2/scrape" {
64
+ return nil, fmt.Errorf("unexpected endpoint %s", req.URL.Path)
65
+ }
66
+ var body map[string]any
67
+ if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
68
+ return nil, err
69
+ }
70
+ options := mapField(mapField(body["alexandria"])["options"])
71
+ offset, _ := numberField(options["offset"])
72
+ upstreamOffsets = append(upstreamOffsets, int(offset))
73
+ start, end := int(offset), int(offset)+100
74
+ if end > total {
75
+ end = total
76
+ }
77
+ var items []any
78
+ for i := start; i < end; i++ {
79
+ items = append(items, map[string]any{
80
+ "id": fmt.Sprintf("fred-stlouisfed-org/data/series_%03d", i),
81
+ "name": "Inflation series observations", "description": "Retrieve inflation observations",
82
+ "creditsCost": 5, "provider": "fred-stlouisfed-org",
83
+ })
84
+ }
85
+ encoded, _ := json.Marshal(map[string]any{"success": true, "data": map[string]any{
86
+ "alexandria": []any{map[string]any{"provider": "firecrawl", "capability": "find-tools",
87
+ "data": map[string]any{"items": items, "total": total}}}}})
88
+ return testHTTPResponse(req, 200, string(encoded), nil), nil
89
+ })}
90
+ input := ResearchDataInput{Query: "inflation series", Mode: "ranked", Sources: []string{"fred-stlouisfed-org"}, Limit: 10}
91
+ first, err := ResearchData(input)
92
+ if err != nil {
93
+ t.Fatal(err)
94
+ }
95
+ if first["ranking_incomplete"] != true || first["ranking_scope"] != "catalogue_page" {
96
+ t.Fatalf("ranked first page should disclose scope: %+v", first)
97
+ }
98
+ seen := map[string]bool{}
99
+ pages := 0
100
+ next := first
101
+ for {
102
+ pages++
103
+ rows := next["results"].([]map[string]any)
104
+ if len(rows) == 0 {
105
+ t.Fatalf("empty page %d", pages)
106
+ }
107
+ for _, row := range rows {
108
+ id := row["id"].(string)
109
+ if seen[id] {
110
+ t.Fatalf("pagination repeated %s", id)
111
+ }
112
+ seen[id] = true
113
+ }
114
+ cont := mapField(next["next"])
115
+ if cont == nil {
116
+ break
117
+ }
118
+ if pages > 30 {
119
+ t.Fatal("pagination did not terminate")
120
+ }
121
+ args := cont["arguments"].(ResearchDataInput)
122
+ next, err = ResearchData(args)
123
+ if err != nil {
124
+ t.Fatalf("page %d: %v", pages, err)
125
+ }
126
+ }
127
+ if len(seen) != total || pages != 22 {
128
+ t.Fatalf("pagination missed items: got %d of %d over %d pages", len(seen), total, pages)
129
+ }
130
+ hasSecondBatch := false
131
+ for _, offset := range upstreamOffsets {
132
+ if offset == 100 {
133
+ hasSecondBatch = true
134
+ break
135
+ }
136
+ }
137
+ if !hasSecondBatch {
138
+ t.Fatalf("never fetched beyond the first 100 entries: %v", upstreamOffsets)
139
+ }
140
+ }
141
+
142
+ func TestAuditPaginationAdaptersPreserveFilters(t *testing.T) {
143
+ tests := []struct {
144
+ name string
145
+ inputs map[string]any
146
+ data map[string]any
147
+ param string
148
+ want any
149
+ }{
150
+ {"treasury-link", map[string]any{"page": float64(1), "fields": "date,rate"},
151
+ map[string]any{"links": map[string]any{"next": "https://fiscaldata.treasury.gov/api?page[number]=2&page[size]=1"}},
152
+ "page", 2},
153
+ {"clinical-trials", map[string]any{"query": "recruiting diabetes", "page_size": float64(2)},
154
+ map[string]any{"next_page_token": "next-study-page"}, "page_token", "next-study-page"},
155
+ {"semantic-scholar", map[string]any{"query": "CRISPR papers"},
156
+ map[string]any{"continuation": "2@"}, "continuation", "2@"},
157
+ {"fullenrich", map[string]any{"query": "finance firms", "limit": float64(1)},
158
+ map[string]any{"metadata": map[string]any{"search_after": "WzAsNjAyNjE2MCJd"}}, "search_after", "WzAsNjAyNjE2MCJd"},
159
+ }
160
+ for _, tt := range tests {
161
+ t.Run(tt.name, func(t *testing.T) {
162
+ next := continuationForDataResult("example/search", tt.inputs, tt.data)
163
+ if next == nil {
164
+ t.Fatal("missing continuation")
165
+ }
166
+ args := mapField(mapField(next)["arguments"])
167
+ call := mapField(listField(args["calls"])[0])
168
+ parameters := mapField(call["inputs"])
169
+ if parameters[tt.param] != tt.want {
170
+ t.Fatalf("missing next %s=%v: %+v", tt.param, tt.want, parameters)
171
+ }
172
+ for k, v := range tt.inputs {
173
+ if k != tt.param && parameters[k] != v {
174
+ t.Fatalf("lost original filter %s=%v", k, v)
175
+ }
176
+ }
177
+ if tt.inputs[tt.param] == tt.want {
178
+ t.Fatal("test did not advance cursor")
179
+ }
180
+ })
181
+ }
182
+ }
183
+
184
+ func TestAuditSourceMapIsBoundedAndFilterable(t *testing.T) {
185
+ old := http.DefaultClient
186
+ t.Cleanup(func() { http.DefaultClient = old })
187
+ t.Setenv("FIRECRAWL_API_KEY", "test-map")
188
+ var links []any
189
+ for i := 0; i < 2803; i++ {
190
+ path := "https://docs.firecrawl.dev/features/page/" + strconv.Itoa(i)
191
+ if i%4 == 0 {
192
+ path = "https://docs.firecrawl.dev/ja/features/page/" + strconv.Itoa(i)
193
+ }
194
+ links = append(links, map[string]any{"url": path, "title": "Guide " + strconv.Itoa(i), "description": "Overview of API features"})
195
+ }
196
+ mock := map[string]any{"success": true, "links": links}
197
+ body, _ := json.Marshal(mock)
198
+ calls := 0
199
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
200
+ calls++
201
+ if req.URL.Path != "/v2/map" {
202
+ return nil, errors.New("unexpected path")
203
+ }
204
+ return testHTTPResponse(req, 200, string(body), nil), nil
205
+ })}
206
+ base := MapSiteInput{URL: "https://docs.firecrawl.dev"}
207
+ first, err := MapSitePage(base)
208
+ if err != nil {
209
+ t.Fatal(err)
210
+ }
211
+ if first["count"] != 30 || first["total_discovered"] != 2803 || first["language"] != "en" {
212
+ t.Fatalf("unbounded mapping: %+v", first)
213
+ }
214
+ next := mapField(first["next"])
215
+ if next["tool"] != "map_site" {
216
+ t.Fatalf("missing paged navigation: %+v", next)
217
+ }
218
+ args := next["arguments"].(MapSiteInput)
219
+ if args.Offset != 30 {
220
+ t.Fatalf("wrong offset: %d", args.Offset)
221
+ }
222
+ second, err := MapSitePage(args)
223
+ if err != nil {
224
+ t.Fatal(err)
225
+ }
226
+ page1 := first["links"].([]map[string]any)
227
+ page2 := second["links"].([]map[string]any)
228
+ if page1[0]["url"] == page2[0]["url"] {
229
+ t.Fatal("next site-map page repeats the first")
230
+ }
231
+ for _, v := range page1 {
232
+ if strings.Contains(v["url"].(string), "/ja/") {
233
+ t.Fatal("localized page leaked into English default")
234
+ }
235
+ }
236
+ jp, err := MapSitePage(MapSiteInput{URL: base.URL, Language: "ja", Limit: 10, PathPrefix: "/ja/features/"})
237
+ if err != nil || jp["count"] != 10 {
238
+ t.Fatalf("Japanese/path filtered page failed: %+v %v", jp, err)
239
+ }
240
+ _, err = MapSitePage(MapSiteInput{URL: base.URL, Limit: 101})
241
+ if err == nil {
242
+ t.Fatal("must refuse unbounded client page")
243
+ }
244
+ oldText, err := MapSite(base.URL)
245
+ if err != nil || len(oldText) > 13500 {
246
+ t.Fatalf("URL-only map output too large (%d bytes): %v", len(oldText), err)
247
+ }
248
+ if calls != 4 {
249
+ t.Logf("Site-map mock calls (cache disabled for test credential): %d", calls)
250
+ }
251
+ }
252
+
253
+ func TestAuditTrueSchemaAlternativesAndExamples(t *testing.T) {
254
+ contract := map[string]any{
255
+ "id": "fiscal-ai/financials/income-statement",
256
+ "options": []any{
257
+ map[string]any{"name": "fscl", "type": "string", "example": "companyFiscalIdentifier"},
258
+ map[string]any{"name": "companyKey", "type": "string"},
259
+ map[string]any{"name": "ticker", "type": "string"},
260
+ },
261
+ "requiresOneOf": []any{[]any{"fscl", "companyKey", "ticker"}},
262
+ }
263
+ template, required := nativeInputTemplate(contract)
264
+ if len(required) > 0 || template["fscl"] != nil {
265
+ t.Fatalf("one-of group made fscl falsely required: %+v %v", template, required)
266
+ }
267
+ example := mapField(listField(contractExamples(contract))[0])
268
+ if !example["ready_to_execute"].(bool) || mapField(example["inputs"])["companyKey"] != "NASDAQ_AAPL" {
269
+ t.Fatalf("expected verified source-compatible alternative: %+v", example)
270
+ }
271
+ if err := validateDataInputs(contract, map[string]any{"companyKey": "NASDAQ_AAPL"}); err != nil {
272
+ t.Fatalf("alternative company key should validate: %v", err)
273
+ }
274
+ emptyLookup := map[string]any{"id": "clinicaltrials-gov/studies/get", "options": []any{map[string]any{"name": "nct_id", "type": "string"}}}
275
+ lookupExample := mapField(listField(contractExamples(emptyLookup))[0])
276
+ if lookupExample["ready_to_execute"] == true || lookupExample["next"] != nil {
277
+ t.Fatalf("no-identifier lookup must not advertise runnable example: %+v", lookupExample)
278
+ }
279
+ expensive := map[string]any{"id": "fullenrich/contacts/mobile", "creditsCost": 175, "options": []any{
280
+ map[string]any{"name": "linkedin_url", "type": "string", "required": true, "example": "https://example.com"},
281
+ }}
282
+ mobile := mapField(listField(contractExamples(expensive))[0])
283
+ if mobile["ready_to_execute"] == true || mobile["next"] != nil {
284
+ t.Fatalf("a fake LinkedIn profile must not be executable: %+v", mobile)
285
+ }
286
+ }
287
+
288
+ func TestAuditDirectRetrievalBeatsAncillarySearchOperations(t *testing.T) {
289
+ cases := []struct {
290
+ query, direct, ancillary string
291
+ }{
292
+ {"Historical US inflation from FRED", "fred-stlouisfed-org/economic-data/series_observations", "fred-stlouisfed-org/economic-data/releases"},
293
+ {"Recruiting diabetes clinical trials", "clinicaltrials-gov/studies/search", "clinicaltrials-gov/studies/suggest"},
294
+ {"Podcast episodes about AI agents", "particle/podcasts/episodes/search", "particle/podcasts/shows/directory"},
295
+ {"Scientific papers about CRISPR", "semantic-scholar/papers/search", "semantic-scholar/papers/related"},
296
+ }
297
+ for _, tt := range cases {
298
+ direct := map[string]any{"id": tt.direct, "similarity": 0.45, "name": "Search source records"}
299
+ side := map[string]any{"id": tt.ancillary, "similarity": 0.55, "name": "Related source operations"}
300
+ if intentRelevance(direct, tt.query) <= intentRelevance(side, tt.query) {
301
+ t.Fatalf("ancillary operation beat direct retrieval for %q: %v <= %v", tt.query, intentRelevance(direct, tt.query), intentRelevance(side, tt.query))
302
+ }
303
+ }
304
+ }
305
+
306
+ func TestAuditTypedConstraintsAndNullableOutput(t *testing.T) {
307
+ c := map[string]any{"id": "example/list", "options": []any{
308
+ map[string]any{"name": "limit", "type": "number", "min": 1, "max": 100},
309
+ map[string]any{"name": "query", "type": "string"},
310
+ }, "response": map[string]any{"fields": []any{
311
+ map[string]any{"name": "value", "type": "number", "about": "null when data is unavailable"},
312
+ }}}
313
+ fields := normalizedDataInputFields(c)
314
+ if fields[0]["type"] != "integer" || normalizedDataOutputFields(c)[0]["nullable"] != true {
315
+ t.Fatalf("lost typed metadata %+v %+v", fields, normalizedDataOutputFields(c))
316
+ }
317
+ if err := validateDataInputs(c, map[string]any{"limit": float64(1.5)}); err == nil {
318
+ t.Fatal("fractional page size accepted even though it must be an integer")
319
+ }
320
+ }
321
+
322
+ func TestAuditURLAndLicenseAnswers(t *testing.T) {
323
+ repo := "https://github.com/firecrawl/firecrawl"
324
+ body := "---\nrepository: firecrawl/firecrawl\nlicense: \"AGPL-3.0\"\n---\n\n" +
325
+ "[License](https://github.com/firecrawl/firecrawl/blob/main/LICENSE)\n" + strings.Repeat("Unrelated markdown line\n", 300)
326
+ answer, err := readLinkFocused(repo, "What license does this public repository use? Quote the specific source URL.", func(string) (string, error) { return body, nil })
327
+ if err != nil || !strings.Contains(answer, "**AGPL-3.0**") ||
328
+ !strings.Contains(answer, repo+"/blob/main/LICENSE") || len(answer) > 650 {
329
+ t.Fatalf("focused GitHub license answer poor: %s / %v", answer, err)
330
+ }
331
+ }
332
+
333
+ func TestAuditRateLimitReturnsMachineReadableReset(t *testing.T) {
334
+ previous := http.DefaultClient
335
+ t.Cleanup(func() {
336
+ http.DefaultClient = previous
337
+ freeDataCooldown.Lock()
338
+ freeDataCooldown.until = time.Time{}
339
+ freeDataCooldown.Unlock()
340
+ })
341
+ t.Setenv("FIRECRAWL_API_KEY", "test-ratelimit")
342
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
343
+ return testHTTPResponse(req, 429, `{"success":false,"error":"Rate limit exceeded. Upgrade your plan."}`,
344
+ map[string]string{"Retry-After": "31"}), nil
345
+ })}
346
+ ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
347
+ defer cancel()
348
+ _, err := doDataAPI(ctx, "/search", map[string]any{"query": "test"}, "")
349
+ e, ok := err.(*DataError)
350
+ if !ok || e.Code != "rate_limited" || e.RetryAfterSeconds != 31 || e.RetryAt == "" || !e.Retryable {
351
+ t.Fatalf("rate limit not actionable: %+v", err)
352
+ }
353
+ _, err = doDataAPI(ctx, "/search", map[string]any{"query": "test"}, "")
354
+ if e, ok = err.(*DataError); !ok || e.Code != "rate_limited" || e.RetryAfterSeconds < 20 {
355
+ t.Fatalf("worker ignored cooldown: %+v", err)
356
+ }
357
+ }
358
+
359
+ func TestAuditUnknownOperationErrorIsPrecise(t *testing.T) {
360
+ installDataTestTransport(t)
361
+ _, err := InspectData(InspectDataInput{ID: "particle/never-existing/operation"})
362
+ e, ok := err.(*DataError)
363
+ if !ok || e.Code != "operation_not_found" {
364
+ t.Fatalf("wrong unknown-op error: %+v", err)
365
+ }
366
+ }
367
+
368
+ func TestAuditCatalogueContinuationAndFallbackExamples(t *testing.T) {
369
+ installDataTestTransport(t)
370
+ id := "particle/podcasts/episodes/search"
371
+ page, err := ResearchData(ResearchDataInput{Mode: "catalogue", View: "operations",
372
+ Operations: []string{id}, Include: []string{"inputs", "output", "examples"}, Limit: 1})
373
+ if err != nil {
374
+ t.Fatal(err)
375
+ }
376
+ items := page["results"].([]map[string]any)
377
+ if len(items) != 1 || mapField(items[0]["next"])["tool"] != "inspect" {
378
+ t.Fatalf("exact operation should have inspect continuation: %+v", page)
379
+ }
380
+ if len(listField(items[0]["examples"])) == 0 {
381
+ t.Fatalf("research include examples must generate usable fallback: %+v", items[0])
382
+ }
383
+ source, err := InspectData(InspectDataInput{ID: "particle"})
384
+ if err != nil {
385
+ t.Fatal(err)
386
+ }
387
+ first := source["results"].([]map[string]any)[0]
388
+ if first["inputs"] != nil || first["output"] != nil {
389
+ t.Fatalf("source overview must not expand every schema by default: %+v", first)
390
+ }
391
+ }
392
+
393
+ func TestAuditWorkerIdempotentReplayAndCollision(t *testing.T) {
394
+ mock := installDataTestTransport(t)
395
+ id := "audit-replay-unique-20261010"
396
+ calls := []DataCall{{ID: "particle/podcasts/episodes/search", Inputs: map[string]any{"semantic_search": "AI agents"}}}
397
+ first, err := ExecuteData(ExecuteDataInput{Calls: calls, RequestID: id})
398
+ if err != nil {
399
+ t.Fatal(err)
400
+ }
401
+ again, err := ExecuteData(ExecuteDataInput{Calls: calls, RequestID: id})
402
+ if err != nil {
403
+ t.Fatal(err)
404
+ }
405
+ if again["replay_status"] != "served_from_local_cache" || again["replayed"] != true ||
406
+ again["credits_charged_this_request"] != float64(0) && again["credits_charged_this_request"] != 0 {
407
+ t.Fatalf("replay accounting ambiguous: %+v", again)
408
+ }
409
+ if again["original_credits_used"] != first["credits_used"] {
410
+ t.Fatal("lost original provider credit amount")
411
+ }
412
+ mock.mu.Lock()
413
+ n := mock.executions
414
+ mock.mu.Unlock()
415
+ if n != 1 {
416
+ t.Fatalf("replay caused duplicate upstream execution (%d)", n)
417
+ }
418
+ changed := []DataCall{{ID: calls[0].ID, Inputs: map[string]any{"semantic_search": "Completely different"}}}
419
+ _, err = ExecuteData(ExecuteDataInput{Calls: changed, RequestID: id})
420
+ e, ok := err.(*DataError)
421
+ if !ok || e.Code != "duplicate_request" || mapField(e.Details)["retry"] != nil {
422
+ t.Fatalf("conflicting ID returned unsafe recovery: %+v", err)
423
+ }
424
+ }
425
+
426
+ func TestConcurrentSameIDHasExactlyOnePaidExecution(t *testing.T) {
427
+ mock := installDataTestTransport(t)
428
+ const workers = 12
429
+ id := "audit-parallel-replay-20261010"
430
+ input := ExecuteDataInput{RequestID: id, Calls: []DataCall{{
431
+ ID: "particle/podcasts/episodes/search",
432
+ Inputs: map[string]any{"semantic_search": "AI agents", "limit": float64(2)},
433
+ }}}
434
+ start := make(chan struct{})
435
+ type result struct {
436
+ output map[string]any
437
+ err error
438
+ }
439
+ out := make(chan result, workers)
440
+ for range workers {
441
+ go func() {
442
+ <-start
443
+ response, err := ExecuteData(input)
444
+ out <- result{response, err}
445
+ }()
446
+ }
447
+ close(start)
448
+ replays := 0
449
+ for range workers {
450
+ r := <-out
451
+ if r.err != nil {
452
+ t.Fatalf("same-ID concurrent request failed: %v", r.err)
453
+ }
454
+ if r.output["replayed"] == true {
455
+ replays++
456
+ }
457
+ }
458
+ mock.mu.Lock()
459
+ calls := mock.executions
460
+ mock.mu.Unlock()
461
+ if calls != 1 || replays != workers-1 {
462
+ t.Fatalf("concurrent same-ID calls executed %d provider requests; %d cached replays; wanted 1 and %d", calls, replays, workers-1)
463
+ }
464
+ }
465
+
466
+ func TestAuditMixedTermsBatchKeepsIndependentResultsAndCredits(t *testing.T) {
467
+ previous := http.DefaultClient
468
+ t.Cleanup(func() { http.DefaultClient = previous })
469
+ t.Setenv("FIRECRAWL_API_KEY", "test-mixed-batch")
470
+ t.Setenv("WEBCTX_ALEXANDRIA_MAX_CREDITS", "200")
471
+ paid := map[string]int{}
472
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
473
+ var payload map[string]any
474
+ _ = json.NewDecoder(req.Body).Decode(&payload)
475
+ if find := mapField(payload["alexandria"]); find != nil {
476
+ providers := listField(mapField(find["options"])["providers"])
477
+ provider := stringField(providers[0])
478
+ item := map[string]any{"id": provider + "/data/search", "provider": provider,
479
+ "capability": "data/search", "name": "Search records", "creditsCost": 5,
480
+ "perRecord": false, "options": []any{map[string]any{"name": "query", "type": "string", "required": true}},
481
+ }
482
+ encoded, _ := json.Marshal(map[string]any{"success": true, "data": map[string]any{"alexandria": []any{
483
+ map[string]any{"provider": "firecrawl", "capability": "find-tools", "data": map[string]any{
484
+ "items": []any{item}, "total": 1,
485
+ }},
486
+ }}})
487
+ return testHTTPResponse(req, 200, string(encoded), nil), nil
488
+ }
489
+ calls := listField(payload["alexandria"])
490
+ if len(calls) != 1 {
491
+ return nil, errors.New("mixed providers must not share an upstream batch transaction")
492
+ }
493
+ provider := stringField(mapField(calls[0])["provider"])
494
+ paid[provider]++
495
+ if req.Header.Get("x-request-id") == "" {
496
+ return nil, errors.New("missing idempotency request id")
497
+ }
498
+ if provider == "particle" {
499
+ return testHTTPResponse(req, 403, `{"success":false,"code":"THIRD_PARTY_DATA_TERMS_REQUIRED","error":"You must accept terms to query Particle","requiresAction":{"url":"https://firecrawl.dev/app/settings?tab=data-sources"}}`, nil), nil
500
+ }
501
+ data := map[string]any{"success": true, "data": map[string]any{"creditsCost": 5, "alexandria": []any{
502
+ map[string]any{"provider": provider, "capability": "data/search", "creditsCost": 5, "alexandriaId": "shared-upstream-id",
503
+ "data": map[string]any{"results": []any{map[string]any{"id": provider + "-row"}}}},
504
+ }}}
505
+ encoded, _ := json.Marshal(data)
506
+ return testHTTPResponse(req, 200, string(encoded), nil), nil
507
+ })}
508
+ calls := []DataCall{
509
+ {ID: "particle/data/search", Inputs: map[string]any{"query": "podcasts"}},
510
+ {ID: "fred/data/search", Inputs: map[string]any{"query": "inflation"}},
511
+ {ID: "sec/data/search", Inputs: map[string]any{"query": "filings"}},
512
+ }
513
+ result, err := ExecuteData(ExecuteDataInput{Calls: calls, RequestID: "audit-partial-replay-20261010"})
514
+ if err != nil {
515
+ t.Fatal(err)
516
+ }
517
+ if result["status"] != "partial_success" || result["failed_operations"] != 1 ||
518
+ result["credits_used"] != float64(10) {
519
+ t.Fatalf("mixed failure hid authorized work or incorrectly counted credits: %+v", result)
520
+ }
521
+ entries := result["results"].([]map[string]any)
522
+ if len(entries) != 3 || entries[0]["ok"] != false || entries[1]["ok"] != true || entries[2]["ok"] != true {
523
+ t.Fatalf("provider isolation failed: %+v", entries)
524
+ }
525
+ failure := entries[0]["error"].(*DataError)
526
+ if failure.Code != "THIRD_PARTY_DATA_TERMS_REQUIRED" || failure.RequiresAction["url"] == nil {
527
+ t.Fatalf("provider terms error lost: %+v", failure)
528
+ }
529
+ for i := 1; i < 3; i++ {
530
+ if entries[i]["operation_index"] != i ||
531
+ entries[i]["receipt_scope"] != "provider_response_or_batch" ||
532
+ entries[i]["provider_request_id"] == entries[1-i+1]["provider_request_id"] && i == 2 {
533
+ t.Fatalf("provenance does not distinguish provider responses: %+v", entries)
534
+ }
535
+ }
536
+ if paid["particle"] != 1 || paid["fred"] != 1 || paid["sec"] != 1 {
537
+ t.Fatalf("provider calls were not independent: %+v", paid)
538
+ }
539
+ replayed, err := ExecuteData(ExecuteDataInput{Calls: calls, RequestID: "audit-partial-replay-20261010"})
540
+ if err != nil || replayed["replayed"] != true || paid["fred"] != 1 || paid["sec"] != 1 ||
541
+ replayed["credits_charged_this_request"] != 0 {
542
+ t.Fatalf("replay of partial batch re-executed charged successes: reply=%+v paid=%+v error=%v", replayed, paid, err)
543
+ }
544
+ }
545
+
546
+ func TestAuditHighPriceOperationCannotBeRecommendedAsRunnable(t *testing.T) {
547
+ t.Setenv("WEBCTX_ALEXANDRIA_MAX_CREDITS", "200")
548
+ original := http.DefaultClient
549
+ t.Cleanup(func() { http.DefaultClient = original })
550
+ t.Setenv("FIRECRAWL_API_KEY", "test-high-price")
551
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
552
+ body := map[string]any{"success": true, "data": map[string]any{"alexandria": []any{
553
+ map[string]any{"provider": "firecrawl", "capability": "find-tools", "creditsCost": 0,
554
+ "data": map[string]any{"items": []any{
555
+ map[string]any{"id": "similarweb/channels/share", "provider": "similarweb", "capability": "channels/share",
556
+ "name": "Channel traffic share", "creditsCost": 550, "options": []any{map[string]any{"name": "domain", "type": "string", "required": true}}},
557
+ }},
558
+ },
559
+ }}}
560
+ encoded, _ := json.Marshal(body)
561
+ return testHTTPResponse(req, 200, string(encoded), nil), nil
562
+ })}
563
+ details, err := InspectData(InspectDataInput{ID: "similarweb/channels/share"})
564
+ if err != nil {
565
+ t.Fatal(err)
566
+ }
567
+ if details["executable"] != false || details["availability"] != "exceeds_server_credit_limit" ||
568
+ details["next"] != nil || details["server_credit_limit"] != 200 {
569
+ t.Fatalf("unaffordable operation falsely recommended: %+v", details)
570
+ }
571
+ }
572
+
573
+ var _ = fmt.Sprint
574
+ var _ = url.URL{}