@jterrazz/test 6.1.0 → 6.3.0
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 +9 -9
- package/dist/index.cjs +66 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +66 -14
- package/dist/index.js.map +1 -1
- package/dist/intercept.cjs +240 -0
- package/dist/intercept.cjs.map +1 -0
- package/dist/intercept.d.cts +89 -0
- package/dist/intercept.d.ts +89 -0
- package/dist/intercept.js +237 -0
- package/dist/intercept.js.map +1 -0
- package/dist/server.cjs +79 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.js +79 -0
- package/dist/server.js.map +1 -0
- package/dist/sqlite.adapter.cjs +22 -3
- package/dist/sqlite.adapter.cjs.map +1 -1
- package/dist/sqlite.adapter.js +22 -3
- package/dist/sqlite.adapter.js.map +1 -1
- package/dist/types.d.cts +35 -0
- package/dist/types.d.ts +35 -0
- package/package.json +6 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intercept.js","names":["matchesFilter"],"sources":["../src/intercept/anthropic.ts","../src/intercept/http.ts","../src/intercept/openai.ts"],"sourcesContent":["import type { InterceptResponse, InterceptTrigger } from './types.js';\n\nconst ANTHROPIC_MESSAGES_URL = 'https://api.anthropic.com/v1/messages';\n\nexport interface AnthropicMessagesFilter {\n /** Match by model name. */\n model?: RegExp | string;\n /** Match by system message content. */\n system?: RegExp | string;\n /** Match by user message content. */\n user?: RegExp | string;\n /** Match by tool names. */\n tools?: string[];\n}\n\nfunction matchesFilter(body: any, filter: AnthropicMessagesFilter): boolean {\n if (filter.model) {\n const model = body?.model;\n if (typeof filter.model === 'string' && model !== filter.model) {\n return false;\n }\n if (filter.model instanceof RegExp && !filter.model.test(model ?? '')) {\n return false;\n }\n }\n\n if (filter.system) {\n const system = typeof body?.system === 'string' ? body.system : '';\n if (typeof filter.system === 'string' && !system.includes(filter.system)) {\n return false;\n }\n if (filter.system instanceof RegExp && !filter.system.test(system)) {\n return false;\n }\n }\n\n if (filter.user) {\n const userMsg = body?.messages?.find((m: any) => m.role === 'user')?.content ?? '';\n const text = typeof userMsg === 'string' ? userMsg : JSON.stringify(userMsg);\n if (typeof filter.user === 'string' && !text.includes(filter.user)) {\n return false;\n }\n if (filter.user instanceof RegExp && !filter.user.test(text)) {\n return false;\n }\n }\n\n if (filter.tools) {\n const requestTools = body?.tools?.map((t: any) => t.name).filter(Boolean) ?? [];\n if (!filter.tools.every((t) => requestTools.includes(t))) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Anthropic API intercept helpers.\n */\nexport const anthropic = {\n /**\n * Trigger: match Anthropic messages API requests.\n */\n messages(filter?: AnthropicMessagesFilter): InterceptTrigger {\n return {\n method: 'POST',\n url: ANTHROPIC_MESSAGES_URL,\n match: filter ? (body: unknown) => matchesFilter(body, filter) : undefined,\n };\n },\n\n /** Response: wrap data in Anthropic messages format. */\n response(data: unknown): InterceptResponse {\n const content = typeof data === 'string' ? data : JSON.stringify(data);\n return {\n status: 200,\n body: {\n id: 'msg-test',\n type: 'message',\n role: 'assistant',\n content: [{ type: 'text', text: content }],\n model: 'claude-sonnet-4-20250514',\n stop_reason: 'end_turn',\n usage: { input_tokens: 10, output_tokens: 10 },\n },\n };\n },\n\n /** Response: return an Anthropic error. */\n error(status: number, message?: string): InterceptResponse {\n return {\n status,\n body: {\n type: 'error',\n error: {\n type: status === 429 ? 'rate_limit_error' : 'api_error',\n message: message ?? `Anthropic error (${status})`,\n },\n },\n };\n },\n\n /** Response: simulate a timeout. */\n timeout(): InterceptResponse {\n return { status: 200, body: {}, delay: 30_000 };\n },\n};\n","import type { InterceptResponse, InterceptTrigger } from './types.js';\n\n/**\n * Generic HTTP intercept helpers for any URL.\n *\n * @example\n * .intercept(http.get('https://api.example.com/data'), { body: [...] })\n * .intercept(http.post('https://api.stripe.com/v1/charges'), { body: { id: 'ch_...' } })\n */\nexport const http = {\n /** Trigger: match GET requests to a URL pattern. */\n get(url: RegExp | string): InterceptTrigger {\n return { method: 'GET', url };\n },\n\n /** Trigger: match POST requests to a URL pattern. */\n post(url: RegExp | string): InterceptTrigger {\n return { method: 'POST', url };\n },\n\n /** Trigger: match PUT requests to a URL pattern. */\n put(url: RegExp | string): InterceptTrigger {\n return { method: 'PUT', url };\n },\n\n /** Trigger: match DELETE requests to a URL pattern. */\n delete(url: RegExp | string): InterceptTrigger {\n return { method: 'DELETE', url };\n },\n\n /** Trigger: match any method to a URL pattern. */\n any(url: RegExp | string): InterceptTrigger {\n return { method: '*', url };\n },\n\n /** Response: simple JSON success. */\n json(data: unknown, status = 200): InterceptResponse {\n return { status, body: data };\n },\n\n /** Response: error with message. */\n error(status: number, message?: string): InterceptResponse {\n return { status, body: { error: message ?? `HTTP ${status}` } };\n },\n};\n","import type { InterceptResponse, InterceptTrigger } from './types.js';\n\nconst OPENAI_CHAT_URL = 'https://api.openai.com/v1/chat/completions';\n\nexport interface OpenAIChatFilter {\n /** Match by model name (exact string or regex). */\n model?: RegExp | string;\n /** Match by system message content (regex or substring). */\n system?: RegExp | string;\n /** Match by user message content (regex or substring). */\n user?: RegExp | string;\n /** Match by tool/function names present in the request. */\n tools?: string[];\n /** Match by temperature value. */\n temperature?: number;\n}\n\nfunction matchesFilter(body: any, filter: OpenAIChatFilter): boolean {\n if (filter.model) {\n const model = body?.model;\n if (typeof filter.model === 'string' && model !== filter.model) {\n return false;\n }\n if (filter.model instanceof RegExp && !filter.model.test(model ?? '')) {\n return false;\n }\n }\n\n if (filter.system) {\n const systemMsg = body?.messages?.find((m: any) => m.role === 'system')?.content ?? '';\n if (typeof filter.system === 'string' && !systemMsg.includes(filter.system)) {\n return false;\n }\n if (filter.system instanceof RegExp && !filter.system.test(systemMsg)) {\n return false;\n }\n }\n\n if (filter.user) {\n const userMsg = body?.messages?.find((m: any) => m.role === 'user')?.content ?? '';\n if (typeof filter.user === 'string' && !userMsg.includes(filter.user)) {\n return false;\n }\n if (filter.user instanceof RegExp && !filter.user.test(userMsg)) {\n return false;\n }\n }\n\n if (filter.tools) {\n const requestTools = body?.tools?.map((t: any) => t.function?.name).filter(Boolean) ?? [];\n if (!filter.tools.every((t) => requestTools.includes(t))) {\n return false;\n }\n }\n\n if (filter.temperature !== undefined && body?.temperature !== filter.temperature) {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI API intercept helpers.\n */\nexport const openai = {\n /**\n * Trigger: match OpenAI chat completion requests.\n *\n * @param filter - Optional filters to narrow which requests match.\n *\n * @example\n * openai.chat() // any chat call\n * openai.chat({ model: 'gpt-4o' }) // specific model\n * openai.chat({ system: /classify/ }) // system prompt contains \"classify\"\n * openai.chat({ tools: ['extract_facts'] }) // function calling\n */\n chat(filter?: OpenAIChatFilter): InterceptTrigger {\n return {\n method: 'POST',\n url: OPENAI_CHAT_URL,\n match: filter ? (body: unknown) => matchesFilter(body, filter) : undefined,\n };\n },\n\n /**\n * Response: wrap data in OpenAI chat completion format.\n *\n * @param data - The content to return (will be JSON.stringified if not a string).\n *\n * @example\n * openai.response({ categories: ['TECH'] })\n */\n response(data: unknown): InterceptResponse {\n const content = typeof data === 'string' ? data : JSON.stringify(data);\n return {\n status: 200,\n body: {\n id: 'chatcmpl-test',\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: 'gpt-4o-test',\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content },\n finish_reason: 'stop',\n },\n ],\n usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 },\n },\n };\n },\n\n /** Response: return an OpenAI error. */\n error(status: number, message?: string): InterceptResponse {\n return {\n status,\n body: {\n error: {\n message: message ?? `OpenAI error (${status})`,\n type: status === 429 ? 'rate_limit_error' : 'api_error',\n code: status === 429 ? 'rate_limit_exceeded' : null,\n },\n },\n };\n },\n\n /** Response: return malformed (non-JSON) content. */\n malformed(content: string): InterceptResponse {\n return {\n status: 200,\n body: {\n id: 'chatcmpl-test',\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: 'gpt-4o-test',\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content },\n finish_reason: 'stop',\n },\n ],\n usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 },\n },\n };\n },\n\n /** Response: simulate a timeout (30s delay). */\n timeout(): InterceptResponse {\n return {\n status: 200,\n body: {},\n delay: 30_000,\n };\n },\n};\n"],"mappings":"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"}
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/intercept/server.ts
|
|
2
|
+
let mswModule = null;
|
|
3
|
+
let mswHttp = null;
|
|
4
|
+
async function loadMsw() {
|
|
5
|
+
if (!mswModule) {
|
|
6
|
+
mswModule = await import("msw/node");
|
|
7
|
+
mswHttp = await import("msw");
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
node: mswModule,
|
|
11
|
+
msw: mswHttp
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
let serverInstance = null;
|
|
15
|
+
/**
|
|
16
|
+
* Start the MSW server (once per process).
|
|
17
|
+
*/
|
|
18
|
+
async function ensureInterceptServer() {
|
|
19
|
+
if (serverInstance) return;
|
|
20
|
+
const { node } = await loadMsw();
|
|
21
|
+
serverInstance = node.setupServer();
|
|
22
|
+
serverInstance.listen({ onUnhandledRequest: "bypass" });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register intercept entries as MSW handlers. Returns a cleanup function.
|
|
26
|
+
* Handlers are consumed in queue order — each trigger match pops one response.
|
|
27
|
+
*/
|
|
28
|
+
async function registerIntercepts(entries) {
|
|
29
|
+
if (entries.length === 0) return () => {};
|
|
30
|
+
await ensureInterceptServer();
|
|
31
|
+
const { msw } = await loadMsw();
|
|
32
|
+
const queues = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const key = `${entry.trigger.method}:${entry.trigger.url}`;
|
|
35
|
+
const existing = queues.get(key) ?? [];
|
|
36
|
+
existing.push(entry);
|
|
37
|
+
queues.set(key, existing);
|
|
38
|
+
}
|
|
39
|
+
const handlers = [];
|
|
40
|
+
for (const [, queue] of queues) {
|
|
41
|
+
let index = 0;
|
|
42
|
+
const trigger = queue[0].trigger;
|
|
43
|
+
const handler = trigger.method === "*" ? msw.http.all(trigger.url instanceof RegExp ? trigger.url : trigger.url, createResolver(queue, () => index, () => {
|
|
44
|
+
index++;
|
|
45
|
+
})) : msw.http[trigger.method.toLowerCase()](trigger.url instanceof RegExp ? trigger.url : trigger.url, createResolver(queue, () => index, () => {
|
|
46
|
+
index++;
|
|
47
|
+
}));
|
|
48
|
+
handlers.push(handler);
|
|
49
|
+
}
|
|
50
|
+
serverInstance.use(...handlers);
|
|
51
|
+
return () => {
|
|
52
|
+
serverInstance.resetHandlers();
|
|
53
|
+
};
|
|
54
|
+
function createResolver(queue, getIndex, advance) {
|
|
55
|
+
return async ({ request }) => {
|
|
56
|
+
const idx = getIndex();
|
|
57
|
+
if (idx >= queue.length) return;
|
|
58
|
+
const entry = queue[idx];
|
|
59
|
+
if (entry.trigger.match) {
|
|
60
|
+
let body = null;
|
|
61
|
+
try {
|
|
62
|
+
body = await request.clone().json();
|
|
63
|
+
} catch {}
|
|
64
|
+
if (!entry.trigger.match(body)) return;
|
|
65
|
+
}
|
|
66
|
+
advance();
|
|
67
|
+
if (entry.response.delay) await new Promise((r) => setTimeout(r, entry.response.delay));
|
|
68
|
+
const { HttpResponse } = await loadMsw().then((m) => m.msw);
|
|
69
|
+
return HttpResponse.json(entry.response.body, {
|
|
70
|
+
status: entry.response.status ?? 200,
|
|
71
|
+
headers: entry.response.headers
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
exports.registerIntercepts = registerIntercepts;
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.cjs","names":[],"sources":["../src/intercept/server.ts"],"sourcesContent":["/**\n * MSW-based intercept server. Manages HTTP interception for spec.run().\n *\n * Intercepts are queued per trigger — the first matching handler is consumed,\n * then the next one in the queue is used for subsequent matching requests.\n */\nimport type { InterceptEntry } from './types.js';\n\nlet mswModule: any = null;\nlet mswHttp: any = null;\n\nasync function loadMsw() {\n if (!mswModule) {\n mswModule = await import('msw/node');\n mswHttp = await import('msw');\n }\n return { node: mswModule, msw: mswHttp! };\n}\n\nlet serverInstance: any = null;\n\n/**\n * Start the MSW server (once per process).\n */\nexport async function ensureInterceptServer(): Promise<void> {\n if (serverInstance) {\n return;\n }\n const { node } = await loadMsw();\n serverInstance = node.setupServer();\n serverInstance.listen({ onUnhandledRequest: 'bypass' });\n}\n\n/**\n * Register intercept entries as MSW handlers. Returns a cleanup function.\n * Handlers are consumed in queue order — each trigger match pops one response.\n */\nexport async function registerIntercepts(entries: InterceptEntry[]): Promise<() => void> {\n if (entries.length === 0) {\n return () => {};\n }\n\n await ensureInterceptServer();\n const { msw } = await loadMsw();\n\n // Build a queue per unique trigger (same method+url+match combo)\n const queues = new Map<string, InterceptEntry[]>();\n for (const entry of entries) {\n const key = `${entry.trigger.method}:${entry.trigger.url}`;\n const existing = queues.get(key) ?? [];\n existing.push(entry);\n queues.set(key, existing);\n }\n\n const handlers: any[] = [];\n\n for (const [, queue] of queues) {\n let index = 0;\n const trigger = queue[0].trigger;\n\n const handler =\n trigger.method === '*'\n ? msw.http.all(\n trigger.url instanceof RegExp ? trigger.url : trigger.url,\n createResolver(\n queue,\n () => index,\n () => {\n index++;\n },\n ),\n )\n : (msw.http as any)[trigger.method.toLowerCase()](\n trigger.url instanceof RegExp ? trigger.url : trigger.url,\n createResolver(\n queue,\n () => index,\n () => {\n index++;\n },\n ),\n );\n\n handlers.push(handler);\n }\n\n serverInstance.use(...handlers);\n\n return () => {\n serverInstance.resetHandlers();\n };\n\n function createResolver(queue: InterceptEntry[], getIndex: () => number, advance: () => void) {\n return async ({ request }: { request: Request }) => {\n const idx = getIndex();\n if (idx >= queue.length) {\n // Queue exhausted — passthrough\n return undefined;\n }\n\n const entry = queue[idx];\n\n // Check body matcher if present\n if (entry.trigger.match) {\n let body: unknown = null;\n try {\n body = await request.clone().json();\n } catch {\n // Not JSON — skip match\n }\n if (!entry.trigger.match(body)) {\n return undefined;\n }\n }\n\n advance();\n\n // Apply delay if specified\n if (entry.response.delay) {\n await new Promise((r) => setTimeout(r, entry.response.delay));\n }\n\n const { HttpResponse } = await loadMsw().then((m) => m.msw);\n return HttpResponse.json(entry.response.body, {\n status: entry.response.status ?? 200,\n headers: entry.response.headers,\n });\n };\n }\n}\n\n/**\n * Stop the MSW server (call in afterAll).\n */\nexport async function stopInterceptServer(): Promise<void> {\n if (serverInstance) {\n serverInstance.close();\n serverInstance = null;\n }\n}\n"],"mappings":";AAQA,IAAI,YAAiB;AACrB,IAAI,UAAe;AAEnB,eAAe,UAAU;AACrB,KAAI,CAAC,WAAW;AACZ,cAAY,MAAM,OAAO;AACzB,YAAU,MAAM,OAAO;;AAE3B,QAAO;EAAE,MAAM;EAAW,KAAK;EAAU;;AAG7C,IAAI,iBAAsB;;;;AAK1B,eAAsB,wBAAuC;AACzD,KAAI,eACA;CAEJ,MAAM,EAAE,SAAS,MAAM,SAAS;AAChC,kBAAiB,KAAK,aAAa;AACnC,gBAAe,OAAO,EAAE,oBAAoB,UAAU,CAAC;;;;;;AAO3D,eAAsB,mBAAmB,SAAgD;AACrF,KAAI,QAAQ,WAAW,EACnB,cAAa;AAGjB,OAAM,uBAAuB;CAC7B,MAAM,EAAE,QAAQ,MAAM,SAAS;CAG/B,MAAM,yBAAS,IAAI,KAA+B;AAClD,MAAK,MAAM,SAAS,SAAS;EACzB,MAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,MAAM,QAAQ;EACrD,MAAM,WAAW,OAAO,IAAI,IAAI,IAAI,EAAE;AACtC,WAAS,KAAK,MAAM;AACpB,SAAO,IAAI,KAAK,SAAS;;CAG7B,MAAM,WAAkB,EAAE;AAE1B,MAAK,MAAM,GAAG,UAAU,QAAQ;EAC5B,IAAI,QAAQ;EACZ,MAAM,UAAU,MAAM,GAAG;EAEzB,MAAM,UACF,QAAQ,WAAW,MACb,IAAI,KAAK,IACL,QAAQ,eAAe,SAAS,QAAQ,MAAM,QAAQ,KACtD,eACI,aACM,aACA;AACF;IAEP,CACJ,GACA,IAAI,KAAa,QAAQ,OAAO,aAAa,EAC1C,QAAQ,eAAe,SAAS,QAAQ,MAAM,QAAQ,KACtD,eACI,aACM,aACA;AACF;IAEP,CACJ;AAEX,WAAS,KAAK,QAAQ;;AAG1B,gBAAe,IAAI,GAAG,SAAS;AAE/B,cAAa;AACT,iBAAe,eAAe;;CAGlC,SAAS,eAAe,OAAyB,UAAwB,SAAqB;AAC1F,SAAO,OAAO,EAAE,cAAoC;GAChD,MAAM,MAAM,UAAU;AACtB,OAAI,OAAO,MAAM,OAEb;GAGJ,MAAM,QAAQ,MAAM;AAGpB,OAAI,MAAM,QAAQ,OAAO;IACrB,IAAI,OAAgB;AACpB,QAAI;AACA,YAAO,MAAM,QAAQ,OAAO,CAAC,MAAM;YAC/B;AAGR,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,CAC1B;;AAIR,YAAS;AAGT,OAAI,MAAM,SAAS,MACf,OAAM,IAAI,SAAS,MAAM,WAAW,GAAG,MAAM,SAAS,MAAM,CAAC;GAGjE,MAAM,EAAE,iBAAiB,MAAM,SAAS,CAAC,MAAM,MAAM,EAAE,IAAI;AAC3D,UAAO,aAAa,KAAK,MAAM,SAAS,MAAM;IAC1C,QAAQ,MAAM,SAAS,UAAU;IACjC,SAAS,MAAM,SAAS;IAC3B,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/intercept/server.ts
|
|
2
|
+
let mswModule = null;
|
|
3
|
+
let mswHttp = null;
|
|
4
|
+
async function loadMsw() {
|
|
5
|
+
if (!mswModule) {
|
|
6
|
+
mswModule = await import("msw/node");
|
|
7
|
+
mswHttp = await import("msw");
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
node: mswModule,
|
|
11
|
+
msw: mswHttp
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
let serverInstance = null;
|
|
15
|
+
/**
|
|
16
|
+
* Start the MSW server (once per process).
|
|
17
|
+
*/
|
|
18
|
+
async function ensureInterceptServer() {
|
|
19
|
+
if (serverInstance) return;
|
|
20
|
+
const { node } = await loadMsw();
|
|
21
|
+
serverInstance = node.setupServer();
|
|
22
|
+
serverInstance.listen({ onUnhandledRequest: "bypass" });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register intercept entries as MSW handlers. Returns a cleanup function.
|
|
26
|
+
* Handlers are consumed in queue order — each trigger match pops one response.
|
|
27
|
+
*/
|
|
28
|
+
async function registerIntercepts(entries) {
|
|
29
|
+
if (entries.length === 0) return () => {};
|
|
30
|
+
await ensureInterceptServer();
|
|
31
|
+
const { msw } = await loadMsw();
|
|
32
|
+
const queues = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const key = `${entry.trigger.method}:${entry.trigger.url}`;
|
|
35
|
+
const existing = queues.get(key) ?? [];
|
|
36
|
+
existing.push(entry);
|
|
37
|
+
queues.set(key, existing);
|
|
38
|
+
}
|
|
39
|
+
const handlers = [];
|
|
40
|
+
for (const [, queue] of queues) {
|
|
41
|
+
let index = 0;
|
|
42
|
+
const trigger = queue[0].trigger;
|
|
43
|
+
const handler = trigger.method === "*" ? msw.http.all(trigger.url instanceof RegExp ? trigger.url : trigger.url, createResolver(queue, () => index, () => {
|
|
44
|
+
index++;
|
|
45
|
+
})) : msw.http[trigger.method.toLowerCase()](trigger.url instanceof RegExp ? trigger.url : trigger.url, createResolver(queue, () => index, () => {
|
|
46
|
+
index++;
|
|
47
|
+
}));
|
|
48
|
+
handlers.push(handler);
|
|
49
|
+
}
|
|
50
|
+
serverInstance.use(...handlers);
|
|
51
|
+
return () => {
|
|
52
|
+
serverInstance.resetHandlers();
|
|
53
|
+
};
|
|
54
|
+
function createResolver(queue, getIndex, advance) {
|
|
55
|
+
return async ({ request }) => {
|
|
56
|
+
const idx = getIndex();
|
|
57
|
+
if (idx >= queue.length) return;
|
|
58
|
+
const entry = queue[idx];
|
|
59
|
+
if (entry.trigger.match) {
|
|
60
|
+
let body = null;
|
|
61
|
+
try {
|
|
62
|
+
body = await request.clone().json();
|
|
63
|
+
} catch {}
|
|
64
|
+
if (!entry.trigger.match(body)) return;
|
|
65
|
+
}
|
|
66
|
+
advance();
|
|
67
|
+
if (entry.response.delay) await new Promise((r) => setTimeout(r, entry.response.delay));
|
|
68
|
+
const { HttpResponse } = await loadMsw().then((m) => m.msw);
|
|
69
|
+
return HttpResponse.json(entry.response.body, {
|
|
70
|
+
status: entry.response.status ?? 200,
|
|
71
|
+
headers: entry.response.headers
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { registerIntercepts };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/intercept/server.ts"],"sourcesContent":["/**\n * MSW-based intercept server. Manages HTTP interception for spec.run().\n *\n * Intercepts are queued per trigger — the first matching handler is consumed,\n * then the next one in the queue is used for subsequent matching requests.\n */\nimport type { InterceptEntry } from './types.js';\n\nlet mswModule: any = null;\nlet mswHttp: any = null;\n\nasync function loadMsw() {\n if (!mswModule) {\n mswModule = await import('msw/node');\n mswHttp = await import('msw');\n }\n return { node: mswModule, msw: mswHttp! };\n}\n\nlet serverInstance: any = null;\n\n/**\n * Start the MSW server (once per process).\n */\nexport async function ensureInterceptServer(): Promise<void> {\n if (serverInstance) {\n return;\n }\n const { node } = await loadMsw();\n serverInstance = node.setupServer();\n serverInstance.listen({ onUnhandledRequest: 'bypass' });\n}\n\n/**\n * Register intercept entries as MSW handlers. Returns a cleanup function.\n * Handlers are consumed in queue order — each trigger match pops one response.\n */\nexport async function registerIntercepts(entries: InterceptEntry[]): Promise<() => void> {\n if (entries.length === 0) {\n return () => {};\n }\n\n await ensureInterceptServer();\n const { msw } = await loadMsw();\n\n // Build a queue per unique trigger (same method+url+match combo)\n const queues = new Map<string, InterceptEntry[]>();\n for (const entry of entries) {\n const key = `${entry.trigger.method}:${entry.trigger.url}`;\n const existing = queues.get(key) ?? [];\n existing.push(entry);\n queues.set(key, existing);\n }\n\n const handlers: any[] = [];\n\n for (const [, queue] of queues) {\n let index = 0;\n const trigger = queue[0].trigger;\n\n const handler =\n trigger.method === '*'\n ? msw.http.all(\n trigger.url instanceof RegExp ? trigger.url : trigger.url,\n createResolver(\n queue,\n () => index,\n () => {\n index++;\n },\n ),\n )\n : (msw.http as any)[trigger.method.toLowerCase()](\n trigger.url instanceof RegExp ? trigger.url : trigger.url,\n createResolver(\n queue,\n () => index,\n () => {\n index++;\n },\n ),\n );\n\n handlers.push(handler);\n }\n\n serverInstance.use(...handlers);\n\n return () => {\n serverInstance.resetHandlers();\n };\n\n function createResolver(queue: InterceptEntry[], getIndex: () => number, advance: () => void) {\n return async ({ request }: { request: Request }) => {\n const idx = getIndex();\n if (idx >= queue.length) {\n // Queue exhausted — passthrough\n return undefined;\n }\n\n const entry = queue[idx];\n\n // Check body matcher if present\n if (entry.trigger.match) {\n let body: unknown = null;\n try {\n body = await request.clone().json();\n } catch {\n // Not JSON — skip match\n }\n if (!entry.trigger.match(body)) {\n return undefined;\n }\n }\n\n advance();\n\n // Apply delay if specified\n if (entry.response.delay) {\n await new Promise((r) => setTimeout(r, entry.response.delay));\n }\n\n const { HttpResponse } = await loadMsw().then((m) => m.msw);\n return HttpResponse.json(entry.response.body, {\n status: entry.response.status ?? 200,\n headers: entry.response.headers,\n });\n };\n }\n}\n\n/**\n * Stop the MSW server (call in afterAll).\n */\nexport async function stopInterceptServer(): Promise<void> {\n if (serverInstance) {\n serverInstance.close();\n serverInstance = null;\n }\n}\n"],"mappings":";AAQA,IAAI,YAAiB;AACrB,IAAI,UAAe;AAEnB,eAAe,UAAU;AACrB,KAAI,CAAC,WAAW;AACZ,cAAY,MAAM,OAAO;AACzB,YAAU,MAAM,OAAO;;AAE3B,QAAO;EAAE,MAAM;EAAW,KAAK;EAAU;;AAG7C,IAAI,iBAAsB;;;;AAK1B,eAAsB,wBAAuC;AACzD,KAAI,eACA;CAEJ,MAAM,EAAE,SAAS,MAAM,SAAS;AAChC,kBAAiB,KAAK,aAAa;AACnC,gBAAe,OAAO,EAAE,oBAAoB,UAAU,CAAC;;;;;;AAO3D,eAAsB,mBAAmB,SAAgD;AACrF,KAAI,QAAQ,WAAW,EACnB,cAAa;AAGjB,OAAM,uBAAuB;CAC7B,MAAM,EAAE,QAAQ,MAAM,SAAS;CAG/B,MAAM,yBAAS,IAAI,KAA+B;AAClD,MAAK,MAAM,SAAS,SAAS;EACzB,MAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,MAAM,QAAQ;EACrD,MAAM,WAAW,OAAO,IAAI,IAAI,IAAI,EAAE;AACtC,WAAS,KAAK,MAAM;AACpB,SAAO,IAAI,KAAK,SAAS;;CAG7B,MAAM,WAAkB,EAAE;AAE1B,MAAK,MAAM,GAAG,UAAU,QAAQ;EAC5B,IAAI,QAAQ;EACZ,MAAM,UAAU,MAAM,GAAG;EAEzB,MAAM,UACF,QAAQ,WAAW,MACb,IAAI,KAAK,IACL,QAAQ,eAAe,SAAS,QAAQ,MAAM,QAAQ,KACtD,eACI,aACM,aACA;AACF;IAEP,CACJ,GACA,IAAI,KAAa,QAAQ,OAAO,aAAa,EAC1C,QAAQ,eAAe,SAAS,QAAQ,MAAM,QAAQ,KACtD,eACI,aACM,aACA;AACF;IAEP,CACJ;AAEX,WAAS,KAAK,QAAQ;;AAG1B,gBAAe,IAAI,GAAG,SAAS;AAE/B,cAAa;AACT,iBAAe,eAAe;;CAGlC,SAAS,eAAe,OAAyB,UAAwB,SAAqB;AAC1F,SAAO,OAAO,EAAE,cAAoC;GAChD,MAAM,MAAM,UAAU;AACtB,OAAI,OAAO,MAAM,OAEb;GAGJ,MAAM,QAAQ,MAAM;AAGpB,OAAI,MAAM,QAAQ,OAAO;IACrB,IAAI,OAAgB;AACpB,QAAI;AACA,YAAO,MAAM,QAAQ,OAAO,CAAC,MAAM;YAC/B;AAGR,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,CAC1B;;AAIR,YAAS;AAGT,OAAI,MAAM,SAAS,MACf,OAAM,IAAI,SAAS,MAAM,WAAW,GAAG,MAAM,SAAS,MAAM,CAAC;GAGjE,MAAM,EAAE,iBAAiB,MAAM,SAAS,CAAC,MAAM,MAAM,EAAE,IAAI;AAC3D,UAAO,aAAa,KAAK,MAAM,SAAS,MAAM;IAC1C,QAAQ,MAAM,SAAS,UAAU;IACjC,SAAS,MAAM,SAAS;IAC3B,CAAC"}
|
package/dist/sqlite.adapter.cjs
CHANGED
|
@@ -227,22 +227,41 @@ var SqliteHandle = class {
|
|
|
227
227
|
}
|
|
228
228
|
async healthcheck() {}
|
|
229
229
|
async initialize() {
|
|
230
|
-
this.templatePath = (0, node_path.resolve)((0, node_os.tmpdir)(),
|
|
230
|
+
this.templatePath = (0, node_path.resolve)((0, node_os.tmpdir)(), "jterrazz-test-sqlite-template.sqlite");
|
|
231
|
+
const lockPath = `${this.templatePath}.lock`;
|
|
232
|
+
if ((0, node_fs.existsSync)(lockPath)) {
|
|
233
|
+
const start = Date.now();
|
|
234
|
+
while ((0, node_fs.existsSync)(lockPath) && Date.now() - start < 3e4) await new Promise((r) => setTimeout(r, 100));
|
|
235
|
+
}
|
|
236
|
+
if ((0, node_fs.existsSync)(this.templatePath)) {
|
|
237
|
+
this.connectionString = `file:${this.templatePath}`;
|
|
238
|
+
this.started = true;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const { writeFileSync } = await import("node:fs");
|
|
242
|
+
writeFileSync(lockPath, process.pid.toString());
|
|
231
243
|
if (this.prismaSchema) {
|
|
232
244
|
const { execSync } = await import("node:child_process");
|
|
233
|
-
execSync("npx prisma db push --force-reset
|
|
245
|
+
execSync("npx prisma db push --force-reset", {
|
|
234
246
|
env: {
|
|
235
247
|
...process.env,
|
|
236
|
-
DATABASE_URL: `file:${this.templatePath}
|
|
248
|
+
DATABASE_URL: `file:${this.templatePath}`,
|
|
249
|
+
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: "yes"
|
|
237
250
|
},
|
|
238
251
|
stdio: "pipe"
|
|
239
252
|
});
|
|
253
|
+
const tmpDb = new better_sqlite3.default(this.templatePath);
|
|
254
|
+
tmpDb.pragma("wal_checkpoint(TRUNCATE)");
|
|
255
|
+
tmpDb.close();
|
|
240
256
|
} else if (this.initSql) {
|
|
241
257
|
const sql = (0, node_fs.readFileSync)(this.initSql, "utf8");
|
|
242
258
|
const templateDb = new better_sqlite3.default(this.templatePath);
|
|
243
259
|
templateDb.exec(sql);
|
|
244
260
|
templateDb.close();
|
|
245
261
|
} else new better_sqlite3.default(this.templatePath).close();
|
|
262
|
+
try {
|
|
263
|
+
(0, node_fs.unlinkSync)(lockPath);
|
|
264
|
+
} catch {}
|
|
246
265
|
this.connectionString = `file:${this.templatePath}`;
|
|
247
266
|
this.started = true;
|
|
248
267
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.adapter.cjs","names":["Client","Database"],"sources":["../src/adapters/postgres.adapter.ts","../src/adapters/redis.adapter.ts","../src/adapters/sqlite.adapter.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Create the template database\n this.templatePath = resolve(tmpdir(), `test-template-${Date.now()}.sqlite`);\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset --skip-generate', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n },\n stdio: 'pipe',\n });\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sqlite.adapter.cjs","names":["Client","Database"],"sources":["../src/adapters/postgres.adapter.ts","../src/adapters/redis.adapter.ts","../src/adapters/sqlite.adapter.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Each test run gets a fresh template; workers share it via lock\n this.templatePath = resolve(tmpdir(), 'jterrazz-test-sqlite-template.sqlite');\n const lockPath = `${this.templatePath}.lock`;\n\n if (existsSync(lockPath)) {\n // Another worker is creating it — wait for it\n const start = Date.now();\n while (existsSync(lockPath) && Date.now() - start < 30_000) {\n await new Promise((r) => setTimeout(r, 100));\n }\n }\n\n if (existsSync(this.templatePath)) {\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n return;\n }\n\n // Acquire lock\n const { writeFileSync } = await import('node:fs');\n writeFileSync(lockPath, process.pid.toString());\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes',\n },\n stdio: 'pipe',\n });\n\n // Checkpoint WAL so the template is a single file (safe to copy)\n const tmpDb = new Database(this.templatePath);\n tmpDb.pragma('wal_checkpoint(TRUNCATE)');\n tmpDb.close();\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n // Release lock\n try {\n unlinkSync(lockPath);\n } catch {\n /* Ignore */\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
|
package/dist/sqlite.adapter.js
CHANGED
|
@@ -225,22 +225,41 @@ var SqliteHandle = class {
|
|
|
225
225
|
}
|
|
226
226
|
async healthcheck() {}
|
|
227
227
|
async initialize() {
|
|
228
|
-
this.templatePath = resolve(tmpdir(),
|
|
228
|
+
this.templatePath = resolve(tmpdir(), "jterrazz-test-sqlite-template.sqlite");
|
|
229
|
+
const lockPath = `${this.templatePath}.lock`;
|
|
230
|
+
if (existsSync(lockPath)) {
|
|
231
|
+
const start = Date.now();
|
|
232
|
+
while (existsSync(lockPath) && Date.now() - start < 3e4) await new Promise((r) => setTimeout(r, 100));
|
|
233
|
+
}
|
|
234
|
+
if (existsSync(this.templatePath)) {
|
|
235
|
+
this.connectionString = `file:${this.templatePath}`;
|
|
236
|
+
this.started = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const { writeFileSync } = await import("node:fs");
|
|
240
|
+
writeFileSync(lockPath, process.pid.toString());
|
|
229
241
|
if (this.prismaSchema) {
|
|
230
242
|
const { execSync } = await import("node:child_process");
|
|
231
|
-
execSync("npx prisma db push --force-reset
|
|
243
|
+
execSync("npx prisma db push --force-reset", {
|
|
232
244
|
env: {
|
|
233
245
|
...process.env,
|
|
234
|
-
DATABASE_URL: `file:${this.templatePath}
|
|
246
|
+
DATABASE_URL: `file:${this.templatePath}`,
|
|
247
|
+
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: "yes"
|
|
235
248
|
},
|
|
236
249
|
stdio: "pipe"
|
|
237
250
|
});
|
|
251
|
+
const tmpDb = new Database(this.templatePath);
|
|
252
|
+
tmpDb.pragma("wal_checkpoint(TRUNCATE)");
|
|
253
|
+
tmpDb.close();
|
|
238
254
|
} else if (this.initSql) {
|
|
239
255
|
const sql = readFileSync(this.initSql, "utf8");
|
|
240
256
|
const templateDb = new Database(this.templatePath);
|
|
241
257
|
templateDb.exec(sql);
|
|
242
258
|
templateDb.close();
|
|
243
259
|
} else new Database(this.templatePath).close();
|
|
260
|
+
try {
|
|
261
|
+
unlinkSync(lockPath);
|
|
262
|
+
} catch {}
|
|
244
263
|
this.connectionString = `file:${this.templatePath}`;
|
|
245
264
|
this.started = true;
|
|
246
265
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.adapter.js","names":[],"sources":["../src/adapters/postgres.adapter.ts","../src/adapters/redis.adapter.ts","../src/adapters/sqlite.adapter.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Create the template database\n this.templatePath = resolve(tmpdir(), `test-template-${Date.now()}.sqlite`);\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset --skip-generate', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n },\n stdio: 'pipe',\n });\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sqlite.adapter.js","names":[],"sources":["../src/adapters/postgres.adapter.ts","../src/adapters/redis.adapter.ts","../src/adapters/sqlite.adapter.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../ports/database.port.js';\nimport type { IsolationStrategy } from '../ports/isolation.port.js';\nimport type { ServiceHandle } from '../ports/service.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Each test run gets a fresh template; workers share it via lock\n this.templatePath = resolve(tmpdir(), 'jterrazz-test-sqlite-template.sqlite');\n const lockPath = `${this.templatePath}.lock`;\n\n if (existsSync(lockPath)) {\n // Another worker is creating it — wait for it\n const start = Date.now();\n while (existsSync(lockPath) && Date.now() - start < 30_000) {\n await new Promise((r) => setTimeout(r, 100));\n }\n }\n\n if (existsSync(this.templatePath)) {\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n return;\n }\n\n // Acquire lock\n const { writeFileSync } = await import('node:fs');\n writeFileSync(lockPath, process.pid.toString());\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes',\n },\n stdio: 'pipe',\n });\n\n // Checkpoint WAL so the template is a single file (safe to copy)\n const tmpDb = new Database(this.templatePath);\n tmpDb.pragma('wal_checkpoint(TRUNCATE)');\n tmpDb.close();\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n // Release lock\n try {\n unlinkSync(lockPath);\n } catch {\n /* Ignore */\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
|
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/intercept/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* An intercept trigger describes which HTTP request to match.
|
|
4
|
+
*/
|
|
5
|
+
interface InterceptTrigger {
|
|
6
|
+
/** HTTP method to match. */
|
|
7
|
+
method: string;
|
|
8
|
+
/** URL pattern to match (string for exact prefix, RegExp for pattern). */
|
|
9
|
+
url: RegExp | string;
|
|
10
|
+
/** Optional request body matcher — the handler only fires if this returns true. */
|
|
11
|
+
match?: (body: unknown) => boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An intercept response describes what to return when the trigger matches.
|
|
15
|
+
*/
|
|
16
|
+
interface InterceptResponse {
|
|
17
|
+
/** HTTP status code (default: 200). */
|
|
18
|
+
status?: number;
|
|
19
|
+
/** Response body (will be JSON.stringified). */
|
|
20
|
+
body: unknown;
|
|
21
|
+
/** Response headers. */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
/** Delay in ms before responding (for timeout testing). */
|
|
24
|
+
delay?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A fully resolved intercept entry ready to be registered with MSW.
|
|
28
|
+
*/
|
|
29
|
+
interface InterceptEntry {
|
|
30
|
+
trigger: InterceptTrigger;
|
|
31
|
+
response: InterceptResponse;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { InterceptResponse as n, InterceptTrigger as r, InterceptEntry as t };
|
|
35
|
+
//# sourceMappingURL=types.d.cts.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/intercept/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* An intercept trigger describes which HTTP request to match.
|
|
4
|
+
*/
|
|
5
|
+
interface InterceptTrigger {
|
|
6
|
+
/** HTTP method to match. */
|
|
7
|
+
method: string;
|
|
8
|
+
/** URL pattern to match (string for exact prefix, RegExp for pattern). */
|
|
9
|
+
url: RegExp | string;
|
|
10
|
+
/** Optional request body matcher — the handler only fires if this returns true. */
|
|
11
|
+
match?: (body: unknown) => boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An intercept response describes what to return when the trigger matches.
|
|
15
|
+
*/
|
|
16
|
+
interface InterceptResponse {
|
|
17
|
+
/** HTTP status code (default: 200). */
|
|
18
|
+
status?: number;
|
|
19
|
+
/** Response body (will be JSON.stringified). */
|
|
20
|
+
body: unknown;
|
|
21
|
+
/** Response headers. */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
/** Delay in ms before responding (for timeout testing). */
|
|
24
|
+
delay?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A fully resolved intercept entry ready to be registered with MSW.
|
|
28
|
+
*/
|
|
29
|
+
interface InterceptEntry {
|
|
30
|
+
trigger: InterceptTrigger;
|
|
31
|
+
response: InterceptResponse;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { InterceptResponse as n, InterceptTrigger as r, InterceptEntry as t };
|
|
35
|
+
//# sourceMappingURL=types.d.ts.map
|