@neurocode-ai/llm 1.18.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/AGENTS.md +321 -0
- package/DESIGN.md +1114 -0
- package/README.md +131 -0
- package/example/call-sites.md +591 -0
- package/example/tutorial.ts +255 -0
- package/package.json +51 -0
- package/script/recording-cost-report.ts +250 -0
- package/script/setup-recording-env.ts +542 -0
- package/src/cache-policy.ts +111 -0
- package/src/index.ts +33 -0
- package/src/llm.ts +186 -0
- package/src/protocols/anthropic-messages.ts +855 -0
- package/src/protocols/bedrock-converse.ts +674 -0
- package/src/protocols/bedrock-event-stream.ts +87 -0
- package/src/protocols/gemini.ts +512 -0
- package/src/protocols/index.ts +6 -0
- package/src/protocols/openai-chat.ts +506 -0
- package/src/protocols/openai-compatible-chat.ts +24 -0
- package/src/protocols/openai-responses.ts +1022 -0
- package/src/protocols/shared.ts +326 -0
- package/src/protocols/utils/bedrock-auth.ts +70 -0
- package/src/protocols/utils/bedrock-cache.ts +37 -0
- package/src/protocols/utils/bedrock-media.ts +90 -0
- package/src/protocols/utils/cache.ts +16 -0
- package/src/protocols/utils/gemini-tool-schema.ts +99 -0
- package/src/protocols/utils/lifecycle.ts +102 -0
- package/src/protocols/utils/openai-options.ts +93 -0
- package/src/protocols/utils/tool-schema.ts +86 -0
- package/src/protocols/utils/tool-stream.ts +218 -0
- package/src/provider-error.ts +43 -0
- package/src/provider.ts +36 -0
- package/src/providers/amazon-bedrock.ts +43 -0
- package/src/providers/anthropic.ts +35 -0
- package/src/providers/azure.ts +110 -0
- package/src/providers/cloudflare.ts +127 -0
- package/src/providers/github-copilot.ts +69 -0
- package/src/providers/google.ts +35 -0
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-compatible-profile.ts +20 -0
- package/src/providers/openai-compatible.ts +65 -0
- package/src/providers/openai-options.ts +83 -0
- package/src/providers/openai.ts +63 -0
- package/src/providers/openrouter.ts +98 -0
- package/src/providers/xai.ts +56 -0
- package/src/route/auth-options.ts +57 -0
- package/src/route/auth.ts +156 -0
- package/src/route/client.ts +436 -0
- package/src/route/endpoint.ts +53 -0
- package/src/route/executor.ts +385 -0
- package/src/route/framing.ts +27 -0
- package/src/route/index.ts +25 -0
- package/src/route/protocol.ts +84 -0
- package/src/route/transport/http.ts +155 -0
- package/src/route/transport/index.ts +33 -0
- package/src/route/transport/websocket.ts +280 -0
- package/src/schema/errors.ts +207 -0
- package/src/schema/events.ts +618 -0
- package/src/schema/ids.ts +43 -0
- package/src/schema/index.ts +5 -0
- package/src/schema/messages.ts +312 -0
- package/src/schema/options.ts +276 -0
- package/src/tool-runtime.ts +78 -0
- package/src/tool.ts +253 -0
- package/src/utils/record.ts +3 -0
- package/sst-env.d.ts +10 -0
- package/test/adapter.test.ts +171 -0
- package/test/auth-options.types.ts +168 -0
- package/test/auth.test.ts +103 -0
- package/test/cache-policy.test.ts +262 -0
- package/test/continuation-scenarios.ts +104 -0
- package/test/endpoint.test.ts +58 -0
- package/test/executor.test.ts +458 -0
- package/test/exports.test.ts +76 -0
- package/test/fixtures/media/restroom.png +0 -0
- package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
- package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
- package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
- package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
- package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
- package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
- package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
- package/test/fixtures/recordings/gemini/streams-text.json +28 -0
- package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
- package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
- package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
- package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
- package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
- package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
- package/test/generate-object.test.ts +184 -0
- package/test/lib/effect.ts +50 -0
- package/test/lib/http.ts +98 -0
- package/test/lib/openai-chunks.ts +27 -0
- package/test/lib/sse.ts +17 -0
- package/test/lib/tool-runtime.ts +146 -0
- package/test/llm.test.ts +199 -0
- package/test/prepare.test.ts +178 -0
- package/test/provider/anthropic-messages-cache.recorded.test.ts +53 -0
- package/test/provider/anthropic-messages.recorded.test.ts +45 -0
- package/test/provider/anthropic-messages.test.ts +895 -0
- package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
- package/test/provider/bedrock-converse.test.ts +744 -0
- package/test/provider/cloudflare.test.ts +230 -0
- package/test/provider/gemini-cache.recorded.test.ts +48 -0
- package/test/provider/gemini.test.ts +584 -0
- package/test/provider/golden.recorded.test.ts +223 -0
- package/test/provider/openai-chat.test.ts +674 -0
- package/test/provider/openai-compatible-chat.test.ts +238 -0
- package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
- package/test/provider/openai-responses.test.ts +1472 -0
- package/test/provider/openrouter.test.ts +56 -0
- package/test/provider-error.test.ts +30 -0
- package/test/provider.types.ts +41 -0
- package/test/recorded-golden.ts +97 -0
- package/test/recorded-runner.ts +100 -0
- package/test/recorded-scenarios.ts +531 -0
- package/test/recorded-test.ts +94 -0
- package/test/recorded-utils.ts +56 -0
- package/test/recorded-websocket.ts +26 -0
- package/test/response.test.ts +98 -0
- package/test/route.test.ts +43 -0
- package/test/schema.test.ts +86 -0
- package/test/tool-runtime.test.ts +818 -0
- package/test/tool-schema-projection.test.ts +117 -0
- package/test/tool-stream.test.ts +99 -0
- package/test/tool.types.ts +40 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { LLM } from "../../src"
|
|
4
|
+
import { LLMClient } from "../../src/route"
|
|
5
|
+
import * as OpenRouter from "../../src/providers/openrouter"
|
|
6
|
+
import { it } from "../lib/effect"
|
|
7
|
+
|
|
8
|
+
describe("OpenRouter", () => {
|
|
9
|
+
it.effect("prepares OpenRouter models through the OpenAI-compatible Chat route", () =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const model = OpenRouter.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini")
|
|
12
|
+
|
|
13
|
+
expect(model).toMatchObject({
|
|
14
|
+
id: "openai/gpt-4o-mini",
|
|
15
|
+
provider: "openrouter",
|
|
16
|
+
route: { id: "openrouter" },
|
|
17
|
+
})
|
|
18
|
+
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
|
|
19
|
+
|
|
20
|
+
const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
|
|
21
|
+
|
|
22
|
+
expect(prepared.route).toBe("openrouter")
|
|
23
|
+
expect(prepared.body).toMatchObject({
|
|
24
|
+
model: "openai/gpt-4o-mini",
|
|
25
|
+
messages: [{ role: "user", content: "Say hello." }],
|
|
26
|
+
stream: true,
|
|
27
|
+
})
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
it.effect("applies OpenRouter payload options from the model helper", () =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const prepared = yield* LLMClient.prepare(
|
|
34
|
+
LLM.request({
|
|
35
|
+
model: OpenRouter.configure({
|
|
36
|
+
apiKey: "test-key",
|
|
37
|
+
providerOptions: {
|
|
38
|
+
openrouter: {
|
|
39
|
+
usage: true,
|
|
40
|
+
reasoning: { effort: "high" },
|
|
41
|
+
promptCacheKey: "session_123",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}).model("anthropic/claude-3.7-sonnet:thinking"),
|
|
45
|
+
prompt: "Think briefly.",
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(prepared.body).toMatchObject({
|
|
50
|
+
usage: { include: true },
|
|
51
|
+
reasoning: { effort: "high" },
|
|
52
|
+
prompt_cache_key: "session_123",
|
|
53
|
+
})
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { isContextOverflow } from "../src"
|
|
3
|
+
|
|
4
|
+
describe("provider error classification", () => {
|
|
5
|
+
test("classifies provider token limit messages as context overflow", () => {
|
|
6
|
+
const messages = [
|
|
7
|
+
"tokens in request more than max tokens allowed",
|
|
8
|
+
'{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}',
|
|
9
|
+
"Requested token count exceeds the model's maximum context length of 131072 tokens.",
|
|
10
|
+
"Input length (265330) exceeds model's maximum context length (262144).",
|
|
11
|
+
"Input length 131393 exceeds the maximum allowed input length of 131040 tokens.",
|
|
12
|
+
"The input (516368 tokens) is longer than the model's context length (262144 tokens).",
|
|
13
|
+
"Prompt has 5,958,968 tokens, but the configured context size is 256,000 tokens",
|
|
14
|
+
"Too many tokens",
|
|
15
|
+
"Token limit exceeded",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
expect(messages.every(isContextOverflow)).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("does not classify rate limits as context overflow", () => {
|
|
22
|
+
const messages = [
|
|
23
|
+
"Throttling error: Too many tokens, please wait before trying again.",
|
|
24
|
+
"Rate limit exceeded, please retry after 30 seconds.",
|
|
25
|
+
"Too many requests. Please slow down.",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
expect(messages.some(isContextOverflow)).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Provider } from "../src/provider"
|
|
2
|
+
import { ProviderID, type Model } from "../src/schema"
|
|
3
|
+
|
|
4
|
+
declare const model: (id: string) => Model
|
|
5
|
+
declare const requiredModel: (id: string, options: { readonly baseURL: string }) => Model
|
|
6
|
+
declare const chat: (id: string, options: { readonly apiKey: string }) => Model
|
|
7
|
+
|
|
8
|
+
Provider.make({
|
|
9
|
+
id: ProviderID.make("example"),
|
|
10
|
+
model,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
Provider.make({
|
|
14
|
+
id: ProviderID.make("bad"),
|
|
15
|
+
model,
|
|
16
|
+
// @ts-expect-error provider definitions should not grow accidental top-level fields.
|
|
17
|
+
routes: [],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const requiredProvider = Provider.make({
|
|
21
|
+
id: ProviderID.make("required"),
|
|
22
|
+
model: requiredModel,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Provider.make is advanced structural typing coverage; built-in providers use
|
|
26
|
+
// configure(...).model(id) facades instead of second-argument selectors.
|
|
27
|
+
requiredProvider.model("custom", { baseURL: "https://example.com/v1" })
|
|
28
|
+
|
|
29
|
+
// @ts-expect-error Provider.make preserves required model options.
|
|
30
|
+
requiredProvider.model("custom")
|
|
31
|
+
|
|
32
|
+
const multiApiProvider = Provider.make({
|
|
33
|
+
id: ProviderID.make("multi-api"),
|
|
34
|
+
model,
|
|
35
|
+
apis: { chat },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
multiApiProvider.apis.chat("chat-model", { apiKey: "key" })
|
|
39
|
+
|
|
40
|
+
// @ts-expect-error Provider.make preserves API-specific option types.
|
|
41
|
+
multiApiProvider.apis.chat("chat-model")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { HttpRecorder } from "@neurocode-ai/http-recorder"
|
|
2
|
+
import { describe } from "bun:test"
|
|
3
|
+
import { Effect } from "effect"
|
|
4
|
+
import type { Model } from "../src"
|
|
5
|
+
import { goldenScenarioTags, goldenScenarioTitle, runGoldenScenario, type GoldenScenarioID } from "./recorded-scenarios"
|
|
6
|
+
import { recordedTests } from "./recorded-test"
|
|
7
|
+
import { kebab } from "./recorded-utils"
|
|
8
|
+
|
|
9
|
+
type Transport = "http" | "websocket"
|
|
10
|
+
|
|
11
|
+
type ScenarioInput =
|
|
12
|
+
| GoldenScenarioID
|
|
13
|
+
| {
|
|
14
|
+
readonly id: GoldenScenarioID
|
|
15
|
+
readonly name?: string
|
|
16
|
+
readonly cassette?: string
|
|
17
|
+
readonly tags?: ReadonlyArray<string>
|
|
18
|
+
readonly maxTokens?: number
|
|
19
|
+
readonly temperature?: number | false
|
|
20
|
+
readonly timeout?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TargetInput = {
|
|
24
|
+
readonly name: string
|
|
25
|
+
readonly model: Model
|
|
26
|
+
readonly protocol?: string
|
|
27
|
+
readonly requires?: ReadonlyArray<string>
|
|
28
|
+
readonly transport?: Transport
|
|
29
|
+
readonly prefix?: string
|
|
30
|
+
readonly tags?: ReadonlyArray<string>
|
|
31
|
+
readonly metadata?: Record<string, unknown>
|
|
32
|
+
readonly options?: HttpRecorder.RecorderOptions
|
|
33
|
+
readonly scenarios: ReadonlyArray<ScenarioInput>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const scenarioInput = (input: ScenarioInput) => (typeof input === "string" ? { id: input } : input)
|
|
37
|
+
|
|
38
|
+
const defaultPrefix = (target: TargetInput) => {
|
|
39
|
+
if (target.prefix) return target.prefix
|
|
40
|
+
const transport = target.transport === "websocket" ? "-websocket" : ""
|
|
41
|
+
return `${target.model.provider}-${target.protocol ?? target.model.route.id}${transport}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const metadata = (target: TargetInput) => ({
|
|
45
|
+
provider: target.model.provider,
|
|
46
|
+
protocol: target.protocol,
|
|
47
|
+
route: target.model.route.id,
|
|
48
|
+
transport: target.transport ?? "http",
|
|
49
|
+
model: target.model.id,
|
|
50
|
+
...target.metadata,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const tags = (target: TargetInput) => [
|
|
54
|
+
...(target.transport === "websocket" ? ["transport:websocket"] : []),
|
|
55
|
+
...(target.tags ?? []),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const runTarget = (target: TargetInput) => {
|
|
59
|
+
const recorded = recordedTests({
|
|
60
|
+
prefix: defaultPrefix(target),
|
|
61
|
+
provider: target.model.provider,
|
|
62
|
+
protocol: target.protocol,
|
|
63
|
+
requires: target.requires,
|
|
64
|
+
tags: tags(target),
|
|
65
|
+
metadata: metadata(target),
|
|
66
|
+
options: target.options,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe(`${target.name} recorded`, () => {
|
|
70
|
+
target.scenarios.forEach((raw) => {
|
|
71
|
+
const input = scenarioInput(raw)
|
|
72
|
+
const name = input.name ?? goldenScenarioTitle(input.id)
|
|
73
|
+
recorded.effect.with(
|
|
74
|
+
name,
|
|
75
|
+
{
|
|
76
|
+
cassette: input.cassette,
|
|
77
|
+
id: `${kebab(target.name)}-${input.id}`,
|
|
78
|
+
tags: [...goldenScenarioTags(input.id), ...(input.tags ?? [])],
|
|
79
|
+
},
|
|
80
|
+
() =>
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
yield* runGoldenScenario(input.id, {
|
|
83
|
+
id: `recorded_${kebab(target.name).replaceAll("-", "_")}_${input.id.replaceAll("-", "_")}`,
|
|
84
|
+
model: target.model,
|
|
85
|
+
maxTokens: input.maxTokens,
|
|
86
|
+
temperature: input.temperature,
|
|
87
|
+
})
|
|
88
|
+
}),
|
|
89
|
+
input.timeout,
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const describeRecordedGoldenScenarios = (targets: ReadonlyArray<TargetInput>) => {
|
|
96
|
+
targets.forEach(runTarget)
|
|
97
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { test, type TestOptions } from "bun:test"
|
|
2
|
+
import { Effect, type Layer } from "effect"
|
|
3
|
+
import { testEffect } from "./lib/effect"
|
|
4
|
+
import { cassetteName, classifiedTags, matchesSelected, missingEnv, unique } from "./recorded-utils"
|
|
5
|
+
|
|
6
|
+
export type RecordedBody<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
|
|
7
|
+
|
|
8
|
+
export type RecordedGroupOptions = {
|
|
9
|
+
readonly prefix: string
|
|
10
|
+
readonly provider?: string
|
|
11
|
+
readonly protocol?: string
|
|
12
|
+
readonly requires?: ReadonlyArray<string>
|
|
13
|
+
readonly tags?: ReadonlyArray<string>
|
|
14
|
+
readonly metadata?: Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RecordedCaseOptions = {
|
|
18
|
+
readonly cassette?: string
|
|
19
|
+
readonly id?: string
|
|
20
|
+
readonly provider?: string
|
|
21
|
+
readonly protocol?: string
|
|
22
|
+
readonly requires?: ReadonlyArray<string>
|
|
23
|
+
readonly tags?: ReadonlyArray<string>
|
|
24
|
+
readonly metadata?: Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const recordedEffectGroup = <
|
|
28
|
+
R,
|
|
29
|
+
E,
|
|
30
|
+
Options extends RecordedGroupOptions,
|
|
31
|
+
CaseOptions extends RecordedCaseOptions,
|
|
32
|
+
>(input: {
|
|
33
|
+
readonly duplicateLabel: string
|
|
34
|
+
readonly options: Options
|
|
35
|
+
readonly cassetteExists: (cassette: string) => boolean
|
|
36
|
+
readonly layer: (input: {
|
|
37
|
+
readonly cassette: string
|
|
38
|
+
readonly tags: ReadonlyArray<string>
|
|
39
|
+
readonly metadata: Record<string, unknown>
|
|
40
|
+
readonly recording: boolean
|
|
41
|
+
readonly options: Options
|
|
42
|
+
readonly caseOptions: CaseOptions
|
|
43
|
+
}) => Layer.Layer<R, E>
|
|
44
|
+
}) => {
|
|
45
|
+
const cassettes = new Set<string>()
|
|
46
|
+
|
|
47
|
+
const run = <A, E2>(
|
|
48
|
+
name: string,
|
|
49
|
+
caseOptions: CaseOptions,
|
|
50
|
+
body: RecordedBody<A, E2, R>,
|
|
51
|
+
testOptions?: number | TestOptions,
|
|
52
|
+
) => {
|
|
53
|
+
const cassette = cassetteName(input.options.prefix, name, caseOptions)
|
|
54
|
+
if (cassettes.has(cassette)) throw new Error(`Duplicate ${input.duplicateLabel} "${cassette}"`)
|
|
55
|
+
cassettes.add(cassette)
|
|
56
|
+
const tags = unique([
|
|
57
|
+
...classifiedTags(input.options),
|
|
58
|
+
...classifiedTags({
|
|
59
|
+
provider: caseOptions.provider,
|
|
60
|
+
protocol: caseOptions.protocol,
|
|
61
|
+
tags: caseOptions.tags,
|
|
62
|
+
}),
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
if (!matchesSelected({ prefix: input.options.prefix, name, cassette, tags }))
|
|
66
|
+
return test.skip(name, () => {}, testOptions)
|
|
67
|
+
|
|
68
|
+
const recording = process.env.RECORD === "true"
|
|
69
|
+
if (recording) {
|
|
70
|
+
if (missingEnv([...(input.options.requires ?? []), ...(caseOptions.requires ?? [])]).length > 0) {
|
|
71
|
+
return test.skip(name, () => {}, testOptions)
|
|
72
|
+
}
|
|
73
|
+
} else if (!input.cassetteExists(cassette)) {
|
|
74
|
+
return test.skip(name, () => {}, testOptions)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return testEffect(
|
|
78
|
+
input.layer({
|
|
79
|
+
cassette,
|
|
80
|
+
tags,
|
|
81
|
+
metadata: { ...input.options.metadata, ...caseOptions.metadata, tags },
|
|
82
|
+
recording,
|
|
83
|
+
options: input.options,
|
|
84
|
+
caseOptions,
|
|
85
|
+
}),
|
|
86
|
+
).live(name, body, testOptions)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const effect = <A, E2>(name: string, body: RecordedBody<A, E2, R>, testOptions?: number | TestOptions) =>
|
|
90
|
+
run(name, {} as CaseOptions, body, testOptions)
|
|
91
|
+
|
|
92
|
+
effect.with = <A, E2>(
|
|
93
|
+
name: string,
|
|
94
|
+
caseOptions: CaseOptions,
|
|
95
|
+
body: RecordedBody<A, E2, R>,
|
|
96
|
+
testOptions?: number | TestOptions,
|
|
97
|
+
) => run(name, caseOptions, body, testOptions)
|
|
98
|
+
|
|
99
|
+
return { effect }
|
|
100
|
+
}
|