@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50
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/AUTHORING.md +537 -33
- package/CHANGELOG.md +201 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1178 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +179 -16
- package/dist/config/loader.js +435 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +43 -10
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +9 -4
- package/dist/stealth/profiles.js +160 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1654 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +690 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +202 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/testing/run.ts
CHANGED
|
@@ -3,22 +3,45 @@ import { describe, expect, it } from "bun:test";
|
|
|
3
3
|
import { createProviderCache } from "../runtime/cache.js";
|
|
4
4
|
import { createTestProviderChoiceContext } from "../runtime/choice.js";
|
|
5
5
|
import { createMemoryProviderRuntimeState } from "../runtime/state.js";
|
|
6
|
+
import { createUnsupportedOcrClient } from "../runtime/ocr.js";
|
|
6
7
|
import { createUnsupportedSttClient } from "../runtime/stt.js";
|
|
8
|
+
import {
|
|
9
|
+
createNativeEgressAuthorization,
|
|
10
|
+
NativeNetworkError,
|
|
11
|
+
snapshotNativeConnectInput,
|
|
12
|
+
snapshotNativeGrantInput,
|
|
13
|
+
} from "../runtime/native-network.js";
|
|
7
14
|
import { safeParseSchemaSync } from "../schema.js";
|
|
15
|
+
import { requestPathForFixture } from "../fixture-sanitization.js";
|
|
16
|
+
import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence.js";
|
|
8
17
|
import type {
|
|
9
18
|
AuthMode,
|
|
19
|
+
BrowserPage,
|
|
10
20
|
CredentialContext,
|
|
11
21
|
HttpResponse,
|
|
22
|
+
NativeNetworkConnection,
|
|
12
23
|
ProviderContext,
|
|
13
24
|
ProviderDefinition,
|
|
25
|
+
StealthCookieStoreV1,
|
|
26
|
+
StealthResponse,
|
|
14
27
|
} from "../types.js";
|
|
15
28
|
|
|
16
29
|
// Mirrors CONNECTOR_ID_REGEX in ../define.ts, which defineProvider() enforces.
|
|
17
30
|
// A single lowercase segment (no hyphen) is a valid id, so the trailing group
|
|
18
31
|
// is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
|
|
19
32
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
20
|
-
const VALID_AUTH_MODES = [
|
|
33
|
+
const VALID_AUTH_MODES = [
|
|
34
|
+
"none",
|
|
35
|
+
"platform-managed",
|
|
36
|
+
"credentials",
|
|
37
|
+
"oauth2",
|
|
38
|
+
"oauth2_proxied",
|
|
39
|
+
] as const;
|
|
21
40
|
const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
|
|
41
|
+
const snapshotCaptureStates = new WeakMap<
|
|
42
|
+
ProviderContext,
|
|
43
|
+
{ assertConsumed(): void; consumed(): number }
|
|
44
|
+
>();
|
|
22
45
|
|
|
23
46
|
export interface StandardTestsManifest {
|
|
24
47
|
id?: string;
|
|
@@ -44,6 +67,40 @@ export interface StandardTestsOptions {
|
|
|
44
67
|
validateAuthMode?: boolean;
|
|
45
68
|
/** Override inferred __fixtures__ directory for tests generated outside providers/<id>. */
|
|
46
69
|
fixtureDir?: string;
|
|
70
|
+
/** Opt in to real-handler E2E with strict, offline canned upstream responses. */
|
|
71
|
+
upstreamStub?: StandardTestsUpstreamStub;
|
|
72
|
+
/** Require a committed snapshot; missing files fail unless --update-snapshots is passed. */
|
|
73
|
+
requireSnapshot?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface StandardTestsUpstreamCall {
|
|
77
|
+
/** Operation whose real handler initiated the call. */
|
|
78
|
+
operationName: string;
|
|
79
|
+
/** ProviderContext transport surface used by the handler. */
|
|
80
|
+
transport: "http" | "stealth" | "browser" | "native";
|
|
81
|
+
method: string;
|
|
82
|
+
url?: string;
|
|
83
|
+
body?: unknown;
|
|
84
|
+
options?: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface StandardTestsUpstreamResponse {
|
|
88
|
+
status?: number;
|
|
89
|
+
headers?: Readonly<Record<string, string>>;
|
|
90
|
+
/** JSON-compatible values are encoded as JSON; strings and bytes are preserved. */
|
|
91
|
+
body?: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type StandardTestsUpstreamStub = (
|
|
95
|
+
call: StandardTestsUpstreamCall,
|
|
96
|
+
) =>
|
|
97
|
+
| Response
|
|
98
|
+
| StandardTestsUpstreamResponse
|
|
99
|
+
| undefined
|
|
100
|
+
| Promise<Response | StandardTestsUpstreamResponse | undefined>;
|
|
101
|
+
|
|
102
|
+
export interface StandardTestsResult {
|
|
103
|
+
warnings: readonly string[];
|
|
47
104
|
}
|
|
48
105
|
|
|
49
106
|
interface FixtureEnvelope {
|
|
@@ -136,11 +193,422 @@ function jsonResponse(data: unknown): HttpResponse {
|
|
|
136
193
|
};
|
|
137
194
|
}
|
|
138
195
|
|
|
196
|
+
interface NormalizedUpstreamResponse {
|
|
197
|
+
status: number;
|
|
198
|
+
headers: Record<string, string>;
|
|
199
|
+
data: unknown;
|
|
200
|
+
text: string;
|
|
201
|
+
bytes: Uint8Array;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function headersToRecord(headers: Headers): Record<string, string> {
|
|
205
|
+
return Object.fromEntries(headers.entries());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function normalizeUpstreamResponse(
|
|
209
|
+
response: Response | StandardTestsUpstreamResponse,
|
|
210
|
+
): Promise<NormalizedUpstreamResponse> {
|
|
211
|
+
if (response instanceof Response) {
|
|
212
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
213
|
+
const text = new TextDecoder().decode(bytes);
|
|
214
|
+
const headers = headersToRecord(response.headers);
|
|
215
|
+
let data: unknown = text;
|
|
216
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
217
|
+
data = text.length > 0 ? JSON.parse(text) : null;
|
|
218
|
+
}
|
|
219
|
+
return { status: response.status, headers, data, text, bytes };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const headers = { ...(response.headers ?? {}) };
|
|
223
|
+
const body = response.body ?? null;
|
|
224
|
+
let bytes: Uint8Array;
|
|
225
|
+
let text: string;
|
|
226
|
+
let data: unknown;
|
|
227
|
+
if (body instanceof Uint8Array) {
|
|
228
|
+
bytes = body.slice(0);
|
|
229
|
+
text = new TextDecoder().decode(bytes);
|
|
230
|
+
data = text;
|
|
231
|
+
} else if (body instanceof ArrayBuffer) {
|
|
232
|
+
bytes = new Uint8Array(body.slice(0));
|
|
233
|
+
text = new TextDecoder().decode(bytes);
|
|
234
|
+
data = text;
|
|
235
|
+
} else if (typeof body === "string") {
|
|
236
|
+
text = body;
|
|
237
|
+
bytes = new TextEncoder().encode(text);
|
|
238
|
+
data = body;
|
|
239
|
+
} else {
|
|
240
|
+
text = JSON.stringify(body);
|
|
241
|
+
bytes = new TextEncoder().encode(text);
|
|
242
|
+
data = body;
|
|
243
|
+
if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
|
|
244
|
+
headers["content-type"] = "application/json";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { status: response.status ?? 200, headers, data, text, bytes };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function toHttpResponse(response: NormalizedUpstreamResponse): HttpResponse {
|
|
251
|
+
return {
|
|
252
|
+
status: response.status,
|
|
253
|
+
ok: response.status >= 200 && response.status < 300,
|
|
254
|
+
headers: response.headers,
|
|
255
|
+
data: response.data,
|
|
256
|
+
json: async <T = unknown>() => JSON.parse(response.text) as T,
|
|
257
|
+
text: async () => response.text,
|
|
258
|
+
arrayBuffer: async () => response.bytes.slice(0).buffer,
|
|
259
|
+
bytes: async () => response.bytes.slice(0),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const emptyCookieJar = {
|
|
264
|
+
get: () => undefined,
|
|
265
|
+
getAll: () => ({}),
|
|
266
|
+
toString: () => "",
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function emptyCookieStore(): StealthCookieStoreV1 {
|
|
270
|
+
return {
|
|
271
|
+
version: 1,
|
|
272
|
+
jar: { cookies: [] } as unknown as StealthCookieStoreV1["jar"],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function toStealthResponse(response: NormalizedUpstreamResponse, url?: string): StealthResponse {
|
|
277
|
+
return {
|
|
278
|
+
status: response.status,
|
|
279
|
+
ok: response.status >= 200 && response.status < 300,
|
|
280
|
+
url,
|
|
281
|
+
redirected: false,
|
|
282
|
+
headers: response.headers,
|
|
283
|
+
rawHeaders: Object.entries(response.headers),
|
|
284
|
+
body: response.text,
|
|
285
|
+
cookies: emptyCookieJar,
|
|
286
|
+
json: async <T>() => JSON.parse(response.text) as T,
|
|
287
|
+
arrayBuffer: async () => response.bytes.slice(0).buffer,
|
|
288
|
+
bytes: async () => response.bytes.slice(0),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function streamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
293
|
+
return new ReadableStream({
|
|
294
|
+
start(controller) {
|
|
295
|
+
controller.enqueue(bytes.slice(0));
|
|
296
|
+
controller.close();
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function* singleBytes(bytes: Uint8Array): AsyncIterable<Uint8Array> {
|
|
302
|
+
yield bytes.slice(0);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function* singleText(textValue: string): AsyncIterable<string> {
|
|
306
|
+
yield textValue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function createUpstreamContext(
|
|
310
|
+
provider: ProviderDefinition,
|
|
311
|
+
operationName: string,
|
|
312
|
+
upstreamStub: StandardTestsUpstreamStub,
|
|
313
|
+
): ProviderContext {
|
|
314
|
+
const credential: CredentialContext = {
|
|
315
|
+
mode: "none",
|
|
316
|
+
get: () => undefined,
|
|
317
|
+
getAll: () => ({}),
|
|
318
|
+
getAccessToken: () => undefined,
|
|
319
|
+
getScopes: () => [],
|
|
320
|
+
};
|
|
321
|
+
const request = { headers: {} };
|
|
322
|
+
const state = createMemoryProviderRuntimeState();
|
|
323
|
+
const nativeEgress = provider.native
|
|
324
|
+
? createNativeEgressAuthorization({ egress: provider.native.network })
|
|
325
|
+
: undefined;
|
|
326
|
+
const requireNativeEgress = () => {
|
|
327
|
+
if (!nativeEgress)
|
|
328
|
+
throw new NativeNetworkError(
|
|
329
|
+
"Native egress authorization is unavailable",
|
|
330
|
+
"native_egress_policy_invalid",
|
|
331
|
+
);
|
|
332
|
+
return nativeEgress;
|
|
333
|
+
};
|
|
334
|
+
const dispatch = async (
|
|
335
|
+
call: Omit<StandardTestsUpstreamCall, "operationName">,
|
|
336
|
+
): Promise<NormalizedUpstreamResponse> => {
|
|
337
|
+
const canned = await upstreamStub({ operationName, ...call });
|
|
338
|
+
if (canned === undefined) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Unmatched upstream call for operation "${operationName}": ${call.transport}.${call.method}${call.url ? ` ${call.url}` : ""}. Add a canned response to upstreamStub; live network passthrough is disabled.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return normalizeUpstreamResponse(canned);
|
|
344
|
+
};
|
|
345
|
+
const httpCall = async (
|
|
346
|
+
method: string,
|
|
347
|
+
url: string,
|
|
348
|
+
body?: unknown,
|
|
349
|
+
options?: unknown,
|
|
350
|
+
): Promise<HttpResponse> =>
|
|
351
|
+
toHttpResponse(await dispatch({ transport: "http", method, url, body, options }));
|
|
352
|
+
const stealthCall = async (url: string, options?: { method?: string; body?: unknown }) =>
|
|
353
|
+
toStealthResponse(
|
|
354
|
+
await dispatch({
|
|
355
|
+
transport: "stealth",
|
|
356
|
+
method: options?.method?.toUpperCase() ?? "GET",
|
|
357
|
+
url,
|
|
358
|
+
body: options?.body,
|
|
359
|
+
options,
|
|
360
|
+
}),
|
|
361
|
+
url,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const createBrowserPage = (): BrowserPage => {
|
|
365
|
+
let currentUrl = "about:blank";
|
|
366
|
+
let currentResponse: NormalizedUpstreamResponse | undefined;
|
|
367
|
+
const browserAction = async (method: string, body?: unknown) => {
|
|
368
|
+
currentResponse = await dispatch({
|
|
369
|
+
transport: "browser",
|
|
370
|
+
method,
|
|
371
|
+
url: currentUrl,
|
|
372
|
+
body,
|
|
373
|
+
});
|
|
374
|
+
return currentResponse;
|
|
375
|
+
};
|
|
376
|
+
const page: BrowserPage = {
|
|
377
|
+
id: `standard-test-${operationName}`,
|
|
378
|
+
url: async () => currentUrl,
|
|
379
|
+
title: async () => currentResponse?.text.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] ?? "",
|
|
380
|
+
userAgent: async () => String((await browserAction("userAgent")).data),
|
|
381
|
+
content: async () => currentResponse?.text ?? "",
|
|
382
|
+
evaluate: async <T>(fn: string | (() => T)) =>
|
|
383
|
+
(await browserAction("evaluate", typeof fn === "string" ? fn : String(fn))).data as T,
|
|
384
|
+
locator: (selector) => ({
|
|
385
|
+
click: async () => {
|
|
386
|
+
await browserAction("locator.click", { selector });
|
|
387
|
+
},
|
|
388
|
+
fill: async (textValue) => {
|
|
389
|
+
await browserAction("locator.fill", { selector, text: textValue });
|
|
390
|
+
},
|
|
391
|
+
textContent: async () => {
|
|
392
|
+
const value = (await browserAction("locator.textContent", { selector })).data;
|
|
393
|
+
return value === null || value === undefined ? null : String(value);
|
|
394
|
+
},
|
|
395
|
+
waitFor: async (options) => {
|
|
396
|
+
await browserAction("locator.waitFor", { selector, options });
|
|
397
|
+
},
|
|
398
|
+
}),
|
|
399
|
+
close: async () => {},
|
|
400
|
+
cookies: async () =>
|
|
401
|
+
(await browserAction("cookies")).data as Awaited<ReturnType<BrowserPage["cookies"]>>,
|
|
402
|
+
fill: async (selector, textValue) => {
|
|
403
|
+
await browserAction("fill", { selector, text: textValue });
|
|
404
|
+
},
|
|
405
|
+
goto: async (url) => {
|
|
406
|
+
currentUrl = url;
|
|
407
|
+
await browserAction("goto");
|
|
408
|
+
},
|
|
409
|
+
screenshot: async (options) =>
|
|
410
|
+
Buffer.from((await browserAction("screenshot", options)).bytes),
|
|
411
|
+
click: async (selector) => {
|
|
412
|
+
await browserAction("click", { selector });
|
|
413
|
+
},
|
|
414
|
+
type: async (selector, textValue) => {
|
|
415
|
+
await browserAction("type", { selector, text: textValue });
|
|
416
|
+
},
|
|
417
|
+
waitForSelector: async (selector, options) => {
|
|
418
|
+
await browserAction("waitForSelector", { selector, options });
|
|
419
|
+
},
|
|
420
|
+
frames: async () => [page],
|
|
421
|
+
withResourcePolicy: async (_policy, run) => run(),
|
|
422
|
+
};
|
|
423
|
+
return page;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const createStealthSession = (): ReturnType<ProviderContext["stealth"]["createSession"]> => ({
|
|
427
|
+
fetch: stealthCall,
|
|
428
|
+
cookies: {
|
|
429
|
+
...emptyCookieJar,
|
|
430
|
+
has: () => false,
|
|
431
|
+
setFromCookieStrings: () => {},
|
|
432
|
+
toHeader: () => "",
|
|
433
|
+
snapshot: () => ({}),
|
|
434
|
+
restore: () => {},
|
|
435
|
+
serialize: emptyCookieStore,
|
|
436
|
+
deserialize: () => {},
|
|
437
|
+
clear: () => {},
|
|
438
|
+
},
|
|
439
|
+
redirects: {
|
|
440
|
+
run: async (options) => {
|
|
441
|
+
const final = await stealthCall(options.url, options);
|
|
442
|
+
return {
|
|
443
|
+
final,
|
|
444
|
+
hops: [],
|
|
445
|
+
reason: "completed",
|
|
446
|
+
cookies: {},
|
|
447
|
+
cookieStore: emptyCookieStore(),
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
close: () => {},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
env: { get: () => undefined },
|
|
456
|
+
credential,
|
|
457
|
+
request,
|
|
458
|
+
http: {
|
|
459
|
+
request: (url, options) =>
|
|
460
|
+
httpCall(options?.method?.toUpperCase() ?? "GET", url, options?.body, options),
|
|
461
|
+
get: (url, options) => httpCall("GET", url, undefined, options),
|
|
462
|
+
post: (url, body, options) => httpCall("POST", url, body, options),
|
|
463
|
+
put: (url, body, options) => httpCall("PUT", url, body, options),
|
|
464
|
+
delete: (url, options) => httpCall("DELETE", url, undefined, options),
|
|
465
|
+
stream: async (url, options) => {
|
|
466
|
+
const response = await dispatch({
|
|
467
|
+
transport: "http",
|
|
468
|
+
method: options?.method?.toUpperCase() ?? "GET",
|
|
469
|
+
url,
|
|
470
|
+
body: options?.body,
|
|
471
|
+
options,
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
status: response.status,
|
|
475
|
+
ok: response.status >= 200 && response.status < 300,
|
|
476
|
+
headers: response.headers,
|
|
477
|
+
body: streamFromBytes(response.bytes),
|
|
478
|
+
bytes: () => singleBytes(response.bytes),
|
|
479
|
+
textChunks: () => singleText(response.text),
|
|
480
|
+
lines: () => singleText(response.text),
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
sse: async (url, options) => {
|
|
484
|
+
const response = await dispatch({
|
|
485
|
+
transport: "http",
|
|
486
|
+
method: options?.method?.toUpperCase() ?? "GET",
|
|
487
|
+
url,
|
|
488
|
+
body: options?.body,
|
|
489
|
+
options,
|
|
490
|
+
});
|
|
491
|
+
async function* messages() {
|
|
492
|
+
for (const block of response.text.split(/\r?\n\r?\n/)) {
|
|
493
|
+
const data = block
|
|
494
|
+
.split(/\r?\n/)
|
|
495
|
+
.filter((line) => line.startsWith("data:"))
|
|
496
|
+
.map((line) => line.slice(5).trimStart())
|
|
497
|
+
.join("\n");
|
|
498
|
+
if (!data) continue;
|
|
499
|
+
yield { event: "message", data, json: <T>() => JSON.parse(data) as T };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return messages();
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
cache: createProviderCache({ providerId: `standard-test-${operationName}` }),
|
|
506
|
+
state,
|
|
507
|
+
stealth: {
|
|
508
|
+
fetch: stealthCall,
|
|
509
|
+
createSession: createStealthSession,
|
|
510
|
+
},
|
|
511
|
+
browser: {
|
|
512
|
+
engine: "playwright-stealth",
|
|
513
|
+
newPage: async () => createBrowserPage(),
|
|
514
|
+
rawPage: async () => createBrowserPage(),
|
|
515
|
+
withIsolatedContext: async (handler) => handler(createBrowserPage()),
|
|
516
|
+
solveChallenge: async (challenge) =>
|
|
517
|
+
(
|
|
518
|
+
await dispatch({
|
|
519
|
+
transport: "browser",
|
|
520
|
+
method: "solveChallenge",
|
|
521
|
+
body: challenge,
|
|
522
|
+
})
|
|
523
|
+
).data as Awaited<ReturnType<ProviderContext["browser"]["solveChallenge"]>>,
|
|
524
|
+
},
|
|
525
|
+
...(provider.native
|
|
526
|
+
? {
|
|
527
|
+
native: {
|
|
528
|
+
network: {
|
|
529
|
+
connectTcp: async (options) => {
|
|
530
|
+
const request = snapshotNativeConnectInput(options);
|
|
531
|
+
requireNativeEgress().assertConnect(request, "disabled");
|
|
532
|
+
return createNativeConnection(
|
|
533
|
+
await dispatch({
|
|
534
|
+
transport: "native",
|
|
535
|
+
method: "connectTcp",
|
|
536
|
+
url: `tcp://${request.host}:${request.port}`,
|
|
537
|
+
options: request,
|
|
538
|
+
}),
|
|
539
|
+
dispatch,
|
|
540
|
+
`tcp://${request.host}:${request.port}`,
|
|
541
|
+
);
|
|
542
|
+
},
|
|
543
|
+
connectTls: async (options) => {
|
|
544
|
+
const request = snapshotNativeConnectInput(options);
|
|
545
|
+
requireNativeEgress().assertConnect(request, "required");
|
|
546
|
+
return createNativeConnection(
|
|
547
|
+
await dispatch({
|
|
548
|
+
transport: "native",
|
|
549
|
+
method: "connectTls",
|
|
550
|
+
url: `tls://${request.host}:${request.port}`,
|
|
551
|
+
options: request,
|
|
552
|
+
}),
|
|
553
|
+
dispatch,
|
|
554
|
+
`tls://${request.host}:${request.port}`,
|
|
555
|
+
);
|
|
556
|
+
},
|
|
557
|
+
grantTcpEgress: (input) => {
|
|
558
|
+
return requireNativeEgress().grant(snapshotNativeGrantInput(input));
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
: {}),
|
|
564
|
+
trace: { span: async (_name, fn) => fn() },
|
|
565
|
+
auth: { requestField: async (name) => unsupported(`ctx.auth.requestField(${name})`) },
|
|
566
|
+
ocr: createUnsupportedOcrClient(
|
|
567
|
+
"Standard test upstream context does not support ctx.ocr.recognize",
|
|
568
|
+
),
|
|
569
|
+
stt: createUnsupportedSttClient(
|
|
570
|
+
"Standard test upstream context does not support ctx.stt.transcribe",
|
|
571
|
+
),
|
|
572
|
+
resolver: {
|
|
573
|
+
solve: async () => unsupported("ctx.resolver.solve"),
|
|
574
|
+
},
|
|
575
|
+
choice: createTestProviderChoiceContext({
|
|
576
|
+
providerId: `standard-test-${operationName}`,
|
|
577
|
+
request,
|
|
578
|
+
credential,
|
|
579
|
+
state,
|
|
580
|
+
}),
|
|
581
|
+
} as ProviderContext;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function createNativeConnection(
|
|
585
|
+
initialResponse: NormalizedUpstreamResponse,
|
|
586
|
+
dispatch: (
|
|
587
|
+
call: Omit<StandardTestsUpstreamCall, "operationName">,
|
|
588
|
+
) => Promise<NormalizedUpstreamResponse>,
|
|
589
|
+
url: string,
|
|
590
|
+
): NativeNetworkConnection {
|
|
591
|
+
let unread = initialResponse.bytes.slice(0);
|
|
592
|
+
return {
|
|
593
|
+
read: async () => {
|
|
594
|
+
if (unread.byteLength === 0) return null;
|
|
595
|
+
const bytes = unread;
|
|
596
|
+
unread = new Uint8Array();
|
|
597
|
+
return bytes;
|
|
598
|
+
},
|
|
599
|
+
write: async (body) => {
|
|
600
|
+
const response = await dispatch({ transport: "native", method: "write", url, body });
|
|
601
|
+
unread = response.bytes.slice(0);
|
|
602
|
+
},
|
|
603
|
+
close: async () => {},
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
139
607
|
function unsupported(name: string): never {
|
|
140
608
|
throw new Error(`Standard test snapshot context does not support ${name}`);
|
|
141
609
|
}
|
|
142
610
|
|
|
143
|
-
function createSnapshotContext(rawFixture: unknown): ProviderContext {
|
|
611
|
+
export function createSnapshotContext(rawFixture: unknown): ProviderContext {
|
|
144
612
|
const credential: CredentialContext = {
|
|
145
613
|
mode: "none",
|
|
146
614
|
get: () => undefined,
|
|
@@ -150,18 +618,61 @@ function createSnapshotContext(rawFixture: unknown): ProviderContext {
|
|
|
150
618
|
};
|
|
151
619
|
const request = { headers: {} };
|
|
152
620
|
const state = createMemoryProviderRuntimeState();
|
|
621
|
+
const streamCaptureGroup = findStreamCaptureGroup(rawFixture);
|
|
622
|
+
let nextCaptureItem = 0;
|
|
623
|
+
const replayJsonResponse = () => {
|
|
624
|
+
if (!streamCaptureGroup) return jsonResponse(rawFixture);
|
|
625
|
+
const item = streamCaptureGroup.items[nextCaptureItem];
|
|
626
|
+
if (!item) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Stream fixture exhausted: no recorded response exists for ordinary HTTP call ${nextCaptureItem + 1}.`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
if (item.kind !== "response") {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Stream fixture call-order mismatch: expected a stream call at position ${nextCaptureItem + 1}, received an ordinary HTTP call.`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
nextCaptureItem += 1;
|
|
637
|
+
return jsonResponse(item.value);
|
|
638
|
+
};
|
|
153
639
|
|
|
154
|
-
|
|
640
|
+
const context: ProviderContext = {
|
|
155
641
|
env: { get: () => undefined },
|
|
156
642
|
credential,
|
|
157
643
|
request,
|
|
158
644
|
http: {
|
|
159
|
-
request: async () =>
|
|
160
|
-
get: async () =>
|
|
161
|
-
post: async () =>
|
|
162
|
-
put: async () =>
|
|
163
|
-
delete: async () =>
|
|
164
|
-
stream: async () =>
|
|
645
|
+
request: async () => replayJsonResponse(),
|
|
646
|
+
get: async () => replayJsonResponse(),
|
|
647
|
+
post: async () => replayJsonResponse(),
|
|
648
|
+
put: async () => replayJsonResponse(),
|
|
649
|
+
delete: async () => replayJsonResponse(),
|
|
650
|
+
stream: async (...args) => {
|
|
651
|
+
if (!streamCaptureGroup) return unsupported("ctx.http.stream");
|
|
652
|
+
const item = streamCaptureGroup.items[nextCaptureItem];
|
|
653
|
+
if (!item) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Stream fixture exhausted: no recorded evidence exists for stream call ${nextCaptureItem + 1}.`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
if (item.kind !== "stream") {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`Stream fixture call-order mismatch: expected an ordinary HTTP call at position ${nextCaptureItem + 1}, received a stream call.`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (item.evidence.request) {
|
|
664
|
+
const expected = item.evidence.request;
|
|
665
|
+
const actualMethod = (args[1]?.method ?? "GET").toUpperCase();
|
|
666
|
+
const actualPath = replayRequestPath(args[0], expected.path);
|
|
667
|
+
if (actualMethod !== expected.method || actualPath !== expected.path) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Stream fixture request mismatch: expected ${expected.method} ${expected.path}, received ${actualMethod} ${actualPath}.`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
nextCaptureItem += 1;
|
|
674
|
+
return replayStreamEvidence(item.evidence);
|
|
675
|
+
},
|
|
165
676
|
sse: async () => unsupported("ctx.http.sse"),
|
|
166
677
|
},
|
|
167
678
|
cache: createProviderCache({ providerId: "standard-test" }),
|
|
@@ -183,16 +694,42 @@ function createSnapshotContext(rawFixture: unknown): ProviderContext {
|
|
|
183
694
|
auth: {
|
|
184
695
|
requestField: async (name) => unsupported(`ctx.auth.requestField(${name})`),
|
|
185
696
|
},
|
|
697
|
+
ocr: createUnsupportedOcrClient(
|
|
698
|
+
"Standard test snapshot context does not support ctx.ocr.recognize",
|
|
699
|
+
),
|
|
186
700
|
stt: createUnsupportedSttClient(
|
|
187
701
|
"Standard test snapshot context does not support ctx.stt.transcribe",
|
|
188
702
|
),
|
|
703
|
+
resolver: {
|
|
704
|
+
solve: async () => unsupported("ctx.resolver.solve"),
|
|
705
|
+
},
|
|
189
706
|
choice: createTestProviderChoiceContext({
|
|
190
707
|
providerId: "standard-test",
|
|
191
708
|
request,
|
|
192
709
|
credential,
|
|
193
710
|
state,
|
|
194
711
|
}),
|
|
195
|
-
};
|
|
712
|
+
} satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
|
|
713
|
+
snapshotCaptureStates.set(context, {
|
|
714
|
+
assertConsumed() {
|
|
715
|
+
if (streamCaptureGroup && nextCaptureItem !== streamCaptureGroup.items.length) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Stream fixture has ${streamCaptureGroup.items.length - nextCaptureItem} unconsumed capture item${streamCaptureGroup.items.length - nextCaptureItem === 1 ? "" : "s"} after handler completion.`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
consumed: () => nextCaptureItem,
|
|
722
|
+
});
|
|
723
|
+
return context;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function replayRequestPath(requestUrl: string, expectedPath: string): string {
|
|
727
|
+
if (/^[a-z][a-z\d+.-]*:\/\//i.test(requestUrl) || requestUrl.startsWith("/")) {
|
|
728
|
+
return requestPathForFixture(requestUrl);
|
|
729
|
+
}
|
|
730
|
+
return requestPathForFixture(
|
|
731
|
+
new URL(requestUrl, `https://fixture.invalid${expectedPath}`).toString(),
|
|
732
|
+
);
|
|
196
733
|
}
|
|
197
734
|
|
|
198
735
|
async function transformSnapshotOutput(
|
|
@@ -200,14 +737,21 @@ async function transformSnapshotOutput(
|
|
|
200
737
|
rawFixture: unknown,
|
|
201
738
|
): Promise<unknown> {
|
|
202
739
|
const entries = Object.entries(provider.operations);
|
|
203
|
-
const
|
|
740
|
+
const captureStates: Array<{ assertConsumed(): void; consumed(): number }> = [];
|
|
204
741
|
const outputs = await Promise.all(
|
|
205
742
|
entries.map(async ([operationName, operation]) => {
|
|
743
|
+
const context = createSnapshotContext(rawFixture);
|
|
206
744
|
const request = operation.fixtures?.request ?? {};
|
|
207
745
|
const output = await operation.handler(context, request);
|
|
746
|
+
const captureState = snapshotCaptureStates.get(context);
|
|
747
|
+
if (captureState) captureStates.push(captureState);
|
|
748
|
+
if (captureState?.consumed()) captureState.assertConsumed();
|
|
208
749
|
return [operationName, output] as const;
|
|
209
750
|
}),
|
|
210
751
|
);
|
|
752
|
+
if (findStreamCaptureGroup(rawFixture) && !captureStates.some((state) => state.consumed() > 0)) {
|
|
753
|
+
captureStates[0]?.assertConsumed();
|
|
754
|
+
}
|
|
211
755
|
|
|
212
756
|
if (outputs.length === 1) {
|
|
213
757
|
return outputs[0]?.[1];
|
|
@@ -232,7 +776,7 @@ function formatJsonDiff(current: unknown, expected: unknown): string {
|
|
|
232
776
|
|
|
233
777
|
if (currentLine === expectedLine) {
|
|
234
778
|
if (currentLine !== undefined) {
|
|
235
|
-
lines.push(`
|
|
779
|
+
lines.push(` ${currentLine}`);
|
|
236
780
|
}
|
|
237
781
|
continue;
|
|
238
782
|
}
|
|
@@ -284,6 +828,58 @@ function parseSchemaFixture(
|
|
|
284
828
|
);
|
|
285
829
|
}
|
|
286
830
|
|
|
831
|
+
async function materializeHandlerOutput(output: unknown): Promise<unknown> {
|
|
832
|
+
if (!(output instanceof Response)) return output;
|
|
833
|
+
const contentType = output.headers.get("content-type") ?? "";
|
|
834
|
+
if (contentType.includes("application/json")) return output.json();
|
|
835
|
+
return output.text();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Internal execution seam exported for focused SDK tests; use runStandardTests as public API. */
|
|
839
|
+
export async function executeStandardTestHandler(
|
|
840
|
+
provider: ProviderDefinition,
|
|
841
|
+
operationName: string,
|
|
842
|
+
upstreamStub: StandardTestsUpstreamStub,
|
|
843
|
+
): Promise<unknown> {
|
|
844
|
+
const operation = provider.operations[operationName];
|
|
845
|
+
if (!operation) throw new Error(`Unknown operation "${operationName}".`);
|
|
846
|
+
if (operation.fixtures?.request === undefined) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`Operation "${operationName}" has no fixtures.request for handler E2E execution.`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const context = createUpstreamContext(provider, operationName, upstreamStub);
|
|
852
|
+
const output = await materializeHandlerOutput(
|
|
853
|
+
await operation.handler(context, operation.fixtures.request),
|
|
854
|
+
);
|
|
855
|
+
const result = safeParseSchemaSync(
|
|
856
|
+
operation.output,
|
|
857
|
+
output,
|
|
858
|
+
`operations.${operationName}.handler.output`,
|
|
859
|
+
);
|
|
860
|
+
if (!result.success) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
[
|
|
863
|
+
`Handler output for operation "${operationName}" failed schema validation.`,
|
|
864
|
+
formatJsonDiff(
|
|
865
|
+
{ valid: false, value: output, error: result.error },
|
|
866
|
+
{ valid: true, value: output },
|
|
867
|
+
),
|
|
868
|
+
].join("\n"),
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
return output;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function isOptionsShortcut(value: unknown): value is StandardTestsOptions {
|
|
875
|
+
return (
|
|
876
|
+
value !== null &&
|
|
877
|
+
typeof value === "object" &&
|
|
878
|
+
!Array.isArray(value) &&
|
|
879
|
+
Object.hasOwn(value, "upstreamStub")
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
287
883
|
/**
|
|
288
884
|
* Run standard SDK tests for a provider in one line.
|
|
289
885
|
*
|
|
@@ -292,13 +888,39 @@ function parseSchemaFixture(
|
|
|
292
888
|
* import { runStandardTests } from "@apifuse/provider-sdk/testing";
|
|
293
889
|
* runStandardTests(myProvider, rawFixture, manifest, { snapshot: true });
|
|
294
890
|
*/
|
|
891
|
+
export function runStandardTests(
|
|
892
|
+
provider: ProviderDefinition,
|
|
893
|
+
options: StandardTestsOptions & { upstreamStub: StandardTestsUpstreamStub },
|
|
894
|
+
): StandardTestsResult;
|
|
295
895
|
export function runStandardTests(
|
|
296
896
|
provider: ProviderDefinition,
|
|
297
897
|
rawFixture?: unknown,
|
|
298
898
|
manifest?: StandardTestsManifest,
|
|
299
|
-
options
|
|
300
|
-
):
|
|
899
|
+
options?: StandardTestsOptions,
|
|
900
|
+
): StandardTestsResult;
|
|
901
|
+
export function runStandardTests(
|
|
902
|
+
provider: ProviderDefinition,
|
|
903
|
+
rawFixtureOrOptions?: unknown,
|
|
904
|
+
manifest?: StandardTestsManifest,
|
|
905
|
+
legacyOptions?: StandardTestsOptions,
|
|
906
|
+
): StandardTestsResult {
|
|
907
|
+
const shortcut =
|
|
908
|
+
manifest === undefined && legacyOptions === undefined && isOptionsShortcut(rawFixtureOrOptions);
|
|
909
|
+
const rawFixture = shortcut ? undefined : rawFixtureOrOptions;
|
|
910
|
+
const options = shortcut ? rawFixtureOrOptions : (legacyOptions ?? {});
|
|
301
911
|
const operations = Object.entries(provider.operations);
|
|
912
|
+
const warnings = options.upstreamStub
|
|
913
|
+
? operations
|
|
914
|
+
.filter(([, operation]) => operation.fixtures?.request === undefined)
|
|
915
|
+
.map(
|
|
916
|
+
([operationName]) =>
|
|
917
|
+
`[provider-sdk] Operation "${provider.id}.${operationName}" has no fixtures.request, so runStandardTests cannot invoke its handler E2E.`,
|
|
918
|
+
)
|
|
919
|
+
: operations.map(
|
|
920
|
+
([operationName]) =>
|
|
921
|
+
`[provider-sdk] Operation "${provider.id}.${operationName}" has no handler E2E coverage in runStandardTests; configure upstreamStub to invoke the real handler.`,
|
|
922
|
+
);
|
|
923
|
+
for (const warning of warnings) console.warn(warning);
|
|
302
924
|
|
|
303
925
|
const assertFixtureValidation = (): void => {
|
|
304
926
|
expect(rawFixture).toBeDefined();
|
|
@@ -421,7 +1043,13 @@ export function runStandardTests(
|
|
|
421
1043
|
const serialized = stableStringify(actual);
|
|
422
1044
|
const snapshotFile = Bun.file(snapshotPath);
|
|
423
1045
|
|
|
424
|
-
|
|
1046
|
+
const snapshotExists = await snapshotFile.exists();
|
|
1047
|
+
if (!snapshotExists && options.requireSnapshot && !shouldUpdateSnapshots()) {
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`Required golden snapshot is missing: ${snapshotPath}. Regenerate it with bun test --update-snapshots.`,
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (shouldUpdateSnapshots() || !snapshotExists) {
|
|
425
1053
|
await Bun.write(snapshotPath, serialized);
|
|
426
1054
|
}
|
|
427
1055
|
|
|
@@ -429,5 +1057,20 @@ export function runStandardTests(
|
|
|
429
1057
|
expect(actual).toEqual(expected);
|
|
430
1058
|
});
|
|
431
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
if (options.upstreamStub) {
|
|
1062
|
+
for (const [operationName, operation] of operations) {
|
|
1063
|
+
if (operation.fixtures?.request === undefined) continue;
|
|
1064
|
+
it(`invokes the real ${operationName} handler with canned upstream responses`, async () => {
|
|
1065
|
+
await executeStandardTestHandler(
|
|
1066
|
+
provider,
|
|
1067
|
+
operationName,
|
|
1068
|
+
options.upstreamStub as StandardTestsUpstreamStub,
|
|
1069
|
+
);
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
432
1073
|
});
|
|
1074
|
+
|
|
1075
|
+
return { warnings };
|
|
433
1076
|
}
|