@stratal/testing 0.0.0-canary-ccb3f17 → 0.0.0-canary-e5681b8
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/CHANGELOG.md +603 -0
- package/README.md +137 -36
- package/dist/database/index.d.mts +2 -2
- package/dist/database/index.mjs +2 -2
- package/dist/{database-DeMI4Igi.mjs → database-Rojs69r3.mjs} +112 -60
- package/dist/database-Rojs69r3.mjs.map +1 -0
- package/dist/{decorate-ZDdbRmsv.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/feature-flags/index.mjs +1 -1
- package/dist/{feature-flags-BMYurC1B.mjs → feature-flags-CZ1a4M2g.mjs} +2 -2
- package/dist/{feature-flags-BMYurC1B.mjs.map → feature-flags-CZ1a4M2g.mjs.map} +1 -1
- package/dist/{index-DUScdrKC.d.mts → index-B8Mw1Dxk.d.mts} +46 -33
- package/dist/index-B8Mw1Dxk.d.mts.map +1 -0
- package/dist/{index-Dm6e2N61.d.mts → index-D7FM6EKp.d.mts} +21 -2
- package/dist/index-D7FM6EKp.d.mts.map +1 -0
- package/dist/index.d.mts +55 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +67 -61
- package/dist/index.mjs.map +1 -1
- package/dist/mocks/cloudflare-workers.d.mts +6 -7
- package/dist/mocks/cloudflare-workers.d.mts.map +1 -1
- package/dist/mocks/index.d.mts +1 -1
- package/dist/mocks/index.mjs +1 -1
- package/dist/mocks/noop-rate-limiter-store.d.mts +1 -2
- package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -1
- package/dist/mocks/zenstack-language.d.mts +21 -21
- package/dist/mocks/zenstack-language.d.mts.map +1 -1
- package/dist/mocks/zenstack-language.mjs +1 -1
- package/dist/mocks/zenstack-language.mjs.map +1 -1
- package/dist/storage/index.d.mts +1 -1
- package/dist/storage/index.mjs +1 -1
- package/dist/{storage-q8RFahgU.mjs → storage-BiGamMA8.mjs} +70 -3
- package/dist/storage-BiGamMA8.mjs.map +1 -0
- package/dist/{test-worker-exports-DMct0Egv.d.mts → test-worker-exports-dCRARYEc.d.mts} +8 -7
- package/dist/test-worker-exports-dCRARYEc.d.mts.map +1 -0
- package/dist/{test-workers-cache-eiGMw9Yj.mjs → test-workers-cache-D02Pt_dE.mjs} +8 -7
- package/dist/{test-workers-cache-eiGMw9Yj.mjs.map → test-workers-cache-D02Pt_dE.mjs.map} +1 -1
- package/dist/vitest-plugin/index.d.mts +9 -9
- package/dist/vitest-plugin/index.d.mts.map +1 -1
- package/dist/vitest-plugin/index.mjs +22 -15
- package/dist/vitest-plugin/index.mjs.map +1 -1
- package/package.json +22 -19
- package/dist/database-DeMI4Igi.mjs.map +0 -1
- package/dist/index-DUScdrKC.d.mts.map +0 -1
- package/dist/index-Dm6e2N61.d.mts.map +0 -1
- package/dist/storage-q8RFahgU.mjs.map +0 -1
- package/dist/test-worker-exports-DMct0Egv.d.mts.map +0 -1
|
@@ -6,32 +6,32 @@
|
|
|
6
6
|
* incompatible with Cloudflare Workers runtime. These exports are only used
|
|
7
7
|
* for schema generation (CLI functionality), not runtime authentication.
|
|
8
8
|
*
|
|
9
|
-
* This mock provides stub implementations to allow tests to run in
|
|
9
|
+
* This mock provides stub implementations to allow tests to run in the Workers Vitest integration.
|
|
10
10
|
*/
|
|
11
|
-
declare const formatDocument: (content: string) => string;
|
|
12
|
-
declare const loadDocument: (_path: string) => {
|
|
11
|
+
export declare const formatDocument: (content: string) => string;
|
|
12
|
+
export declare const loadDocument: (_path: string) => {
|
|
13
13
|
success: boolean;
|
|
14
14
|
errors: string[];
|
|
15
15
|
};
|
|
16
|
-
declare class ZModelCodeGenerator {
|
|
16
|
+
export declare class ZModelCodeGenerator {
|
|
17
17
|
generate(_model: unknown): string;
|
|
18
18
|
}
|
|
19
|
-
declare const isDataModel: (_item: unknown) => boolean;
|
|
20
|
-
declare const isAttribute: (_item: unknown) => boolean;
|
|
21
|
-
declare const isAbstractDeclaration: (_item: unknown) => boolean;
|
|
22
|
-
declare const isExpression: (_item: unknown) => boolean;
|
|
23
|
-
declare const isLiteralExpr: (_item: unknown) => boolean;
|
|
24
|
-
declare const isArrayExpr: (_item: unknown) => boolean;
|
|
25
|
-
declare const isAttributeArg: (_item: unknown) => boolean;
|
|
26
|
-
declare const isBinaryExpr: (_item: unknown) => boolean;
|
|
27
|
-
declare const isBooleanLiteral: (_item: unknown) => boolean;
|
|
28
|
-
declare const isConfigArrayExpr: (_item: unknown) => boolean;
|
|
29
|
-
declare const isConfigField: (_item: unknown) => boolean;
|
|
30
|
-
declare const isDataField: (_item: unknown) => boolean;
|
|
31
|
-
declare const isDataFieldAttribute: (_item: unknown) => boolean;
|
|
32
|
-
declare const isDataFieldType: (_item: unknown) => boolean;
|
|
33
|
-
declare const isDataModelAttribute: (_item: unknown) => boolean;
|
|
34
|
-
declare const hasAttribute: (_node: unknown, _name?: string) => boolean;
|
|
19
|
+
export declare const isDataModel: (_item: unknown) => boolean;
|
|
20
|
+
export declare const isAttribute: (_item: unknown) => boolean;
|
|
21
|
+
export declare const isAbstractDeclaration: (_item: unknown) => boolean;
|
|
22
|
+
export declare const isExpression: (_item: unknown) => boolean;
|
|
23
|
+
export declare const isLiteralExpr: (_item: unknown) => boolean;
|
|
24
|
+
export declare const isArrayExpr: (_item: unknown) => boolean;
|
|
25
|
+
export declare const isAttributeArg: (_item: unknown) => boolean;
|
|
26
|
+
export declare const isBinaryExpr: (_item: unknown) => boolean;
|
|
27
|
+
export declare const isBooleanLiteral: (_item: unknown) => boolean;
|
|
28
|
+
export declare const isConfigArrayExpr: (_item: unknown) => boolean;
|
|
29
|
+
export declare const isConfigField: (_item: unknown) => boolean;
|
|
30
|
+
export declare const isDataField: (_item: unknown) => boolean;
|
|
31
|
+
export declare const isDataFieldAttribute: (_item: unknown) => boolean;
|
|
32
|
+
export declare const isDataFieldType: (_item: unknown) => boolean;
|
|
33
|
+
export declare const isDataModelAttribute: (_item: unknown) => boolean;
|
|
34
|
+
export declare const hasAttribute: (_node: unknown, _name?: string) => boolean;
|
|
35
35
|
declare const _default: {
|
|
36
36
|
formatDocument: typeof formatDocument;
|
|
37
37
|
loadDocument: typeof loadDocument;
|
|
@@ -41,5 +41,5 @@ declare const _default: {
|
|
|
41
41
|
hasAttribute: typeof hasAttribute;
|
|
42
42
|
};
|
|
43
43
|
//#endregion
|
|
44
|
-
export {
|
|
44
|
+
export { _default as default };
|
|
45
45
|
//# sourceMappingURL=zenstack-language.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zenstack-language.d.mts","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"zenstack-language.d.mts","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"mappings":";;;;;;;;;;qBAWa,iBAAc;qBAEd,eAAY;EACxB;EACA;;qBAGY;EACZ,SAAS;;qBAMG,cAAW;qBACX,cAAW;qBACX,wBAAqB;qBACrB,eAAY;qBACZ,gBAAa;qBACb,cAAW;qBACX,iBAAc;qBACd,eAAY;qBACZ,mBAAgB;qBAChB,oBAAiB;qBACjB,gBAAa;qBACb,cAAW;qBACX,uBAAoB;qBACpB,kBAAe;qBACf,uBAAoB;qBAGpB,eAAY,gBAAkB"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* incompatible with Cloudflare Workers runtime. These exports are only used
|
|
7
7
|
* for schema generation (CLI functionality), not runtime authentication.
|
|
8
8
|
*
|
|
9
|
-
* This mock provides stub implementations to allow tests to run in
|
|
9
|
+
* This mock provides stub implementations to allow tests to run in the Workers Vitest integration.
|
|
10
10
|
*/
|
|
11
11
|
const formatDocument = (content) => content;
|
|
12
12
|
const loadDocument = (_path) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zenstack-language.mjs","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"sourcesContent":["/**\n * Mock for @zenstackhq/language package\n *\n * The @zenstackhq/language package depends on vscode-languageserver which is\n * incompatible with Cloudflare Workers runtime. These exports are only used\n * for schema generation (CLI functionality), not runtime authentication.\n *\n * This mock provides stub implementations to allow tests to run in
|
|
1
|
+
{"version":3,"file":"zenstack-language.mjs","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"sourcesContent":["/**\n * Mock for @zenstackhq/language package\n *\n * The @zenstackhq/language package depends on vscode-languageserver which is\n * incompatible with Cloudflare Workers runtime. These exports are only used\n * for schema generation (CLI functionality), not runtime authentication.\n *\n * This mock provides stub implementations to allow tests to run in the Workers Vitest integration.\n */\n\n// Main exports from @zenstackhq/language\nexport const formatDocument = (content: string) => content\n\nexport const loadDocument = (_path: string) => ({\n\tsuccess: false,\n\terrors: ['Mock: loadDocument not available in test environment'],\n})\n\nexport class ZModelCodeGenerator {\n\tgenerate(_model: unknown): string {\n\t\treturn ''\n\t}\n}\n\n// AST exports from @zenstackhq/language/ast\nexport const isDataModel = (_item: unknown): boolean => false\nexport const isAttribute = (_item: unknown): boolean => false\nexport const isAbstractDeclaration = (_item: unknown): boolean => false\nexport const isExpression = (_item: unknown): boolean => false\nexport const isLiteralExpr = (_item: unknown): boolean => false\nexport const isArrayExpr = (_item: unknown): boolean => false\nexport const isAttributeArg = (_item: unknown): boolean => false\nexport const isBinaryExpr = (_item: unknown): boolean => false\nexport const isBooleanLiteral = (_item: unknown): boolean => false\nexport const isConfigArrayExpr = (_item: unknown): boolean => false\nexport const isConfigField = (_item: unknown): boolean => false\nexport const isDataField = (_item: unknown): boolean => false\nexport const isDataFieldAttribute = (_item: unknown): boolean => false\nexport const isDataFieldType = (_item: unknown): boolean => false\nexport const isDataModelAttribute = (_item: unknown): boolean => false\n\n// Utils exports from @zenstackhq/language/utils\nexport const hasAttribute = (_node: unknown, _name?: string): boolean => false\n\nexport default {\n\tformatDocument,\n\tloadDocument,\n\tZModelCodeGenerator,\n\tisDataModel,\n\tisAttribute,\n\thasAttribute,\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAa,kBAAkB,YAAoB;AAEnD,MAAa,gBAAgB,WAAmB;CAC/C,SAAS;CACT,QAAQ,CAAC,sDAAsD;AAChE;AAEA,IAAa,sBAAb,MAAiC;CAChC,SAAS,QAAyB;EACjC,OAAO;CACR;AACD;AAGA,MAAa,eAAe,UAA4B;AACxD,MAAa,eAAe,UAA4B;AACxD,MAAa,yBAAyB,UAA4B;AAClE,MAAa,gBAAgB,UAA4B;AACzD,MAAa,iBAAiB,UAA4B;AAC1D,MAAa,eAAe,UAA4B;AACxD,MAAa,kBAAkB,UAA4B;AAC3D,MAAa,gBAAgB,UAA4B;AACzD,MAAa,oBAAoB,UAA4B;AAC7D,MAAa,qBAAqB,UAA4B;AAC9D,MAAa,iBAAiB,UAA4B;AAC1D,MAAa,eAAe,UAA4B;AACxD,MAAa,wBAAwB,UAA4B;AACjE,MAAa,mBAAmB,UAA4B;AAC5D,MAAa,wBAAwB,UAA4B;AAGjE,MAAa,gBAAgB,OAAgB,UAA4B;AAEzE,IAAA,4BAAe;CACd;CACA;CACA;CACA;CACA;CACA;AACD"}
|
package/dist/storage/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as StoredFile, t as FakeStorageService } from "../index-
|
|
1
|
+
import { n as StoredFile, t as FakeStorageService } from "../index-D7FM6EKp.mjs";
|
|
2
2
|
export { FakeStorageService, type StoredFile };
|
package/dist/storage/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as FakeStorageService } from "../storage-
|
|
1
|
+
import { t as FakeStorageService } from "../storage-BiGamMA8.mjs";
|
|
2
2
|
export { FakeStorageService };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as __decorate } from "./decorate-
|
|
1
|
+
import { t as __decorate } from "./decorate-RQD1h28J.mjs";
|
|
2
2
|
import { FileNotFoundError, STORAGE_TOKENS, StorageService } from "stratal/storage";
|
|
3
3
|
import { Transient, inject } from "stratal/di";
|
|
4
4
|
import { expect } from "vitest";
|
|
5
|
-
//#region \0@oxc-project+runtime@0.
|
|
5
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
|
|
6
6
|
function __decorateParam(paramIndex, decorator) {
|
|
7
7
|
return function(target, key) {
|
|
8
8
|
decorator(target, key, paramIndex);
|
|
@@ -10,6 +10,23 @@ function __decorateParam(paramIndex, decorator) {
|
|
|
10
10
|
}
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/storage/fake-storage.service.ts
|
|
13
|
+
/**
|
|
14
|
+
* Objects per `list` page when a test does not ask for a size.
|
|
15
|
+
*
|
|
16
|
+
* One, so that any listing of two or more objects truncates and a caller who skips the `cursor`
|
|
17
|
+
* loop fails here rather than against a real bucket.
|
|
18
|
+
*/
|
|
19
|
+
const FAKE_LIST_PAGE_SIZE = 1;
|
|
20
|
+
/** Shared by `head` and `list` so a listed object and a headed one cannot drift apart. */
|
|
21
|
+
function toHeadResult(path, file) {
|
|
22
|
+
return {
|
|
23
|
+
path,
|
|
24
|
+
size: file.size,
|
|
25
|
+
contentType: file.mimeType,
|
|
26
|
+
uploadedAt: file.uploadedAt,
|
|
27
|
+
metadata: file.metadata
|
|
28
|
+
};
|
|
29
|
+
}
|
|
13
30
|
let FakeStorageService = class FakeStorageService extends StorageService {
|
|
14
31
|
storageManager;
|
|
15
32
|
options;
|
|
@@ -73,6 +90,56 @@ let FakeStorageService = class FakeStorageService extends StorageService {
|
|
|
73
90
|
return Promise.resolve(this.files.has(path));
|
|
74
91
|
}
|
|
75
92
|
/**
|
|
93
|
+
* Delete many files from fake storage
|
|
94
|
+
*/
|
|
95
|
+
async deleteMany(paths, disk) {
|
|
96
|
+
this.resolveDisk(disk);
|
|
97
|
+
for (const path of paths) this.files.delete(path);
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read a file's metadata from fake storage, without its contents
|
|
102
|
+
*/
|
|
103
|
+
async head(path, disk) {
|
|
104
|
+
this.resolveDisk(disk);
|
|
105
|
+
const file = this.files.get(path);
|
|
106
|
+
if (!file) return Promise.resolve(null);
|
|
107
|
+
return Promise.resolve(toHeadResult(path, file));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* List files in fake storage, one page at a time
|
|
111
|
+
*
|
|
112
|
+
* Truncates after ONE object unless `limit` says otherwise. Real R2 pages at 1000, but no
|
|
113
|
+
* fixture stores 1000 files — matching that number would return everything in one page and let
|
|
114
|
+
* a caller who never follows `cursor` pass every test, then undercount against a real bucket.
|
|
115
|
+
* Paging at one means any listing of two or more objects exercises the loop.
|
|
116
|
+
*
|
|
117
|
+
* Pass `limit` to widen the page when a test is asserting page contents rather than the loop.
|
|
118
|
+
*/
|
|
119
|
+
async list(options = {}, disk) {
|
|
120
|
+
this.resolveDisk(disk);
|
|
121
|
+
const prefix = options.prefix ?? "";
|
|
122
|
+
const matching = Array.from(this.files.keys()).filter((path) => path.startsWith(prefix)).sort();
|
|
123
|
+
const start = options.cursor === void 0 ? 0 : matching.indexOf(options.cursor);
|
|
124
|
+
if (start < 0) return Promise.reject(/* @__PURE__ */ new Error(`Unknown storage list cursor: ${options.cursor ?? ""}`));
|
|
125
|
+
const limit = options.limit ?? FAKE_LIST_PAGE_SIZE;
|
|
126
|
+
const page = matching.slice(start, start + limit);
|
|
127
|
+
const next = matching[start + limit];
|
|
128
|
+
return Promise.resolve({
|
|
129
|
+
objects: page.map((path) => {
|
|
130
|
+
const object = toHeadResult(path, this.files.get(path));
|
|
131
|
+
if (options.includeMetadata) return object;
|
|
132
|
+
return {
|
|
133
|
+
...object,
|
|
134
|
+
contentType: void 0,
|
|
135
|
+
metadata: void 0
|
|
136
|
+
};
|
|
137
|
+
}),
|
|
138
|
+
truncated: next !== void 0,
|
|
139
|
+
cursor: next
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
76
143
|
* Generate a fake presigned download URL
|
|
77
144
|
*/
|
|
78
145
|
getPresignedDownloadUrl(path, expiresIn) {
|
|
@@ -191,4 +258,4 @@ FakeStorageService = __decorate([
|
|
|
191
258
|
//#endregion
|
|
192
259
|
export { FakeStorageService as t };
|
|
193
260
|
|
|
194
|
-
//# sourceMappingURL=storage-
|
|
261
|
+
//# sourceMappingURL=storage-BiGamMA8.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-BiGamMA8.mjs","names":[],"sources":["../src/storage/fake-storage.service.ts"],"sourcesContent":["import { Transient, inject } from 'stratal/di'\nimport {\n FileNotFoundError,\n STORAGE_TOKENS,\n type StorageManagerService,\n StorageService,\n type StreamingBlobPayloadInputTypes,\n type DownloadResult,\n type HeadResult,\n type ListOptions,\n type ListResult,\n type PresignedUrlResult,\n type StorageConfig,\n type UploadOptions,\n type UploadResult,\n} from 'stratal/storage'\nimport { expect } from 'vitest'\n\n/**\n * Objects per `list` page when a test does not ask for a size.\n *\n * One, so that any listing of two or more objects truncates and a caller who skips the `cursor`\n * loop fails here rather than against a real bucket.\n */\nconst FAKE_LIST_PAGE_SIZE = 1\n\n/** Shared by `head` and `list` so a listed object and a headed one cannot drift apart. */\nfunction toHeadResult(path: string, file: StoredFile): HeadResult {\n return {\n path,\n size: file.size,\n contentType: file.mimeType,\n uploadedAt: file.uploadedAt,\n metadata: file.metadata,\n }\n}\n\n/**\n * Stored file representation in memory\n */\nexport interface StoredFile {\n content: Uint8Array\n mimeType: string\n size: number\n metadata?: Record<string, string>\n uploadedAt: Date\n}\n\n/**\n * FakeStorageService\n *\n * In-memory storage implementation for testing.\n * Registered by default in TestingModuleBuilder.\n *\n * Similar to Laravel's Storage::fake() - stores files in memory\n * and provides assertion helpers for testing.\n *\n * @example\n * ```typescript\n * // Access via TestingModule\n * module.storage.assertExists('path/to/file.pdf')\n * module.storage.assertMissing('deleted/file.pdf')\n * module.storage.clear() // Reset between tests\n * ```\n */\n@Transient(STORAGE_TOKENS.StorageService)\nexport class FakeStorageService extends StorageService {\n private files = new Map<string, StoredFile>()\n\n constructor(\n @inject(STORAGE_TOKENS.StorageManager)\n protected readonly storageManager: StorageManagerService,\n @inject(STORAGE_TOKENS.Options)\n protected readonly options: StorageConfig\n ) {\n super(storageManager, options)\n }\n\n /**\n * Upload content to fake storage\n */\n async upload(\n body: StreamingBlobPayloadInputTypes,\n relativePath: string,\n options: UploadOptions,\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const diskName = this.resolveDisk(disk)\n\n this.files.set(relativePath, {\n content,\n mimeType: options.mimeType ?? 'application/octet-stream',\n size: options.size,\n metadata: options.metadata,\n uploadedAt: new Date(),\n })\n\n return {\n path: relativePath,\n disk: diskName,\n fullPath: relativePath,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n /**\n * Download a file from fake storage\n */\n download(path: string): Promise<DownloadResult> {\n const file = this.files.get(path)\n\n if (!file) {\n return Promise.reject(new FileNotFoundError(path))\n }\n\n return Promise.resolve({\n toStream: () => new ReadableStream({\n start(controller) {\n controller.enqueue(file.content)\n controller.close()\n },\n }),\n toString: () => Promise.resolve(new TextDecoder().decode(file.content)),\n toArrayBuffer: () => Promise.resolve(file.content),\n contentType: file.mimeType,\n size: file.size,\n metadata: file.metadata,\n })\n }\n\n /**\n * Delete a file from fake storage\n */\n delete(path: string): Promise<void> {\n this.files.delete(path)\n return Promise.resolve()\n }\n\n /**\n * Check if a file exists in fake storage\n */\n exists(path: string): Promise<boolean> {\n return Promise.resolve(this.files.has(path))\n }\n\n /**\n * Delete many files from fake storage\n */\n // `async` so an unconfigured disk arrives as a rejected promise, as it does from the real\n // service. Thrown synchronously it would slip past a caller that chains `.catch()` without\n // awaiting — a difference in error surface is still a difference the fake exists to avoid.\n async deleteMany(paths: string[], disk?: string): Promise<void> {\n this.resolveDisk(disk)\n\n for (const path of paths) {\n this.files.delete(path)\n }\n\n return Promise.resolve()\n }\n\n /**\n * Read a file's metadata from fake storage, without its contents\n */\n async head(path: string, disk?: string): Promise<HeadResult | null> {\n this.resolveDisk(disk)\n\n const file = this.files.get(path)\n if (!file) {\n return Promise.resolve(null)\n }\n\n return Promise.resolve(toHeadResult(path, file))\n }\n\n /**\n * List files in fake storage, one page at a time\n *\n * Truncates after ONE object unless `limit` says otherwise. Real R2 pages at 1000, but no\n * fixture stores 1000 files — matching that number would return everything in one page and let\n * a caller who never follows `cursor` pass every test, then undercount against a real bucket.\n * Paging at one means any listing of two or more objects exercises the loop.\n *\n * Pass `limit` to widen the page when a test is asserting page contents rather than the loop.\n */\n async list(options: ListOptions = {}, disk?: string): Promise<ListResult> {\n this.resolveDisk(disk)\n\n const prefix = options.prefix ?? ''\n // Sorted so pagination is deterministic — an unordered Map iteration would make `cursor`\n // meaningless and the page boundaries unrepeatable between runs.\n const matching = Array.from(this.files.keys())\n .filter((path) => path.startsWith(prefix))\n .sort()\n\n const start = options.cursor === undefined ? 0 : matching.indexOf(options.cursor)\n if (start < 0) {\n return Promise.reject(new Error(`Unknown storage list cursor: ${options.cursor ?? ''}`))\n }\n\n const limit = options.limit ?? FAKE_LIST_PAGE_SIZE\n const page = matching.slice(start, start + limit)\n const next = matching[start + limit]\n\n return Promise.resolve({\n objects: page.map((path) => {\n const object = toHeadResult(path, this.files.get(path)!)\n if (options.includeMetadata) {\n return object\n }\n\n // Dropped unless asked for, because R2 omits them unless asked for. A fake that always\n // returned them would let a caller read `contentType` off a listing in tests and find it\n // undefined against a real bucket.\n return { ...object, contentType: undefined, metadata: undefined }\n }),\n truncated: next !== undefined,\n cursor: next,\n })\n }\n\n /**\n * Generate a fake presigned download URL\n */\n getPresignedDownloadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'GET', expiresIn))\n }\n\n /**\n * Generate a fake presigned upload URL\n */\n getPresignedUploadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'PUT', expiresIn))\n }\n\n /**\n * Generate a fake presigned delete URL\n */\n getPresignedDeleteUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'DELETE', expiresIn))\n }\n\n /**\n * Chunked upload (same as regular upload for fake)\n */\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number },\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const size = options.size ?? content.length\n\n // Reuse the already-materialized bytes rather than `body`: a `ReadableStream`\n // is single-use, so reading it here and again inside `upload` would throw\n // \"ReadableStream is disturbed\". `content` is a Uint8Array and safe to re-read.\n return this.upload(content, path, { ...options, size }, disk)\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Test Assertion Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Assert that a file exists at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file does not exist\n */\n assertExists(path: string): void {\n expect(\n this.files.has(path),\n `Expected file to exist at: ${path}\\nStored files: ${this.getStoredPaths().join(', ') || '(none)'}`\n ).toBe(true)\n }\n\n /**\n * Assert that a file does NOT exist at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file exists\n */\n assertMissing(path: string): void {\n expect(\n this.files.has(path),\n `Expected file NOT to exist at: ${path}`\n ).toBe(false)\n }\n\n /**\n * Assert storage is empty\n *\n * @throws AssertionError if any files exist\n */\n assertEmpty(): void {\n expect(\n this.files.size,\n `Expected storage to be empty but found ${this.files.size} files: ${this.getStoredPaths().join(', ')}`\n ).toBe(0)\n }\n\n /**\n * Assert storage has exactly N files\n *\n * @param count - Expected number of files\n * @throws AssertionError if count doesn't match\n */\n assertCount(count: number): void {\n expect(\n this.files.size,\n `Expected ${count} files in storage but found ${this.files.size}`\n ).toBe(count)\n }\n\n /**\n * Get all stored files (for inspection)\n */\n getStoredFiles(): Map<string, StoredFile> {\n return new Map(this.files)\n }\n\n /**\n * Get all stored file paths\n */\n getStoredPaths(): string[] {\n return Array.from(this.files.keys())\n }\n\n /**\n * Get a specific file by path\n */\n getFile(path: string): StoredFile | undefined {\n return this.files.get(path)\n }\n\n /**\n * Clear all stored files (call in beforeEach for test isolation)\n */\n clear(): void {\n this.files.clear()\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Private Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n private createPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn = 300\n ): PresignedUrlResult {\n const expiresAt = new Date(Date.now() + expiresIn * 1000)\n\n return {\n url: `https://fake-storage.test/${path}?method=${method}&expires=${expiresAt.toISOString()}`,\n expiresIn,\n expiresAt,\n method,\n }\n }\n\n private async bodyToUint8Array(body: StreamingBlobPayloadInputTypes | null | undefined): Promise<Uint8Array> {\n if (!body) {\n return new Uint8Array(0)\n }\n\n if (body instanceof Uint8Array) {\n return body\n }\n\n if (body instanceof ArrayBuffer) {\n return new Uint8Array(body)\n }\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body)\n }\n\n if (body instanceof Blob) {\n const buffer = await body.arrayBuffer()\n return new Uint8Array(buffer)\n }\n\n if (body instanceof ReadableStream) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n // FormData or URLSearchParams - convert via Response\n if (body instanceof FormData || body instanceof URLSearchParams) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n return new Uint8Array(0)\n }\n}\n"],"mappings":"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"}
|
|
@@ -22,12 +22,13 @@ declare class TestEmailProvider implements IEmailProvider {
|
|
|
22
22
|
* In-memory stand-in for Cloudflare Workers Caching's `ctx.cache`.
|
|
23
23
|
*
|
|
24
24
|
* Neither Miniflare nor workerd populates `ExecutionContext.cache` locally,
|
|
25
|
-
* which would otherwise
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* which would otherwise leave every `@Cacheable` route in a test serving
|
|
26
|
+
* uncached and stamped `private, no-store` — so a spec asserting the headers
|
|
27
|
+
* the route declares would read the ones an unconfigured runtime produces. The
|
|
28
|
+
* testing module builder installs this by default so those routes are testable
|
|
29
|
+
* with zero configuration: `@Cacheable` responses carry real
|
|
30
|
+
* `Cache-Control`/`Cache-Tag` headers, and `@PurgesCache` purges are recorded
|
|
31
|
+
* rather than skipped.
|
|
31
32
|
*
|
|
32
33
|
* Every spec passed to `purge()` is recorded, in call order, for assertion
|
|
33
34
|
* via `module.cache.purges`. Purges always succeed — this stub only proves an
|
|
@@ -140,4 +141,4 @@ declare class TestWorkerExports {
|
|
|
140
141
|
}
|
|
141
142
|
//#endregion
|
|
142
143
|
export { TestEmailProvider as i, TestWorkerExports as n, TestWorkersCache as r, LoopbackCall as t };
|
|
143
|
-
//# sourceMappingURL=test-worker-exports-
|
|
144
|
+
//# sourceMappingURL=test-worker-exports-dCRARYEc.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-worker-exports-dCRARYEc.d.mts","names":[],"sources":["../src/mocks/test-email-provider.ts","../src/mocks/test-workers-cache.ts","../src/mocks/test-worker-exports.ts"],"mappings":";;;;;;;;;;;;cAgBa,6BAA6B;;WAEhC,MAAM;EAEf,KAAK,SAAS,uBAAuB,QAAQ;EAKvC,UAAU,UAAU,yBAAyB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;cCH/C,4BAA4B;;WAE9B,QAAQ;EAEjB,MAAM,MAAM,YAAY;IAAU;;;;;;UCtBnB;;EAEf;EACA;EACA;;EAEA,OAAO;;;KAIG,mBACV,SAAS,SACT,OAAO,4BACJ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA;mBAOkB;;WALpB,WAAW;UAEZ;mBACS;EAEjB,YAA6B,OAAO;;;;;;;;;;EAWpC,WAAW,SAAS;;;;;;;;;;;;;;;;;;MAqBhB,WAAW;;;;;;;UAmBP;UAkBM;UAiBA"}
|
|
@@ -153,12 +153,13 @@ var TestWorkerExports = class {
|
|
|
153
153
|
* In-memory stand-in for Cloudflare Workers Caching's `ctx.cache`.
|
|
154
154
|
*
|
|
155
155
|
* Neither Miniflare nor workerd populates `ExecutionContext.cache` locally,
|
|
156
|
-
* which would otherwise
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
156
|
+
* which would otherwise leave every `@Cacheable` route in a test serving
|
|
157
|
+
* uncached and stamped `private, no-store` — so a spec asserting the headers
|
|
158
|
+
* the route declares would read the ones an unconfigured runtime produces. The
|
|
159
|
+
* testing module builder installs this by default so those routes are testable
|
|
160
|
+
* with zero configuration: `@Cacheable` responses carry real
|
|
161
|
+
* `Cache-Control`/`Cache-Tag` headers, and `@PurgesCache` purges are recorded
|
|
162
|
+
* rather than skipped.
|
|
162
163
|
*
|
|
163
164
|
* Every spec passed to `purge()` is recorded, in call order, for assertion
|
|
164
165
|
* via `module.cache.purges`. Purges always succeed — this stub only proves an
|
|
@@ -179,4 +180,4 @@ var TestWorkersCache = class {
|
|
|
179
180
|
//#endregion
|
|
180
181
|
export { TestWorkerExports as n, TestEmailProvider as r, TestWorkersCache as t };
|
|
181
182
|
|
|
182
|
-
//# sourceMappingURL=test-workers-cache-
|
|
183
|
+
//# sourceMappingURL=test-workers-cache-D02Pt_dE.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-workers-cache-eiGMw9Yj.mjs","names":[],"sources":["../src/mocks/test-email-provider.ts","../src/mocks/test-worker-exports.ts","../src/mocks/test-workers-cache.ts"],"sourcesContent":["import type {\n\tEmailBatchSendResult,\n\tEmailSendResult,\n\tIEmailProvider,\n\tResolvedEmailMessage,\n} from 'stratal/email'\n\n/**\n * In-memory email provider for tests.\n *\n * The sync queue provider runs `EmailConsumer` inline on dispatch, which would\n * otherwise open a real SMTP connection from the test worker. The testing\n * module builder installs this provider by default (overridable via\n * `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every\n * message so tests can assert on what was sent.\n */\nexport class TestEmailProvider implements IEmailProvider {\n\t/** Every message handed to the provider, in send order. */\n\treadonly sent: ResolvedEmailMessage[] = []\n\n\tsend(message: ResolvedEmailMessage): Promise<EmailSendResult> {\n\t\tthis.sent.push(message)\n\t\treturn Promise.resolve({ messageId: `test-${this.sent.length}`, accepted: true })\n\t}\n\n\tasync sendBatch(messages: ResolvedEmailMessage[]): Promise<EmailBatchSendResult> {\n\t\tconst results = await Promise.all(messages.map((message) => this.send(message)))\n\t\treturn {\n\t\t\ttotal: results.length,\n\t\t\tsuccessful: results.length,\n\t\t\tfailed: 0,\n\t\t\tresults,\n\t\t}\n\t}\n}\n","import type { PurgeSpec } from 'stratal/response-cache'\nimport type { TestWorkersCache } from './test-workers-cache'\n\n/** One `ctx.exports.<Name>.fetch()` the gateway made, recorded for assertion. */\nexport interface LoopbackCall {\n /** The export name the gateway dispatched to — `gateway: { entrypoint }`. */\n entrypoint: string\n method: string\n url: string\n /** The resolved partitions the gateway placed in `ctx.props`. */\n props: Record<string, unknown>\n}\n\n/** Runs a loopback in cached mode. Installed by the testing module builder. */\nexport type LoopbackHandler = (\n request: Request,\n props: Record<string, unknown>,\n) => Promise<Response>\n\n/**\n * In-memory stand-in for `ctx.exports` — the loopback bindings a Worker has to\n * its own top-level exports.\n *\n * Neither Miniflare nor workerd populates `ExecutionContext.exports` for a\n * `Test.createTestingModule()` app, so without this every consumer that adopts\n * `ResponseCacheModule.forRoot({ gateway: { entrypoint } })` would have their\n * whole suite fail on the first request: the gateway's boot verification\n * throws `ResponseCacheConfigError` the moment it cannot reach the configured\n * entrypoint. That is the same usability defect {@link TestWorkersCache} fixes\n * for `ctx.cache`, and it is fixed the same way — installed by default, opt out\n * with `Test.createTestingModule({ cache: false })`.\n *\n * The stub is not inert. A dispatched request is re-run through the *same*\n * Hono app with an unmarked execution context carrying the gateway's chosen\n * `props` — exactly what the real cached entrypoint does — so a partitioned\n * route is genuinely exercised end to end rather than short-circuited. Every\n * call is recorded, in order, for assertion via `module.gateway.loopbacks`,\n * mirroring `module.cache.purges`:\n *\n * ```typescript\n * await module.http.get('/dashboard').withHeaders({ ... }).send()\n * expect(module.gateway.loopbacks).toEqual([\n * { entrypoint: 'Cached', method: 'GET', url: '...', props: { user: 'u-1' } },\n * ])\n * ```\n *\n * An empty `loopbacks` after a request to a partitioned route is itself the\n * assertion for the fail-closed cases — an unresolved partition, a non-GET, a\n * route with no `partitionBy` — all of which must run inline.\n *\n * Purges forwarded over RPC (`ctx.exports.<Name>.purge(spec)`) land on the same\n * {@link TestWorkersCache} as a direct `ctx.cache.purge()`, so\n * `module.cache.purges` reads identically whether or not a gateway is\n * configured.\n */\nexport class TestWorkerExports {\n /** Every loopback dispatch, in call order. */\n readonly loopbacks: LoopbackCall[] = []\n\n private handler: LoopbackHandler | undefined\n private readonly stubs = new Map<string, unknown>()\n\n constructor(private readonly cache: TestWorkersCache | undefined) {}\n\n /**\n * Install the function that actually serves a loopback.\n *\n * Set after the `Application` exists, because the handler needs its Hono\n * app — while the execution context carrying this stub has to be built\n * before it, to be passed to the constructor.\n *\n * @internal\n */\n setHandler(handler: LoopbackHandler): void {\n this.handler = handler\n }\n\n /**\n * The object to install as `ctx.exports`.\n *\n * A `Proxy` rather than a fixed record because the export name is the\n * consumer's to choose — the framework only ever sees the string in\n * `gateway: { entrypoint }`. Answering for every name keeps a typo'd\n * entrypoint from being caught here, in a stub, instead of in the\n * consumer's real Wrangler config where it matters; that trade is\n * deliberate, since a suite failing on a name the test never mentions is\n * far more confusing than a deploy-time error.\n *\n * A typo is meant to be caught earlier still: `gateway.entrypoint` is typed\n * as `CachedEntrypointName`, so once the consumer has run `wrangler types` a\n * wrong name is a compile error at the config site — before this stub, and\n * before deploy. This Proxy is the runtime fallback for projects without\n * generated types, not the primary guard.\n */\n get context(): Record<string, unknown> {\n return new Proxy(\n {},\n {\n get: (_target, property) =>\n typeof property === 'string' ? this.stubFor(property) : undefined,\n has: () => true,\n ownKeys: () => [...this.stubs.keys()],\n getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true }),\n },\n )\n }\n\n /**\n * A loopback binding, shaped like the real one:\n * `Fetcher<T> & ((opts: { props }) => Fetcher<T>)`. Both the callable form\n * (which is how `ctx.props` is chosen) and the direct form are supported,\n * so whichever the framework uses is genuinely exercised.\n */\n private stubFor(entrypoint: string): unknown {\n const existing = this.stubs.get(entrypoint)\n if (existing) return existing\n\n const bound = (props: Record<string, unknown>) => ({\n fetch: (request: Request) => this.dispatch(entrypoint, request, props),\n purge: (spec: PurgeSpec) => this.purge(spec),\n })\n\n const stub = Object.assign(\n (options?: { props?: Record<string, unknown> }) => bound(options?.props ?? {}),\n bound({}),\n )\n\n this.stubs.set(entrypoint, stub)\n return stub\n }\n\n private async dispatch(\n entrypoint: string,\n request: Request,\n props: Record<string, unknown>,\n ): Promise<Response> {\n this.loopbacks.push({ entrypoint, method: request.method, url: request.url, props })\n\n if (!this.handler) {\n throw new Error(\n '[stratal:testing] A gateway loopback was dispatched before the testing module finished ' +\n 'compiling. This is a bug in @stratal/testing — please report it.',\n )\n }\n\n return this.handler(request, props)\n }\n\n private async purge(spec: PurgeSpec): Promise<{ success: boolean }> {\n // Recorded on the same stub a direct `ctx.cache.purge()` writes to, so\n // `module.cache.purges` reads the same with or without a gateway.\n await this.cache?.purge(spec)\n return { success: true }\n }\n}\n","import type { PurgeSpec, WorkersCache } from 'stratal/response-cache'\n\n/**\n * In-memory stand-in for Cloudflare Workers Caching's `ctx.cache`.\n *\n * Neither Miniflare nor workerd populates `ExecutionContext.cache` locally,\n * which would otherwise make every `@Cacheable`/`@PurgesCache` route 500 in\n * tests — `assertCachingAvailable` fails boot the moment such a route exists\n * without it. The testing module builder installs this by default so those\n * routes are testable with zero configuration: `@Cacheable` responses carry\n * real `Cache-Control`/`Cache-Tag` headers, and `@PurgesCache` purges succeed\n * instead of throwing `CachePurgeError`.\n *\n * Every spec passed to `purge()` is recorded, in call order, for assertion\n * via `module.cache.purges`. Purges always succeed — this stub only proves an\n * app's cache configuration is correct, not Workers Caching's own failure\n * modes (that's covered inside `stratal` itself, against a mocked\n * `WorkersCache`). Opt out with `Test.createTestingModule({ cache: false })`\n * to reproduce a runtime where Workers Caching is genuinely unconfigured —\n * e.g. to test the `ResponseCacheConfigError` boot guard.\n */\nexport class TestWorkersCache implements WorkersCache {\n /** Every spec passed to `purge()`, in call order. */\n readonly purges: PurgeSpec[] = []\n\n purge(spec: PurgeSpec): Promise<{ success: boolean }> {\n this.purges.push(spec)\n return Promise.resolve({ success: true })\n }\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAa,oBAAb,MAAyD;;CAExD,OAAwC,CAAC;CAEzC,KAAK,SAAyD;EAC7D,KAAK,KAAK,KAAK,OAAO;EACtB,OAAO,QAAQ,QAAQ;GAAE,WAAW,QAAQ,KAAK,KAAK;GAAU,UAAU;EAAK,CAAC;CACjF;CAEA,MAAM,UAAU,UAAiE;EAChF,MAAM,UAAU,MAAM,QAAQ,IAAI,SAAS,KAAK,YAAY,KAAK,KAAK,OAAO,CAAC,CAAC;EAC/E,OAAO;GACN,OAAO,QAAQ;GACf,YAAY,QAAQ;GACpB,QAAQ;GACR;EACD;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACqBA,IAAa,oBAAb,MAA+B;CAOA;;CAL7B,YAAqC,CAAC;CAEtC;CACA,wBAAyB,IAAI,IAAqB;CAElD,YAAY,OAAsD;EAArC,KAAA,QAAA;CAAsC;;;;;;;;;;CAWnE,WAAW,SAAgC;EACzC,KAAK,UAAU;CACjB;;;;;;;;;;;;;;;;;;CAmBA,IAAI,UAAmC;EACrC,OAAO,IAAI,MACT,CAAC,GACD;GACE,MAAM,SAAS,aACb,OAAO,aAAa,WAAW,KAAK,QAAQ,QAAQ,IAAI,KAAA;GAC1D,WAAW;GACX,eAAe,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC;GACpC,iCAAiC;IAAE,YAAY;IAAM,cAAc;GAAK;EAC1E,CACF;CACF;;;;;;;CAQA,QAAgB,YAA6B;EAC3C,MAAM,WAAW,KAAK,MAAM,IAAI,UAAU;EAC1C,IAAI,UAAU,OAAO;EAErB,MAAM,SAAS,WAAoC;GACjD,QAAQ,YAAqB,KAAK,SAAS,YAAY,SAAS,KAAK;GACrE,QAAQ,SAAoB,KAAK,MAAM,IAAI;EAC7C;EAEA,MAAM,OAAO,OAAO,QACjB,YAAkD,MAAM,SAAS,SAAS,CAAC,CAAC,GAC7E,MAAM,CAAC,CAAC,CACV;EAEA,KAAK,MAAM,IAAI,YAAY,IAAI;EAC/B,OAAO;CACT;CAEA,MAAc,SACZ,YACA,SACA,OACmB;EACnB,KAAK,UAAU,KAAK;GAAE;GAAY,QAAQ,QAAQ;GAAQ,KAAK,QAAQ;GAAK;EAAM,CAAC;EAEnF,IAAI,CAAC,KAAK,SACR,MAAM,IAAI,MACR,yJAEF;EAGF,OAAO,KAAK,QAAQ,SAAS,KAAK;CACpC;CAEA,MAAc,MAAM,MAAgD;EAGlE,MAAM,KAAK,OAAO,MAAM,IAAI;EAC5B,OAAO,EAAE,SAAS,KAAK;CACzB;AACF;;;;;;;;;;;;;;;;;;;;;;ACrIA,IAAa,mBAAb,MAAsD;;CAEpD,SAA+B,CAAC;CAEhC,MAAM,MAAgD;EACpD,KAAK,OAAO,KAAK,IAAI;EACrB,OAAO,QAAQ,QAAQ,EAAE,SAAS,KAAK,CAAC;CAC1C;AACF"}
|
|
1
|
+
{"version":3,"file":"test-workers-cache-D02Pt_dE.mjs","names":[],"sources":["../src/mocks/test-email-provider.ts","../src/mocks/test-worker-exports.ts","../src/mocks/test-workers-cache.ts"],"sourcesContent":["import type {\n\tEmailBatchSendResult,\n\tEmailSendResult,\n\tIEmailProvider,\n\tResolvedEmailMessage,\n} from 'stratal/email'\n\n/**\n * In-memory email provider for tests.\n *\n * The sync queue provider runs `EmailConsumer` inline on dispatch, which would\n * otherwise open a real SMTP connection from the test worker. The testing\n * module builder installs this provider by default (overridable via\n * `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every\n * message so tests can assert on what was sent.\n */\nexport class TestEmailProvider implements IEmailProvider {\n\t/** Every message handed to the provider, in send order. */\n\treadonly sent: ResolvedEmailMessage[] = []\n\n\tsend(message: ResolvedEmailMessage): Promise<EmailSendResult> {\n\t\tthis.sent.push(message)\n\t\treturn Promise.resolve({ messageId: `test-${this.sent.length}`, accepted: true })\n\t}\n\n\tasync sendBatch(messages: ResolvedEmailMessage[]): Promise<EmailBatchSendResult> {\n\t\tconst results = await Promise.all(messages.map((message) => this.send(message)))\n\t\treturn {\n\t\t\ttotal: results.length,\n\t\t\tsuccessful: results.length,\n\t\t\tfailed: 0,\n\t\t\tresults,\n\t\t}\n\t}\n}\n","import type { PurgeSpec } from 'stratal/response-cache'\nimport type { TestWorkersCache } from './test-workers-cache'\n\n/** One `ctx.exports.<Name>.fetch()` the gateway made, recorded for assertion. */\nexport interface LoopbackCall {\n /** The export name the gateway dispatched to — `gateway: { entrypoint }`. */\n entrypoint: string\n method: string\n url: string\n /** The resolved partitions the gateway placed in `ctx.props`. */\n props: Record<string, unknown>\n}\n\n/** Runs a loopback in cached mode. Installed by the testing module builder. */\nexport type LoopbackHandler = (\n request: Request,\n props: Record<string, unknown>,\n) => Promise<Response>\n\n/**\n * In-memory stand-in for `ctx.exports` — the loopback bindings a Worker has to\n * its own top-level exports.\n *\n * Neither Miniflare nor workerd populates `ExecutionContext.exports` for a\n * `Test.createTestingModule()` app, so without this every consumer that adopts\n * `ResponseCacheModule.forRoot({ gateway: { entrypoint } })` would have their\n * whole suite fail on the first request: the gateway's boot verification\n * throws `ResponseCacheConfigError` the moment it cannot reach the configured\n * entrypoint. That is the same usability defect {@link TestWorkersCache} fixes\n * for `ctx.cache`, and it is fixed the same way — installed by default, opt out\n * with `Test.createTestingModule({ cache: false })`.\n *\n * The stub is not inert. A dispatched request is re-run through the *same*\n * Hono app with an unmarked execution context carrying the gateway's chosen\n * `props` — exactly what the real cached entrypoint does — so a partitioned\n * route is genuinely exercised end to end rather than short-circuited. Every\n * call is recorded, in order, for assertion via `module.gateway.loopbacks`,\n * mirroring `module.cache.purges`:\n *\n * ```typescript\n * await module.http.get('/dashboard').withHeaders({ ... }).send()\n * expect(module.gateway.loopbacks).toEqual([\n * { entrypoint: 'Cached', method: 'GET', url: '...', props: { user: 'u-1' } },\n * ])\n * ```\n *\n * An empty `loopbacks` after a request to a partitioned route is itself the\n * assertion for the fail-closed cases — an unresolved partition, a non-GET, a\n * route with no `partitionBy` — all of which must run inline.\n *\n * Purges forwarded over RPC (`ctx.exports.<Name>.purge(spec)`) land on the same\n * {@link TestWorkersCache} as a direct `ctx.cache.purge()`, so\n * `module.cache.purges` reads identically whether or not a gateway is\n * configured.\n */\nexport class TestWorkerExports {\n /** Every loopback dispatch, in call order. */\n readonly loopbacks: LoopbackCall[] = []\n\n private handler: LoopbackHandler | undefined\n private readonly stubs = new Map<string, unknown>()\n\n constructor(private readonly cache: TestWorkersCache | undefined) {}\n\n /**\n * Install the function that actually serves a loopback.\n *\n * Set after the `Application` exists, because the handler needs its Hono\n * app — while the execution context carrying this stub has to be built\n * before it, to be passed to the constructor.\n *\n * @internal\n */\n setHandler(handler: LoopbackHandler): void {\n this.handler = handler\n }\n\n /**\n * The object to install as `ctx.exports`.\n *\n * A `Proxy` rather than a fixed record because the export name is the\n * consumer's to choose — the framework only ever sees the string in\n * `gateway: { entrypoint }`. Answering for every name keeps a typo'd\n * entrypoint from being caught here, in a stub, instead of in the\n * consumer's real Wrangler config where it matters; that trade is\n * deliberate, since a suite failing on a name the test never mentions is\n * far more confusing than a deploy-time error.\n *\n * A typo is meant to be caught earlier still: `gateway.entrypoint` is typed\n * as `CachedEntrypointName`, so once the consumer has run `wrangler types` a\n * wrong name is a compile error at the config site — before this stub, and\n * before deploy. This Proxy is the runtime fallback for projects without\n * generated types, not the primary guard.\n */\n get context(): Record<string, unknown> {\n return new Proxy(\n {},\n {\n get: (_target, property) =>\n typeof property === 'string' ? this.stubFor(property) : undefined,\n has: () => true,\n ownKeys: () => [...this.stubs.keys()],\n getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true }),\n },\n )\n }\n\n /**\n * A loopback binding, shaped like the real one:\n * `Fetcher<T> & ((opts: { props }) => Fetcher<T>)`. Both the callable form\n * (which is how `ctx.props` is chosen) and the direct form are supported,\n * so whichever the framework uses is genuinely exercised.\n */\n private stubFor(entrypoint: string): unknown {\n const existing = this.stubs.get(entrypoint)\n if (existing) return existing\n\n const bound = (props: Record<string, unknown>) => ({\n fetch: (request: Request) => this.dispatch(entrypoint, request, props),\n purge: (spec: PurgeSpec) => this.purge(spec),\n })\n\n const stub = Object.assign(\n (options?: { props?: Record<string, unknown> }) => bound(options?.props ?? {}),\n bound({}),\n )\n\n this.stubs.set(entrypoint, stub)\n return stub\n }\n\n private async dispatch(\n entrypoint: string,\n request: Request,\n props: Record<string, unknown>,\n ): Promise<Response> {\n this.loopbacks.push({ entrypoint, method: request.method, url: request.url, props })\n\n if (!this.handler) {\n throw new Error(\n '[stratal:testing] A gateway loopback was dispatched before the testing module finished ' +\n 'compiling. This is a bug in @stratal/testing — please report it.',\n )\n }\n\n return this.handler(request, props)\n }\n\n private async purge(spec: PurgeSpec): Promise<{ success: boolean }> {\n // Recorded on the same stub a direct `ctx.cache.purge()` writes to, so\n // `module.cache.purges` reads the same with or without a gateway.\n await this.cache?.purge(spec)\n return { success: true }\n }\n}\n","import type { PurgeSpec, WorkersCache } from 'stratal/response-cache'\n\n/**\n * In-memory stand-in for Cloudflare Workers Caching's `ctx.cache`.\n *\n * Neither Miniflare nor workerd populates `ExecutionContext.cache` locally,\n * which would otherwise leave every `@Cacheable` route in a test serving\n * uncached and stamped `private, no-store` — so a spec asserting the headers\n * the route declares would read the ones an unconfigured runtime produces. The\n * testing module builder installs this by default so those routes are testable\n * with zero configuration: `@Cacheable` responses carry real\n * `Cache-Control`/`Cache-Tag` headers, and `@PurgesCache` purges are recorded\n * rather than skipped.\n *\n * Every spec passed to `purge()` is recorded, in call order, for assertion\n * via `module.cache.purges`. Purges always succeed — this stub only proves an\n * app's cache configuration is correct, not Workers Caching's own failure\n * modes (that's covered inside `stratal` itself, against a mocked\n * `WorkersCache`). Opt out with `Test.createTestingModule({ cache: false })`\n * to reproduce a runtime where Workers Caching is genuinely unconfigured —\n * e.g. to test the `ResponseCacheConfigError` boot guard.\n */\nexport class TestWorkersCache implements WorkersCache {\n /** Every spec passed to `purge()`, in call order. */\n readonly purges: PurgeSpec[] = []\n\n purge(spec: PurgeSpec): Promise<{ success: boolean }> {\n this.purges.push(spec)\n return Promise.resolve({ success: true })\n }\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAa,oBAAb,MAAyD;;CAExD,OAAwC,CAAC;CAEzC,KAAK,SAAyD;EAC7D,KAAK,KAAK,KAAK,OAAO;EACtB,OAAO,QAAQ,QAAQ;GAAE,WAAW,QAAQ,KAAK,KAAK;GAAU,UAAU;EAAK,CAAC;CACjF;CAEA,MAAM,UAAU,UAAiE;EAChF,MAAM,UAAU,MAAM,QAAQ,IAAI,SAAS,KAAK,YAAY,KAAK,KAAK,OAAO,CAAC,CAAC;EAC/E,OAAO;GACN,OAAO,QAAQ;GACf,YAAY,QAAQ;GACpB,QAAQ;GACR;EACD;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACqBA,IAAa,oBAAb,MAA+B;CAOA;;CAL7B,YAAqC,CAAC;CAEtC;CACA,wBAAyB,IAAI,IAAqB;CAElD,YAAY,OAAsD;EAArC,KAAA,QAAA;CAAsC;;;;;;;;;;CAWnE,WAAW,SAAgC;EACzC,KAAK,UAAU;CACjB;;;;;;;;;;;;;;;;;;CAmBA,IAAI,UAAmC;EACrC,OAAO,IAAI,MACT,CAAC,GACD;GACE,MAAM,SAAS,aACb,OAAO,aAAa,WAAW,KAAK,QAAQ,QAAQ,IAAI,KAAA;GAC1D,WAAW;GACX,eAAe,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC;GACpC,iCAAiC;IAAE,YAAY;IAAM,cAAc;GAAK;EAC1E,CACF;CACF;;;;;;;CAQA,QAAgB,YAA6B;EAC3C,MAAM,WAAW,KAAK,MAAM,IAAI,UAAU;EAC1C,IAAI,UAAU,OAAO;EAErB,MAAM,SAAS,WAAoC;GACjD,QAAQ,YAAqB,KAAK,SAAS,YAAY,SAAS,KAAK;GACrE,QAAQ,SAAoB,KAAK,MAAM,IAAI;EAC7C;EAEA,MAAM,OAAO,OAAO,QACjB,YAAkD,MAAM,SAAS,SAAS,CAAC,CAAC,GAC7E,MAAM,CAAC,CAAC,CACV;EAEA,KAAK,MAAM,IAAI,YAAY,IAAI;EAC/B,OAAO;CACT;CAEA,MAAc,SACZ,YACA,SACA,OACmB;EACnB,KAAK,UAAU,KAAK;GAAE;GAAY,QAAQ,QAAQ;GAAQ,KAAK,QAAQ;GAAK;EAAM,CAAC;EAEnF,IAAI,CAAC,KAAK,SACR,MAAM,IAAI,MACR,yJAEF;EAGF,OAAO,KAAK,QAAQ,SAAS,KAAK;CACpC;CAEA,MAAc,MAAM,MAAgD;EAGlE,MAAM,KAAK,OAAO,MAAM,IAAI;EAC5B,OAAO,EAAE,SAAS,KAAK;CACzB;AACF;;;;;;;;;;;;;;;;;;;;;;;ACpIA,IAAa,mBAAb,MAAsD;;CAEpD,SAA+B,CAAC;CAEhC,MAAM,MAAgD;EACpD,KAAK,OAAO,KAAK,IAAI;EACrB,OAAO,QAAQ,QAAQ,EAAE,SAAS,KAAK,CAAC;CAC1C;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StratalEnv } from "stratal";
|
|
2
|
-
import { cloudflareTest } from "@cloudflare/vitest-
|
|
2
|
+
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
3
3
|
import { Plugin } from "vite";
|
|
4
4
|
//#region src/vitest-plugin/stratal-test.d.ts
|
|
5
5
|
type CloudflareTestOptions = Parameters<typeof cloudflareTest>[0];
|
|
@@ -33,7 +33,7 @@ type StratalTestOptions = WorkersPoolOptions & {
|
|
|
33
33
|
* root Vite instance doesn't resolve.
|
|
34
34
|
*
|
|
35
35
|
* Must be used at the **root** `defineConfig` level so that the
|
|
36
|
-
* `@cloudflare/vitest-
|
|
36
|
+
* `@cloudflare/vitest-plugin` module fallback resolver (which uses the root Vite
|
|
37
37
|
* instance) resolves pg sub-deps correctly.
|
|
38
38
|
*
|
|
39
39
|
* @example
|
|
@@ -52,7 +52,7 @@ type StratalTestOptions = WorkersPoolOptions & {
|
|
|
52
52
|
* })
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
|
-
declare const fixPgCjs: () => Plugin;
|
|
55
|
+
export declare const fixPgCjs: () => Plugin;
|
|
56
56
|
/**
|
|
57
57
|
* Returns a Vite plugin that forces CJS resolution for `@noble/hashes` subpaths
|
|
58
58
|
* used by `@paralleldrive/cuid2@2.x` (the version `@zenstackhq/orm` depends on).
|
|
@@ -82,21 +82,21 @@ declare const fixPgCjs: () => Plugin;
|
|
|
82
82
|
* })
|
|
83
83
|
* ```
|
|
84
84
|
*/
|
|
85
|
-
declare const fixNobleHashesCjs: () => Plugin;
|
|
85
|
+
export declare const fixNobleHashesCjs: () => Plugin;
|
|
86
86
|
/**
|
|
87
87
|
* Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.
|
|
88
88
|
*
|
|
89
89
|
* Includes the cloudflare pool plugin and Stratal alias plugin. Pass a
|
|
90
|
-
* `database` option to give each
|
|
91
|
-
* migrated template,
|
|
92
|
-
*
|
|
90
|
+
* `database` option to give each test file a leased database holding a fresh
|
|
91
|
+
* clone of a migrated template, and to enable file parallelism. Use inside a
|
|
92
|
+
* project-level `plugins` array.
|
|
93
93
|
*
|
|
94
94
|
* **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.
|
|
95
95
|
*
|
|
96
96
|
* @param options - `cloudflareTest()` options plus Stratal `database` options
|
|
97
97
|
* @returns An array of Vite plugins
|
|
98
98
|
*/
|
|
99
|
-
declare function stratalTest(options?: StratalTestOptions): Plugin[];
|
|
99
|
+
export declare function stratalTest(options?: StratalTestOptions): Plugin[];
|
|
100
100
|
//#endregion
|
|
101
|
-
export {
|
|
101
|
+
export type { StratalTestDatabaseOptions };
|
|
102
102
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"mappings":";;;;KAYK,wBAAwB,kBAAkB;;KAG1C,iBAAiB,WACjB,WAAW,eAAe,WAAW,WAAW,aAAa,mBAAkB;;;;;;KAS/E,yBAAyB,2CAA2C;;UAGxD;;;;;EAKf,UAAU;;KAGP,qBAAqB,QAAQ,2BAA2B;KACxD,qBAAqB;EAAuB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"mappings":";;;;KAYK,wBAAwB,kBAAkB;;KAG1C,iBAAiB,WACjB,WAAW,eAAe,WAAW,WAAW,aAAa,mBAAkB;;;;;;KAS/E,yBAAyB,2CAA2C;;UAGxD;;;;;EAKf,UAAU;;KAGP,qBAAqB,QAAQ,2BAA2B;KACxD,qBAAqB;EAAuB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsC/C,gBAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA2Cf,yBAAwB;;;;;;;;;;;;;;wBAwFrB,YAAY,UAAS,qBAA0B"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { i as BINDING_ENV_VAR } from "../database-
|
|
1
|
+
import { i as BINDING_ENV_VAR } from "../database-Rojs69r3.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DB_SHARED_POOL_ENV } from "@stratal/framework/database";
|
|
5
|
-
import { cloudflareTest } from "@cloudflare/vitest-
|
|
5
|
+
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
6
6
|
//#region src/vitest-plugin/stratal-test.ts
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const pgCjsResolvers = /* @__PURE__ */ new Map([
|
|
@@ -21,7 +21,7 @@ const pgCjsResolvers = /* @__PURE__ */ new Map([
|
|
|
21
21
|
* root Vite instance doesn't resolve.
|
|
22
22
|
*
|
|
23
23
|
* Must be used at the **root** `defineConfig` level so that the
|
|
24
|
-
* `@cloudflare/vitest-
|
|
24
|
+
* `@cloudflare/vitest-plugin` module fallback resolver (which uses the root Vite
|
|
25
25
|
* instance) resolves pg sub-deps correctly.
|
|
26
26
|
*
|
|
27
27
|
* @example
|
|
@@ -96,7 +96,8 @@ const fixNobleHashesCjs = () => {
|
|
|
96
96
|
} catch {
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
-
const
|
|
99
|
+
const cuid2Path = createRequire(zenstackPath).resolve("@paralleldrive/cuid2");
|
|
100
|
+
const cuid2Require = createRequire(cuid2Path);
|
|
100
101
|
resolved = new Map(ids.map((id) => [id, cuid2Require.resolve(id)]));
|
|
101
102
|
},
|
|
102
103
|
resolveId(id) {
|
|
@@ -104,9 +105,10 @@ const fixNobleHashesCjs = () => {
|
|
|
104
105
|
}
|
|
105
106
|
};
|
|
106
107
|
};
|
|
108
|
+
/** Builds the Stratal defaults plugin returned by {@link stratalTest}; exported for direct unit testing. */
|
|
107
109
|
const createStratalPlugin = (databaseEnabled) => ({
|
|
108
110
|
name: "stratal-test",
|
|
109
|
-
config() {
|
|
111
|
+
config(userConfig) {
|
|
110
112
|
const config = {
|
|
111
113
|
resolve: { alias: {
|
|
112
114
|
tslib: "tslib/tslib.es6.mjs",
|
|
@@ -116,11 +118,15 @@ const createStratalPlugin = (databaseEnabled) => ({
|
|
|
116
118
|
} },
|
|
117
119
|
ssr: { noExternal: ["@zenstackhq/better-auth"] }
|
|
118
120
|
};
|
|
119
|
-
if (databaseEnabled)
|
|
120
|
-
|
|
121
|
-
isolate: true,
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
if (databaseEnabled) {
|
|
122
|
+
const existing = userConfig.test ?? {};
|
|
123
|
+
if (existing.isolate === false) throw new Error("[stratal-testing] stratalTest({ database }) needs `isolate: true`. Each test file leases its database for the life of its isolate, so files sharing an isolate would share a database. Remove `isolate: false` from this project.");
|
|
124
|
+
config.test = {
|
|
125
|
+
fileParallelism: existing.fileParallelism ?? true,
|
|
126
|
+
isolate: true,
|
|
127
|
+
hookTimeout: existing.hookTimeout ?? 3e4
|
|
128
|
+
};
|
|
129
|
+
}
|
|
124
130
|
return config;
|
|
125
131
|
}
|
|
126
132
|
});
|
|
@@ -128,9 +134,9 @@ const createStratalPlugin = (databaseEnabled) => ({
|
|
|
128
134
|
* Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.
|
|
129
135
|
*
|
|
130
136
|
* Includes the cloudflare pool plugin and Stratal alias plugin. Pass a
|
|
131
|
-
* `database` option to give each
|
|
132
|
-
* migrated template,
|
|
133
|
-
*
|
|
137
|
+
* `database` option to give each test file a leased database holding a fresh
|
|
138
|
+
* clone of a migrated template, and to enable file parallelism. Use inside a
|
|
139
|
+
* project-level `plugins` array.
|
|
134
140
|
*
|
|
135
141
|
* **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.
|
|
136
142
|
*
|
|
@@ -145,7 +151,7 @@ function stratalTest(options = {}) {
|
|
|
145
151
|
[BINDING_ENV_VAR]: binding,
|
|
146
152
|
[DB_SHARED_POOL_ENV]: "true"
|
|
147
153
|
} : {};
|
|
148
|
-
|
|
154
|
+
const merged = {
|
|
149
155
|
...cfOptions,
|
|
150
156
|
miniflare: {
|
|
151
157
|
...cfOptions.miniflare,
|
|
@@ -154,7 +160,8 @@ function stratalTest(options = {}) {
|
|
|
154
160
|
...dbEnv
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
|
-
}
|
|
163
|
+
};
|
|
164
|
+
return [cloudflareTest(merged), createStratalPlugin(databaseEnabled)];
|
|
158
165
|
}
|
|
159
166
|
//#endregion
|
|
160
167
|
export { fixNobleHashesCjs, fixPgCjs, stratalTest };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"sourcesContent":["import { createRequire } from 'node:module'\nimport path from 'node:path'\n\nimport { cloudflareTest } from '@cloudflare/vitest-pool-workers'\nimport type { StratalEnv } from 'stratal'\nimport type { Plugin, UserConfig } from 'vite'\nimport type { TestUserConfig } from 'vitest/config'\nimport { DB_SHARED_POOL_ENV } from '@stratal/framework/database'\nimport { BINDING_ENV_VAR, DEFAULT_DB_BINDING } from '../database'\n\nconst require = createRequire(import.meta.url)\n\ntype CloudflareTestOptions = Parameters<typeof cloudflareTest>[0]\n\n/** String keys of `StratalEnv` whose value is a Hyperdrive binding. */\ntype HyperdriveKeys = Extract<\n { [K in keyof StratalEnv]-?: StratalEnv[K] extends Hyperdrive ? K : never }[keyof StratalEnv],\n string\n>\n\n/**\n * Names of declared Hyperdrive bindings, drawn from the consumer's augmented\n * `StratalEnv` (which extends `Cloudflare.Env`). Falls back to `string` only\n * when no Hyperdrive binding is declared (nothing to constrain to).\n */\ntype HyperdriveBindingName = [HyperdriveKeys] extends [never] ? string : HyperdriveKeys\n\n/** Stratal per-worker database configuration for {@link stratalTest}. */\nexport interface StratalTestDatabaseOptions {\n /**\n * Name of the Hyperdrive binding to point at this worker's database.\n * Defaults to `'DB'`. Constrained to declared Hyperdrive bindings.\n */\n binding?: HyperdriveBindingName\n}\n\ntype WorkersPoolOptions = Exclude<CloudflareTestOptions, (...args: never[]) => unknown>\ntype StratalTestOptions = WorkersPoolOptions & { database?: StratalTestDatabaseOptions }\n\nconst pgCjsResolvers = new Map<string, () => string>([\n ['pg-protocol', () => require.resolve('pg-protocol')],\n ['pg-connection-string', () => require.resolve('pg-connection-string')],\n ['pg-pool', () => require.resolve('pg-pool')],\n ['pg-cloudflare', () => path.join(path.dirname(require.resolve('pg-cloudflare')), 'index.js')],\n])\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `pg` sub-dependencies.\n *\n * `pg` is CJS but its dependencies (`pg-protocol`, `pg-connection-string`, `pg-pool`) ship\n * dual CJS/ESM exports. In workerd, the module fallback resolver prefers the ESM condition,\n * causing `SyntaxError: Cannot use import statement outside a module` when CJS `pg` does\n * `require()`. Additionally, `pg-cloudflare` uses a `workerd` export condition that the\n * root Vite instance doesn't resolve.\n *\n * Must be used at the **root** `defineConfig` level so that the\n * `@cloudflare/vitest-pool-workers` module fallback resolver (which uses the root Vite\n * instance) resolves pg sub-deps correctly.\n *\n * @example\n * ```ts\n * import { fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n * import { defineConfig } from 'vitest/config'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs()],\n * test: {\n * projects: [{\n * plugins: [stratalTest({ wrangler: { configPath: './wrangler.jsonc' } })],\n * test: { name: 'e2e', include: ['test/e2e/**\\/*.spec.ts'] },\n * }],\n * },\n * })\n * ```\n */\nexport const fixPgCjs = (): Plugin => ({\n name: 'stratal-pg-cjs',\n enforce: 'pre',\n resolveId(id) {\n const resolver = pgCjsResolvers.get(id)\n if (!resolver) return\n try {\n return resolver()\n } catch {\n return\n }\n },\n})\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `@noble/hashes` subpaths\n * used by `@paralleldrive/cuid2@2.x` (the version `@zenstackhq/orm` depends on).\n *\n * cuid2@2.x is CJS and does `require(\"@noble/hashes/sha3\")` without the `.js`\n * extension. In a workspace that also installs `@noble/hashes@2.x` (ESM-only),\n * the hoisted v2 package has no extensionless `./sha3` entry in its exports\n * map, so Vite/workerd resolution fails. This plugin routes the extensionless\n * subpaths through the consumer's nested `@noble/hashes@1.x` (a sibling of\n * cuid2 under `@zenstackhq/orm/node_modules`), which ships proper CJS exports.\n *\n * If the consumer doesn't depend on `@zenstackhq/orm`, the plugin is a no-op.\n * Otherwise it throws loudly at config-resolution time if the resolution chain\n * is unexpectedly broken — surfacing dependency drift instead of letting the\n * symptom resurface as an opaque test failure.\n *\n * Must be used at the **root** `defineConfig` level (same constraint as\n * `fixPgCjs`).\n *\n * @example\n * ```ts\n * import { fixNobleHashesCjs, fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs(), fixNobleHashesCjs()],\n * // ...\n * })\n * ```\n */\nexport const fixNobleHashesCjs = (): Plugin => {\n const ids = ['@noble/hashes/sha3', '@noble/hashes/crypto']\n let resolved: Map<string, string> | null = null\n\n return {\n name: 'stratal-noble-hashes-cjs',\n enforce: 'pre',\n configResolved(config) {\n const consumerRequire = createRequire(path.join(config.root, 'noop.js'))\n let zenstackPath: string\n try {\n zenstackPath = consumerRequire.resolve('@zenstackhq/orm')\n } catch {\n // Consumer doesn't use ZenStack — nothing to fix.\n return\n }\n const cuid2Path = createRequire(zenstackPath).resolve('@paralleldrive/cuid2')\n const cuid2Require = createRequire(cuid2Path)\n resolved = new Map<string, string>(\n ids.map((id) => [id, cuid2Require.resolve(id)]),\n )\n },\n resolveId(id) {\n return resolved?.get(id)\n },\n }\n}\n\nconst createStratalPlugin = (databaseEnabled: boolean): Plugin => ({\n name: 'stratal-test',\n config() {\n const config: UserConfig & { test?: TestUserConfig } = {\n resolve: {\n alias: {\n tslib: 'tslib/tslib.es6.mjs',\n '@zenstackhq/language/ast': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language/utils': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language': '@stratal/testing/mocks/zenstack-language',\n },\n },\n ssr: {\n noExternal: ['@zenstackhq/better-auth'],\n },\n }\n // Per-file DB isolation requires real file parallelism; each file gets its\n // own database, cloned from the migrated template, and resets between\n // tests. Setup hooks do real DB work (the clone itself — a\n // `CREATE DATABASE ... TEMPLATE` serialized across concurrent files by a\n // Postgres advisory lock — plus per-test tenant/seed provisioning in\n // `beforeAll`) that routinely exceeds Vitest's 10s default hook timeout\n // under a full worker slot — give a sensible floor. The advisory lock is\n // exactly the contention this timeout has to absorb, not a queue that's\n // been removed. Consumers with heavier setup override `hookTimeout` on\n // their own project.\n if (databaseEnabled) config.test = { fileParallelism: true, isolate: true, hookTimeout: 30_000 }\n return config\n },\n})\n\n/**\n * Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.\n *\n * Includes the cloudflare pool plugin and Stratal alias plugin. Pass a\n * `database` option to give each worker its own database (cloned once from a\n * migrated template, reset between tests) and enable file parallelism. Use\n * inside a project-level `plugins` array.\n *\n * **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.\n *\n * @param options - `cloudflareTest()` options plus Stratal `database` options\n * @returns An array of Vite plugins\n */\nexport function stratalTest(options: StratalTestOptions = {}): Plugin[] {\n const { database, ...cfOptions } = options\n const databaseEnabled = database !== undefined\n const binding = database?.binding ?? DEFAULT_DB_BINDING\n\n // Inject the DB-isolation env ONLY when the consumer opted in via `database`.\n // Its presence is how the testing-module builder tells \"isolation requested\"\n // from \"plain app\" — so it can hard-error on `database: {}` with no connection\n // string instead of silently running parallel files without isolation.\n const dbEnv = databaseEnabled\n ? {\n [BINDING_ENV_VAR]: binding,\n // Run the consuming app's DB connections on ONE shared pool per\n // connection: the test runtime hits a direct Postgres with no Hyperdrive\n // to multiplex, so a fresh pool per request resolution would exhaust\n // `max_connections` across parallel files. The framework's\n // `createPoolFactory(env, …)` reads this and memoizes the pool; in\n // dev/staging/prod the flag is absent → fresh-per-resolution (Hyperdrive\n // multiplexes). See `@stratal/framework/database`.\n [DB_SHARED_POOL_ENV]: 'true',\n }\n : {}\n const merged = {\n ...cfOptions,\n miniflare: {\n ...cfOptions.miniflare,\n bindings: {\n ...(cfOptions.miniflare?.bindings as Record<string, unknown> | undefined),\n ...dbEnv,\n },\n },\n }\n return [cloudflareTest(merged), createStratalPlugin(databaseEnabled)]\n}\n"],"mappings":";;;;;;AAUA,MAAM,UAAU,cAAc,OAAO,KAAK,GAAG;AA6B7C,MAAM,iCAAiB,IAAI,IAA0B;CACnD,CAAC,qBAAqB,QAAQ,QAAQ,aAAa,CAAC;CACpD,CAAC,8BAA8B,QAAQ,QAAQ,sBAAsB,CAAC;CACtE,CAAC,iBAAiB,QAAQ,QAAQ,SAAS,CAAC;CAC5C,CAAC,uBAAuB,KAAK,KAAK,KAAK,QAAQ,QAAQ,QAAQ,eAAe,CAAC,GAAG,UAAU,CAAC;AAC/F,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BD,MAAa,kBAA0B;CACrC,MAAM;CACN,SAAS;CACT,UAAU,IAAI;EACZ,MAAM,WAAW,eAAe,IAAI,EAAE;EACtC,IAAI,CAAC,UAAU;EACf,IAAI;GACF,OAAO,SAAS;EAClB,QAAQ;GACN;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,0BAAkC;CAC7C,MAAM,MAAM,CAAC,sBAAsB,sBAAsB;CACzD,IAAI,WAAuC;CAE3C,OAAO;EACL,MAAM;EACN,SAAS;EACT,eAAe,QAAQ;GACrB,MAAM,kBAAkB,cAAc,KAAK,KAAK,OAAO,MAAM,SAAS,CAAC;GACvE,IAAI;GACJ,IAAI;IACF,eAAe,gBAAgB,QAAQ,iBAAiB;GAC1D,QAAQ;IAEN;GACF;GAEA,MAAM,eAAe,cADH,cAAc,YAAY,CAAC,CAAC,QAAQ,sBACX,CAAC;GAC5C,WAAW,IAAI,IACb,IAAI,KAAK,OAAO,CAAC,IAAI,aAAa,QAAQ,EAAE,CAAC,CAAC,CAChD;EACF;EACA,UAAU,IAAI;GACZ,OAAO,UAAU,IAAI,EAAE;EACzB;CACF;AACF;AAEA,MAAM,uBAAuB,qBAAsC;CACjE,MAAM;CACN,SAAS;EACP,MAAM,SAAiD;GACrD,SAAS,EACP,OAAO;IACL,OAAO;IACP,4BAA4B;IAC5B,8BAA8B;IAC9B,wBAAwB;GAC1B,EACF;GACA,KAAK,EACH,YAAY,CAAC,yBAAyB,EACxC;EACF;EAWA,IAAI,iBAAiB,OAAO,OAAO;GAAE,iBAAiB;GAAM,SAAS;GAAM,aAAa;EAAO;EAC/F,OAAO;CACT;AACF;;;;;;;;;;;;;;AAeA,SAAgB,YAAY,UAA8B,CAAC,GAAa;CACtE,MAAM,EAAE,UAAU,GAAG,cAAc;CACnC,MAAM,kBAAkB,aAAa,KAAA;CACrC,MAAM,UAAU,UAAU,WAAA;CAM1B,MAAM,QAAQ,kBACV;GACG,kBAAkB;GAQlB,qBAAqB;CACxB,IACA,CAAC;CAWL,OAAO,CAAC,eAAe;EATrB,GAAG;EACH,WAAW;GACT,GAAG,UAAU;GACb,UAAU;IACR,GAAI,UAAU,WAAW;IACzB,GAAG;GACL;EACF;CAE0B,CAAC,GAAG,oBAAoB,eAAe,CAAC;AACtE"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"sourcesContent":["import { createRequire } from 'node:module'\nimport path from 'node:path'\n\nimport { cloudflareTest } from '@cloudflare/vitest-plugin'\nimport type { StratalEnv } from 'stratal'\nimport type { Plugin, UserConfig } from 'vite'\nimport type { TestUserConfig } from 'vitest/config'\nimport { DB_SHARED_POOL_ENV } from '@stratal/framework/database'\nimport { BINDING_ENV_VAR, DEFAULT_DB_BINDING } from '../database'\n\nconst require = createRequire(import.meta.url)\n\ntype CloudflareTestOptions = Parameters<typeof cloudflareTest>[0]\n\n/** String keys of `StratalEnv` whose value is a Hyperdrive binding. */\ntype HyperdriveKeys = Extract<\n { [K in keyof StratalEnv]-?: StratalEnv[K] extends Hyperdrive ? K : never }[keyof StratalEnv],\n string\n>\n\n/**\n * Names of declared Hyperdrive bindings, drawn from the consumer's augmented\n * `StratalEnv` (which extends `Cloudflare.Env`). Falls back to `string` only\n * when no Hyperdrive binding is declared (nothing to constrain to).\n */\ntype HyperdriveBindingName = [HyperdriveKeys] extends [never] ? string : HyperdriveKeys\n\n/** Stratal per-worker database configuration for {@link stratalTest}. */\nexport interface StratalTestDatabaseOptions {\n /**\n * Name of the Hyperdrive binding to point at this worker's database.\n * Defaults to `'DB'`. Constrained to declared Hyperdrive bindings.\n */\n binding?: HyperdriveBindingName\n}\n\ntype WorkersPoolOptions = Exclude<CloudflareTestOptions, (...args: never[]) => unknown>\ntype StratalTestOptions = WorkersPoolOptions & { database?: StratalTestDatabaseOptions }\n\nconst pgCjsResolvers = new Map<string, () => string>([\n ['pg-protocol', () => require.resolve('pg-protocol')],\n ['pg-connection-string', () => require.resolve('pg-connection-string')],\n ['pg-pool', () => require.resolve('pg-pool')],\n ['pg-cloudflare', () => path.join(path.dirname(require.resolve('pg-cloudflare')), 'index.js')],\n])\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `pg` sub-dependencies.\n *\n * `pg` is CJS but its dependencies (`pg-protocol`, `pg-connection-string`, `pg-pool`) ship\n * dual CJS/ESM exports. In workerd, the module fallback resolver prefers the ESM condition,\n * causing `SyntaxError: Cannot use import statement outside a module` when CJS `pg` does\n * `require()`. Additionally, `pg-cloudflare` uses a `workerd` export condition that the\n * root Vite instance doesn't resolve.\n *\n * Must be used at the **root** `defineConfig` level so that the\n * `@cloudflare/vitest-plugin` module fallback resolver (which uses the root Vite\n * instance) resolves pg sub-deps correctly.\n *\n * @example\n * ```ts\n * import { fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n * import { defineConfig } from 'vitest/config'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs()],\n * test: {\n * projects: [{\n * plugins: [stratalTest({ wrangler: { configPath: './wrangler.jsonc' } })],\n * test: { name: 'e2e', include: ['test/e2e/**\\/*.spec.ts'] },\n * }],\n * },\n * })\n * ```\n */\nexport const fixPgCjs = (): Plugin => ({\n name: 'stratal-pg-cjs',\n enforce: 'pre',\n resolveId(id) {\n const resolver = pgCjsResolvers.get(id)\n if (!resolver) return\n try {\n return resolver()\n } catch {\n return\n }\n },\n})\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `@noble/hashes` subpaths\n * used by `@paralleldrive/cuid2@2.x` (the version `@zenstackhq/orm` depends on).\n *\n * cuid2@2.x is CJS and does `require(\"@noble/hashes/sha3\")` without the `.js`\n * extension. In a workspace that also installs `@noble/hashes@2.x` (ESM-only),\n * the hoisted v2 package has no extensionless `./sha3` entry in its exports\n * map, so Vite/workerd resolution fails. This plugin routes the extensionless\n * subpaths through the consumer's nested `@noble/hashes@1.x` (a sibling of\n * cuid2 under `@zenstackhq/orm/node_modules`), which ships proper CJS exports.\n *\n * If the consumer doesn't depend on `@zenstackhq/orm`, the plugin is a no-op.\n * Otherwise it throws loudly at config-resolution time if the resolution chain\n * is unexpectedly broken — surfacing dependency drift instead of letting the\n * symptom resurface as an opaque test failure.\n *\n * Must be used at the **root** `defineConfig` level (same constraint as\n * `fixPgCjs`).\n *\n * @example\n * ```ts\n * import { fixNobleHashesCjs, fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs(), fixNobleHashesCjs()],\n * // ...\n * })\n * ```\n */\nexport const fixNobleHashesCjs = (): Plugin => {\n const ids = ['@noble/hashes/sha3', '@noble/hashes/crypto']\n let resolved: Map<string, string> | null = null\n\n return {\n name: 'stratal-noble-hashes-cjs',\n enforce: 'pre',\n configResolved(config) {\n const consumerRequire = createRequire(path.join(config.root, 'noop.js'))\n let zenstackPath: string\n try {\n zenstackPath = consumerRequire.resolve('@zenstackhq/orm')\n } catch {\n // Consumer doesn't use ZenStack — nothing to fix.\n return\n }\n const cuid2Path = createRequire(zenstackPath).resolve('@paralleldrive/cuid2')\n const cuid2Require = createRequire(cuid2Path)\n resolved = new Map<string, string>(\n ids.map((id) => [id, cuid2Require.resolve(id)]),\n )\n },\n resolveId(id) {\n return resolved?.get(id)\n },\n }\n}\n\n/** Builds the Stratal defaults plugin returned by {@link stratalTest}; exported for direct unit testing. */\nexport const createStratalPlugin = (databaseEnabled: boolean): Plugin => ({\n name: 'stratal-test',\n config(userConfig: UserConfig & { test?: TestUserConfig }) {\n const config: UserConfig & { test?: TestUserConfig } = {\n resolve: {\n alias: {\n tslib: 'tslib/tslib.es6.mjs',\n '@zenstackhq/language/ast': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language/utils': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language': '@stratal/testing/mocks/zenstack-language',\n },\n },\n ssr: {\n noExternal: ['@zenstackhq/better-auth'],\n },\n }\n // Each test file leases a worker database for as long as its isolate lives\n // and gets a fresh clone of the migrated template in it. `isolate` is\n // required, not a default: the lease is released when the pool disposes the\n // file's isolate, so a shared isolate would hand the next file the previous\n // file's database, rows and all. Setup hooks do real DB work (the clone\n // itself — a `CREATE DATABASE ... TEMPLATE` serialized across concurrent\n // files by a Postgres advisory lock — plus per-test tenant/seed provisioning\n // in `beforeAll`) that routinely exceeds Vitest's 10s default hook timeout\n // under a full worker slot. `fileParallelism` and `hookTimeout` are\n // defaults, not floors: each only applies where the consuming project's own\n // `test` config leaves it unset.\n if (databaseEnabled) {\n const existing = userConfig.test ?? {}\n if (existing.isolate === false) {\n throw new Error(\n '[stratal-testing] stratalTest({ database }) needs `isolate: true`. Each test file leases its ' +\n 'database for the life of its isolate, so files sharing an isolate would share a database. ' +\n 'Remove `isolate: false` from this project.',\n )\n }\n config.test = {\n fileParallelism: existing.fileParallelism ?? true,\n isolate: true,\n hookTimeout: existing.hookTimeout ?? 30_000,\n }\n }\n return config\n },\n})\n\n/**\n * Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.\n *\n * Includes the cloudflare pool plugin and Stratal alias plugin. Pass a\n * `database` option to give each test file a leased database holding a fresh\n * clone of a migrated template, and to enable file parallelism. Use inside a\n * project-level `plugins` array.\n *\n * **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.\n *\n * @param options - `cloudflareTest()` options plus Stratal `database` options\n * @returns An array of Vite plugins\n */\nexport function stratalTest(options: StratalTestOptions = {}): Plugin[] {\n const { database, ...cfOptions } = options\n const databaseEnabled = database !== undefined\n const binding = database?.binding ?? DEFAULT_DB_BINDING\n\n // Inject the DB-isolation env ONLY when the consumer opted in via `database`.\n // Its presence is how the testing-module builder tells \"isolation requested\"\n // from \"plain app\" — so it can hard-error on `database: {}` with no connection\n // string instead of silently running parallel files without isolation.\n // Annotated rather than inferred: the ternary would otherwise widen to\n // `{ …: string } | { …?: undefined }`, and spreading that union produces\n // optionally-`undefined` bindings, which miniflare's `Record<string, Json>`\n // rejects.\n const dbEnv: Record<string, string> = databaseEnabled\n ? {\n [BINDING_ENV_VAR]: binding,\n // Run the consuming app's DB connections on ONE shared pool per\n // connection: the test runtime hits a direct Postgres with no Hyperdrive\n // to multiplex, so a fresh pool per request resolution would exhaust\n // `max_connections` across parallel files. The framework's\n // `createPoolFactory(env, …)` reads this and memoizes the pool; in\n // dev/staging/prod the flag is absent → fresh-per-resolution (Hyperdrive\n // multiplexes). See `@stratal/framework/database`.\n [DB_SHARED_POOL_ENV]: 'true',\n }\n : {}\n const merged = {\n ...cfOptions,\n miniflare: {\n ...cfOptions.miniflare,\n bindings: {\n ...cfOptions.miniflare?.bindings,\n ...dbEnv,\n },\n },\n }\n return [cloudflareTest(merged), createStratalPlugin(databaseEnabled)]\n}\n"],"mappings":";;;;;;AAUA,MAAM,UAAU,cAAc,YAAY,GAAG;AA6B7C,MAAM,iCAAiB,IAAI,IAA0B;CACnD,CAAC,qBAAqB,QAAQ,QAAQ,aAAa,CAAC;CACpD,CAAC,8BAA8B,QAAQ,QAAQ,sBAAsB,CAAC;CACtE,CAAC,iBAAiB,QAAQ,QAAQ,SAAS,CAAC;CAC5C,CAAC,uBAAuB,KAAK,KAAK,KAAK,QAAQ,QAAQ,QAAQ,eAAe,CAAC,GAAG,UAAU,CAAC;AAC/F,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BD,MAAa,kBAA0B;CACrC,MAAM;CACN,SAAS;CACT,UAAU,IAAI;EACZ,MAAM,WAAW,eAAe,IAAI,EAAE;EACtC,IAAI,CAAC,UAAU;EACf,IAAI;GACF,OAAO,SAAS;EAClB,QAAQ;GACN;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,0BAAkC;CAC7C,MAAM,MAAM,CAAC,sBAAsB,sBAAsB;CACzD,IAAI,WAAuC;CAE3C,OAAO;EACL,MAAM;EACN,SAAS;EACT,eAAe,QAAQ;GACrB,MAAM,kBAAkB,cAAc,KAAK,KAAK,OAAO,MAAM,SAAS,CAAC;GACvE,IAAI;GACJ,IAAI;IACF,eAAe,gBAAgB,QAAQ,iBAAiB;GAC1D,QAAQ;IAEN;GACF;GACA,MAAM,YAAY,cAAc,YAAY,CAAC,CAAC,QAAQ,sBAAsB;GAC5E,MAAM,eAAe,cAAc,SAAS;GAC5C,WAAW,IAAI,IACb,IAAI,KAAK,OAAO,CAAC,IAAI,aAAa,QAAQ,EAAE,CAAC,CAAC,CAChD;EACF;EACA,UAAU,IAAI;GACZ,OAAO,UAAU,IAAI,EAAE;EACzB;CACF;AACF;;AAGA,MAAa,uBAAuB,qBAAsC;CACxE,MAAM;CACN,OAAO,YAAoD;EACzD,MAAM,SAAiD;GACrD,SAAS,EACP,OAAO;IACL,OAAO;IACP,4BAA4B;IAC5B,8BAA8B;IAC9B,wBAAwB;GAC1B,EACF;GACA,KAAK,EACH,YAAY,CAAC,yBAAyB,EACxC;EACF;EAYA,IAAI,iBAAiB;GACnB,MAAM,WAAW,WAAW,QAAQ,CAAC;GACrC,IAAI,SAAS,YAAY,OACvB,MAAM,IAAI,MACR,mOAGF;GAEF,OAAO,OAAO;IACZ,iBAAiB,SAAS,mBAAmB;IAC7C,SAAS;IACT,aAAa,SAAS,eAAe;GACvC;EACF;EACA,OAAO;CACT;AACF;;;;;;;;;;;;;;AAeA,SAAgB,YAAY,UAA8B,CAAC,GAAa;CACtE,MAAM,EAAE,UAAU,GAAG,cAAc;CACnC,MAAM,kBAAkB,aAAa,KAAA;CACrC,MAAM,UAAU,UAAU,WAAA;CAU1B,MAAM,QAAgC,kBAClC;GACG,kBAAkB;GAQlB,qBAAqB;CACxB,IACA,CAAC;CACL,MAAM,SAAS;EACb,GAAG;EACH,WAAW;GACT,GAAG,UAAU;GACb,UAAU;IACR,GAAG,UAAU,WAAW;IACxB,GAAG;GACL;EACF;CACF;CACA,OAAO,CAAC,eAAe,MAAM,GAAG,oBAAoB,eAAe,CAAC;AACtE"}
|