@apiquest/fracture 1.0.4 → 1.0.5

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.
Files changed (59) hide show
  1. package/README.md +90 -2
  2. package/dist/CollectionRunner.d.ts +2 -0
  3. package/dist/CollectionRunner.d.ts.map +1 -1
  4. package/dist/CollectionRunner.js +20 -2
  5. package/dist/CollectionRunner.js.map +1 -1
  6. package/dist/LibraryLoader.d.ts +49 -0
  7. package/dist/LibraryLoader.d.ts.map +1 -0
  8. package/dist/LibraryLoader.js +198 -0
  9. package/dist/LibraryLoader.js.map +1 -0
  10. package/dist/PluginLoader.d.ts.map +1 -1
  11. package/dist/PluginLoader.js +9 -6
  12. package/dist/PluginLoader.js.map +1 -1
  13. package/dist/PluginResolver.d.ts +1 -1
  14. package/dist/PluginResolver.d.ts.map +1 -1
  15. package/dist/PluginResolver.js +1 -1
  16. package/dist/PluginResolver.js.map +1 -1
  17. package/dist/ScriptEngine.d.ts +2 -1
  18. package/dist/ScriptEngine.d.ts.map +1 -1
  19. package/dist/ScriptEngine.js +15 -8
  20. package/dist/ScriptEngine.js.map +1 -1
  21. package/dist/cli/index.js +35 -3
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/plugin-commands.d.ts.map +1 -1
  24. package/dist/cli/plugin-commands.js +47 -81
  25. package/dist/cli/plugin-commands.js.map +1 -1
  26. package/dist/cli/plugin-installer.d.ts +48 -0
  27. package/dist/cli/plugin-installer.d.ts.map +1 -0
  28. package/dist/cli/plugin-installer.js +136 -0
  29. package/dist/cli/plugin-installer.js.map +1 -0
  30. package/dist/cli/plugin-registry.d.ts +17 -0
  31. package/dist/cli/plugin-registry.d.ts.map +1 -0
  32. package/dist/cli/plugin-registry.js +77 -0
  33. package/dist/cli/plugin-registry.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/CollectionAnalyzer.ts +0 -102
  36. package/src/CollectionRunner.ts +0 -1423
  37. package/src/CollectionRunner.types.ts +0 -9
  38. package/src/CollectionValidator.ts +0 -289
  39. package/src/ConsoleReporter.ts +0 -143
  40. package/src/CookieJar.ts +0 -258
  41. package/src/DagScheduler.ts +0 -439
  42. package/src/Logger.ts +0 -85
  43. package/src/PluginLoader.ts +0 -126
  44. package/src/PluginManager.ts +0 -208
  45. package/src/PluginResolver.ts +0 -154
  46. package/src/QuestAPI.ts +0 -764
  47. package/src/QuestAPI.types.ts +0 -33
  48. package/src/QuestTestAPI.ts +0 -164
  49. package/src/RequestFilter.ts +0 -224
  50. package/src/ScriptEngine.ts +0 -219
  51. package/src/ScriptValidator.ts +0 -428
  52. package/src/TaskGraph.ts +0 -598
  53. package/src/TestCounter.ts +0 -109
  54. package/src/VariableResolver.ts +0 -114
  55. package/src/cli/index.ts +0 -480
  56. package/src/cli/plugin-commands.ts +0 -342
  57. package/src/cli/plugin-discovery.ts +0 -44
  58. package/src/index.ts +0 -24
  59. package/src/utils.ts +0 -52
@@ -1,33 +0,0 @@
1
- // Type definitions for Quest API
2
-
3
- export interface RequestConfig {
4
- url: string;
5
- method?: string;
6
- header?: Record<string, string>;
7
- headers?: Record<string, string>;
8
- body?: string | RequestBody;
9
- }
10
-
11
- export interface RequestBody {
12
- mode?: 'raw' | 'urlencoded' | 'formdata';
13
- raw?: string;
14
- urlencoded?: Array<{ key: string; value: string }>;
15
- formdata?: Array<{ key: string; value: string }>;
16
- }
17
-
18
- export interface ResponseObject {
19
- status: number;
20
- statusText: string;
21
- body: string;
22
- headers: Record<string, string | string[]>;
23
- time: number;
24
- json(): unknown | null;
25
- text(): string;
26
- }
27
-
28
- export interface HistoryFilterCriteria {
29
- path?: string;
30
- name?: string;
31
- iteration?: number;
32
- id?: string;
33
- }
@@ -1,164 +0,0 @@
1
- import type { TestResult } from '@apiquest/types';
2
- import { ScriptType } from '@apiquest/types';
3
-
4
- /**
5
- * Special error class for quest.skip()
6
- */
7
- class SkipError extends Error {
8
- public readonly skipReason: string;
9
-
10
- constructor(message: string) {
11
- super(message);
12
- this.name = 'SkipError';
13
- this.skipReason = message;
14
- }
15
- }
16
-
17
- function isPromise(value: unknown): value is Promise<void> {
18
- return value !== null &&
19
- value !== undefined &&
20
- typeof value === 'object' &&
21
- typeof (value as { then?: unknown }).then === 'function';
22
- }
23
-
24
- /**
25
- * Creates the test API methods (quest.test, quest.skip, quest.fail)
26
- * These methods allow scripts to define and control test assertions
27
- */
28
- export function createQuestTestAPI(
29
- tests: TestResult[],
30
- scriptType: ScriptType,
31
- emitAssertion: (test: TestResult) => void,
32
- abortSignal?: AbortSignal
33
- ): {
34
- test: (name: string, fn: () => void | Promise<void>) => void;
35
- skip: (reason: string) => never;
36
- fail: (message: string) => never;
37
- } {
38
- let isInsideTest = false;
39
-
40
- return {
41
- /**
42
- * Define a test assertion
43
- * Can only be called in postRequestScript or plugin event scripts
44
- */
45
- test(name: string, fn: () => void | Promise<void>) {
46
- // Enforce: tests can only be called in request post scripts (never pre, folder, or collection)
47
- const allowedScriptTypes = [
48
- ScriptType.PostRequest,
49
- ScriptType.PluginEvent
50
- ];
51
- if (!allowedScriptTypes.includes(scriptType)) {
52
- throw new Error(
53
- `quest.test() can only be called in request post scripts. ` +
54
- `Current script type: ${scriptType}. ` +
55
- `Tests require request/response context and cannot be used in ` +
56
- `collectionPost, folderPost, or pre-request scripts.`
57
- );
58
- }
59
-
60
- // Check abort signal - skip test if already aborted
61
- if (abortSignal?.aborted === true) {
62
- const testResult: TestResult = {
63
- name,
64
- passed: false,
65
- skipped: true,
66
- error: 'Test skipped - execution aborted'
67
- };
68
- tests.push(testResult);
69
- emitAssertion(testResult);
70
- return;
71
- }
72
-
73
- isInsideTest = true;
74
- try {
75
- const result = fn();
76
-
77
- // If fn returns a Promise, handle it
78
- if (isPromise(result)) {
79
- result
80
- .then(() => {
81
- const testResult: TestResult = {
82
- name,
83
- passed: true,
84
- skipped: false
85
- };
86
- tests.push(testResult);
87
- emitAssertion(testResult);
88
- })
89
- .catch((error: unknown) => {
90
- // Check if this is a skip error
91
- const testResult: TestResult = error instanceof SkipError
92
- ? {
93
- name,
94
- passed: false,
95
- skipped: true,
96
- error: error.skipReason
97
- }
98
- : {
99
- name,
100
- passed: false,
101
- skipped: false,
102
- error: (error as { message?: string }).message ?? String(error)
103
- };
104
- tests.push(testResult);
105
- emitAssertion(testResult);
106
- })
107
- .finally(() => {
108
- isInsideTest = false;
109
- });
110
- } else {
111
- // Synchronous test
112
- const testResult: TestResult = {
113
- name,
114
- passed: true,
115
- skipped: false
116
- };
117
- tests.push(testResult);
118
- emitAssertion(testResult);
119
- isInsideTest = false;
120
- }
121
- } catch (error: unknown) {
122
- // Check if this is a skip error
123
- const testResult: TestResult = error instanceof SkipError
124
- ? {
125
- name,
126
- passed: false,
127
- skipped: true,
128
- error: error.skipReason // Include skip reason in error field
129
- }
130
- : {
131
- name,
132
- passed: false,
133
- skipped: false,
134
- error: (error as { message?: string }).message ?? String(error)
135
- };
136
- tests.push(testResult);
137
- emitAssertion(testResult);
138
- isInsideTest = false;
139
- }
140
- },
141
-
142
- /**
143
- * Skip the current test
144
- * Must be called inside quest.test() callback
145
- */
146
- skip(reason: string): never {
147
- if (!isInsideTest) {
148
- throw new Error('quest.skip() must be called inside quest.test() callback');
149
- }
150
- throw new SkipError(reason);
151
- },
152
-
153
- /**
154
- * Fail the current test with custom message
155
- * Must be called inside quest.test() callback
156
- */
157
- fail(message: string): never {
158
- if (!isInsideTest) {
159
- throw new Error('quest.fail() must be called inside quest.test() callback');
160
- }
161
- throw new Error(message);
162
- }
163
- };
164
- }
@@ -1,224 +0,0 @@
1
- import type { Collection, CollectionItem, Folder, Request, PathType } from '@apiquest/types';
2
-
3
- export interface FilterOptions {
4
- filter?: string;
5
- excludeDeps?: boolean;
6
- pruneEmptyFolders?: boolean; // Default: true
7
- }
8
-
9
- interface ItemWithPath {
10
- item: CollectionItem;
11
- path: PathType;
12
- }
13
-
14
- export class RequestFilter {
15
- /**
16
- * Filter collection removing non-matching requests and empty folders
17
- * Returns filtered copy or original if no filtering
18
- */
19
- static filterCollection(collection: Collection, options: FilterOptions): Collection {
20
- if (options.filter === undefined) {
21
- return collection;
22
- }
23
-
24
- const filterSet = this.getFilterSet(collection, options);
25
- if (filterSet === null) {
26
- return collection;
27
- }
28
-
29
- const pruneEmpty = options.pruneEmptyFolders !== false;
30
-
31
- // Clone collection and filter items
32
- const filtered: Collection = {
33
- ...collection,
34
- items: this.filterItems(collection.items, filterSet, pruneEmpty)
35
- };
36
-
37
- return filtered;
38
- }
39
-
40
- /**
41
- * Recursively filter items keeping only requests in filterSet
42
- * Optionally prune empty folders
43
- */
44
- private static filterItems(
45
- items: CollectionItem[],
46
- filterSet: Set<string>,
47
- pruneEmpty: boolean
48
- ): CollectionItem[] {
49
- const filtered: CollectionItem[] = [];
50
-
51
- for (const item of items) {
52
- if (item.type === 'request') {
53
- if (filterSet.has(item.id)) {
54
- filtered.push(item);
55
- }
56
- } else {
57
- // Folder: recursively filter children
58
- const filteredChildren = this.filterItems(item.items, filterSet, pruneEmpty);
59
-
60
- if (!pruneEmpty || filteredChildren.length > 0) {
61
- filtered.push({
62
- ...item,
63
- items: filteredChildren
64
- });
65
- }
66
- }
67
- }
68
-
69
- return filtered;
70
- }
71
-
72
- /**
73
- * Get set of request IDs to execute
74
- * Returns null if no filtering needed
75
- */
76
- private static getFilterSet(collection: Collection, options: FilterOptions): Set<string> | null {
77
- if (options.filter === undefined) {
78
- return null;
79
- }
80
-
81
- // Collect requests matching filter
82
- const matchingIds = new Set<string>();
83
- try {
84
- const filterRegex = new RegExp(options.filter);
85
-
86
- // Walk collection structure and match paths
87
- this.collectMatchingRequests(collection.items, 'collection:/', filterRegex, matchingIds);
88
- } catch (error) {
89
- // Invalid regex - no filtering
90
- return null;
91
- }
92
-
93
- if (options.excludeDeps !== true) {
94
- return this.includeDependencies(collection, matchingIds);
95
- }
96
-
97
- return matchingIds;
98
- }
99
-
100
- /**
101
- * Build path with proper type prefix (same logic as TaskGraph.buildPath)
102
- */
103
- private static buildPath(parent: string, name: string, type: 'folder' | 'request'): PathType {
104
- // If parent is collection:/
105
- if (parent === 'collection:/') {
106
- return `${type}:/${name}` as PathType;
107
- }
108
-
109
- // Remove type prefix from parent path
110
- const basePath = parent.replace(/^(folder|request):\//, '');
111
- return `${type}:/${basePath}/${name}` as PathType;
112
- }
113
-
114
- /**
115
- * Collect requests matching path filter (regex)
116
- * Matches against both folder and request paths
117
- * If a folder matches, all requests in that folder are included
118
- */
119
- private static collectMatchingRequests(
120
- items: CollectionItem[],
121
- parentPath: string,
122
- filterRegex: RegExp,
123
- result: Set<string>
124
- ): void {
125
- for (const item of items) {
126
- if (item.type === 'folder') {
127
- const folderPath = this.buildPath(parentPath, item.name, 'folder');
128
- const folderMatches = filterRegex.test(folderPath);
129
-
130
- if (folderMatches) {
131
- // Folder matches - include ALL requests in this folder
132
- this.collectAllRequests(item.items, result);
133
- }
134
-
135
- // Always recurse to children
136
- this.collectMatchingRequests(item.items, folderPath, filterRegex, result);
137
- } else {
138
- // Request
139
- const requestPath = this.buildPath(parentPath, item.name, 'request');
140
- const requestMatches = filterRegex.test(requestPath);
141
-
142
- if (requestMatches) {
143
- result.add(item.id);
144
- }
145
- }
146
- }
147
- }
148
-
149
- /**
150
- * Collect all requests from items (helper for when folder matches)
151
- */
152
- private static collectAllRequests(items: CollectionItem[], result: Set<string>): void {
153
- for (const item of items) {
154
- if (item.type === 'request') {
155
- result.add(item.id);
156
- } else {
157
- this.collectAllRequests(item.items, result);
158
- }
159
- }
160
- }
161
-
162
- /**
163
- * Include dependencies by walking collection and resolving dependsOn
164
- */
165
- private static includeDependencies(
166
- collection: Collection,
167
- matchingIds: Set<string>
168
- ): Set<string> {
169
- const result = new Set<string>();
170
- const depMap = new Map<string, string[]>();
171
-
172
- // Build dependency map
173
- this.buildDependencyMap(collection.items, depMap);
174
-
175
- // Resolve dependencies recursively
176
- for (const requestId of matchingIds) {
177
- this.resolveDependencies(requestId, depMap, result);
178
- }
179
-
180
- return result;
181
- }
182
-
183
- /**
184
- * Build map of request/folder ID -> dependencies
185
- */
186
- private static buildDependencyMap(
187
- items: CollectionItem[],
188
- depMap: Map<string, string[]>
189
- ): void {
190
- for (const item of items) {
191
- if (item.type === 'request') {
192
- depMap.set(item.id, item.dependsOn ?? []);
193
- } else {
194
- // Folders can also have dependencies
195
- const folder = item;
196
- const folderDeps = folder.dependsOn;
197
- if (folderDeps !== undefined && folderDeps.length > 0) {
198
- depMap.set(folder.id, folderDeps);
199
- }
200
-
201
- // Recurse to children
202
- this.buildDependencyMap(folder.items, depMap);
203
- }
204
- }
205
- }
206
-
207
- /**
208
- * Recursively resolve dependencies for a request/folder
209
- */
210
- private static resolveDependencies(
211
- itemId: string,
212
- depMap: Map<string, string[]>,
213
- result: Set<string>
214
- ): void {
215
- result.add(itemId);
216
-
217
- const deps = depMap.get(itemId) ?? [];
218
- for (const depId of deps) {
219
- if (!result.has(depId)) {
220
- this.resolveDependencies(depId, depMap, result);
221
- }
222
- }
223
- }
224
- }
@@ -1,219 +0,0 @@
1
- import vm from 'vm';
2
- import { expect } from 'chai';
3
- import _ from 'lodash';
4
- import type { ExecutionContext, ScriptResult, TestResult } from '@apiquest/types';
5
- import { ScriptType } from '@apiquest/types';
6
- import { Logger } from './Logger.js';
7
- import { createQuestAPI } from './QuestAPI.js';
8
- import { isNullOrWhitespace } from './utils.js';
9
-
10
- interface ConsoleAPI {
11
- log(...args: unknown[]): void;
12
- info(...args: unknown[]): void;
13
- warn(...args: unknown[]): void;
14
- error(...args: unknown[]): void;
15
- }
16
-
17
- export class ScriptEngine {
18
- private tests: TestResult[] = [];
19
- private consoleOutput: string[] = [];
20
- private logger: Logger;
21
-
22
- constructor(baseLogger?: Logger) {
23
- this.logger = baseLogger?.createLogger('ScriptEngine') ?? new Logger('ScriptEngine');
24
- }
25
-
26
- /**
27
- * Execute a script in VM context with quest API
28
- */
29
- async execute(
30
- script: string,
31
- context: ExecutionContext,
32
- scriptType: ScriptType,
33
- emitAssertion: (test: TestResult) => void
34
- ): Promise<ScriptResult> {
35
- if (isNullOrWhitespace(script)) {
36
- this.logger.trace('Empty script, skipping execution');
37
- return {
38
- success: true,
39
- tests: [],
40
- consoleOutput: []
41
- };
42
- }
43
-
44
- const scriptPreview = script.length > 100 ? script.substring(0, 100) + '...' : script;
45
- this.logger.debug(`Executing ${scriptType} script (${script.length} chars)`);
46
- this.logger.trace(`Script preview: ${scriptPreview}`);
47
-
48
- this.tests = [];
49
- this.consoleOutput = [];
50
-
51
- try {
52
- this.logger.trace('Creating quest API sandbox');
53
- const questAPI = createQuestAPI(context, scriptType, this.tests, emitAssertion);
54
-
55
- const sandbox = {
56
- quest: questAPI,
57
- expect,
58
- _,
59
- console: this.createConsoleAPI(),
60
- require: this.createRequire(),
61
- setTimeout,
62
- setInterval,
63
- clearTimeout,
64
- clearInterval,
65
- Promise,
66
- Buffer,
67
- AbortController,
68
- AbortSignal,
69
- signal: context.abortSignal
70
- };
71
-
72
- const vmContext = vm.createContext(sandbox);
73
- this.logger.trace('VM context created');
74
-
75
- // Wrap script in async function to support top-level await
76
- const wrappedScript = `
77
- (async () => {
78
- ${script}
79
- })()
80
- `;
81
-
82
- // Execute script and get the promise
83
- const startTime = Date.now();
84
- const result = vm.runInContext(wrappedScript, vmContext, {
85
- timeout: 30000,
86
- displayErrors: true
87
- }) as unknown;
88
-
89
- // If result is a promise, wait for it
90
- if (result !== null && result !== undefined && typeof (result as { then?: unknown }).then === 'function') {
91
- await (result as Promise<void>);
92
- }
93
-
94
- // Wait for any other pending promises
95
- await new Promise(resolve => setImmediate(resolve));
96
-
97
- const duration = Date.now() - startTime;
98
- this.logger.debug(`Script executed successfully in ${duration}ms`);
99
- this.logger.trace(`Tests collected: ${this.tests.length}, Console output: ${this.consoleOutput.length} lines`);
100
-
101
- return {
102
- success: true,
103
- tests: this.tests,
104
- consoleOutput: this.consoleOutput
105
- };
106
- } catch (error: unknown) {
107
- const errorMsg = (error as { message?: string }).message ?? String(error);
108
- const errorStack = (error as { stack?: string }).stack;
109
- const errorName = (error as { name?: string }).name;
110
-
111
- // Check if this was an abort
112
- if (errorName === 'AbortError' || errorMsg.includes('abort') || errorMsg.includes('Abort')) {
113
- this.logger.debug('Script execution interrupted by abort signal');
114
- return {
115
- success: false,
116
- tests: this.tests,
117
- error: 'Script aborted',
118
- consoleOutput: this.consoleOutput
119
- };
120
- }
121
-
122
- this.logger.error(`Script execution failed: ${errorMsg}`);
123
- if (errorStack !== undefined) {
124
- this.logger.trace(`Error stack: ${errorStack}`);
125
- }
126
- return {
127
- success: false,
128
- tests: this.tests,
129
- error: errorMsg,
130
- consoleOutput: this.consoleOutput
131
- };
132
- }
133
- }
134
-
135
- /**
136
- * Create console API that captures output
137
- */
138
- private createConsoleAPI(): ConsoleAPI {
139
- const self = this;
140
-
141
- const safeStringify = (value: unknown): string => {
142
- if (typeof value === 'string') return value;
143
- if (value === null || value === undefined) return String(value);
144
- if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
145
- if (typeof value === 'symbol') return value.toString();
146
- if (typeof value === 'function') return '[Function]';
147
-
148
- try {
149
- const seen = new WeakSet<object>();
150
- return JSON.stringify(
151
- value,
152
- (_key, val: unknown) => {
153
- if (typeof val === 'bigint') {
154
- return val.toString();
155
- }
156
- if (typeof val === 'function') {
157
- return '[Function]';
158
- }
159
- if (typeof val === 'symbol') {
160
- return val.toString();
161
- }
162
- if (typeof val === 'object' && val !== null) {
163
- if (seen.has(val)) {
164
- return '[Circular]';
165
- }
166
- seen.add(val);
167
- }
168
- return val as string | number | boolean | null;
169
- },
170
- 2
171
- );
172
- } catch {
173
- return String(value);
174
- }
175
- };
176
-
177
- return {
178
- log(...args: unknown[]) {
179
- const message = args.map(safeStringify).join(' ');
180
- self.consoleOutput.push(message);
181
- console.log(message);
182
- },
183
- info(...args: unknown[]) {
184
- const message = args.map(safeStringify).join(' ');
185
- self.consoleOutput.push(`[INFO] ${message}`);
186
- console.info(message);
187
- },
188
- warn(...args: unknown[]) {
189
- const message = args.map(safeStringify).join(' ');
190
- self.consoleOutput.push(`[WARN] ${message}`);
191
- console.warn(message);
192
- },
193
- error(...args: unknown[]) {
194
- const message = args.map(safeStringify).join(' ');
195
- self.consoleOutput.push(`[ERROR] ${message}`);
196
- console.error(message);
197
- }
198
- };
199
- }
200
-
201
- /**
202
- * Create minimal require function
203
- */
204
- private createRequire() {
205
- return (moduleName: string) => {
206
- const allowedModules: Record<string, unknown> = {
207
- 'chai': require('chai') as unknown,
208
- 'lodash': require('lodash') as unknown,
209
- 'moment': require('moment') as unknown
210
- };
211
-
212
- if (moduleName in allowedModules) {
213
- return allowedModules[moduleName];
214
- }
215
-
216
- throw new Error(`Module '${moduleName}' is not allowed in scripts`);
217
- };
218
- }
219
- }