aimyapi 1.0.8 → 1.0.10
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/LICENSE +21 -21
- package/README.md +221 -195
- package/dist/index.cjs +233 -110
- package/dist/index.d.cts +16 -8
- package/dist/index.d.mts +16 -8
- package/dist/index.d.ts +16 -8
- package/dist/index.js +233 -110
- package/dist/index.mjs +235 -112
- package/example/example1.ts +41 -30
- package/example/example2.ts +58 -58
- package/example/example3.ts +69 -69
- package/example/ordering_api.md +94 -94
- package/example/ordering_api.ts +191 -191
- package/example/ordering_api_impl.ts +207 -207
- package/example/sales_api.md +1 -5
- package/example/sales_api.ts +31 -29
- package/example/sales_api_impl.ts +74 -60
- package/package.json +2 -3
- package/prompts/generate-code-prompt.md +37 -0
- package/src/index.ts +79 -55
- package/src/log-tester-inject.ts +233 -0
- package/src/log-tester.ts +185 -0
- package/src/logger.ts +113 -0
- package/src/sandbox-wrappers.ts +296 -249
- package/src/sandbox.ts +35 -58
- package/src/utils.ts +33 -3
- package/tsconfig.json +12 -12
- package/prompts/create-task-prompt.md +0 -37
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
Most recent user query:
|
|
3
|
+
{{USER_QUERY}}
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Based on the prior conversation, write a typescript script that performs the operation requested by the user and call the `submitCode` tool with the script.
|
|
8
|
+
|
|
9
|
+
## API documentation
|
|
10
|
+
|
|
11
|
+
You can make use of this api:
|
|
12
|
+
|
|
13
|
+
`api.ts`:
|
|
14
|
+
```ts
|
|
15
|
+
{{API}}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Instructions
|
|
19
|
+
|
|
20
|
+
Use the global object defined above to make calls to the API. The API runs in a development environment using mocked data.
|
|
21
|
+
|
|
22
|
+
The provided tools will execute code in a sandbox and you will only have access to the API provided above and the standard javascript api's.
|
|
23
|
+
|
|
24
|
+
You may use top level `await` since your code will be wrapped in an async function before being run.
|
|
25
|
+
You should write safe code and catch exceptions. For errors, use `console.error` with a helpful message.
|
|
26
|
+
Avoid logging PII or sensitive information.
|
|
27
|
+
|
|
28
|
+
You may use `runCode` to experiment or test. Console logs will be returned with the tool results, including 'traces' which show calls to the API.
|
|
29
|
+
|
|
30
|
+
When finished, you must call `submitCode` to check and run the code. Examine the log traces that get returned to confirm that the expected calls were made and the code is running as expected. If not, you may call submit code again. Once finished, respond with "Successfully submitted code".
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
{{DOCUMENTATION}}
|
|
35
|
+
|
|
36
|
+
{{CONTEXT}}
|
|
37
|
+
|
package/src/index.ts
CHANGED
|
@@ -2,16 +2,17 @@ require('dotenv').config()
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { OpenAI } from "openai";
|
|
4
4
|
|
|
5
|
+
import { LogSource, SandboxLog } from "./logger";
|
|
5
6
|
import createSandbox, { CodeRunResult } from "./sandbox";
|
|
6
7
|
import { rejectOpenPromises } from './sandbox-wrappers';
|
|
7
8
|
|
|
8
9
|
import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
|
|
9
|
-
import {
|
|
10
|
+
import { getQuickJS, QuickJSWASMModule } from "quickjs-emscripten";
|
|
10
11
|
|
|
11
12
|
import { AutoParseableTool } from 'openai/lib/parser.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import path from 'path';
|
|
15
16
|
|
|
16
17
|
const openai = new OpenAI( { baseURL: process.env.OPENAI_API_BASE_URL || undefined, apiKey: process.env.OPENAI_API_KEY || undefined } );
|
|
17
18
|
|
|
@@ -46,14 +47,13 @@ export interface GenerateCodeResult {
|
|
|
46
47
|
comments?: string;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
function createLog( message: string, source: LogSource) {
|
|
51
|
+
return { type: "log", args: [message], source } as SandboxLog
|
|
52
|
+
}
|
|
49
53
|
|
|
50
54
|
const wrapCode = (apiGlobalName:string, apiFileDefPath:string, code:string) => `
|
|
51
55
|
(async () => {
|
|
52
|
-
|
|
53
|
-
${code}
|
|
54
|
-
} catch(err) {
|
|
55
|
-
console.error(err);
|
|
56
|
-
}
|
|
56
|
+
${code}
|
|
57
57
|
})();
|
|
58
58
|
`;
|
|
59
59
|
|
|
@@ -63,7 +63,7 @@ ${code}
|
|
|
63
63
|
\`\`\`
|
|
64
64
|
`;
|
|
65
65
|
|
|
66
|
-
export function zodParseJSON<T>(schema:
|
|
66
|
+
export function zodParseJSON<T>(schema: { parse: any }) {
|
|
67
67
|
return (input: string): T => schema.parse(JSON.parse(input));
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -72,7 +72,7 @@ export function createBasePrompt(apiFilePath:string, apiGlobalName: string, docu
|
|
|
72
72
|
const documentationText: string = documentationPath ? fs.readFileSync(documentationPath, 'utf8') : '';
|
|
73
73
|
|
|
74
74
|
//console.time("Loading templates");
|
|
75
|
-
const createTaskPrompt: string = fs.readFileSync( '
|
|
75
|
+
const createTaskPrompt: string = fs.readFileSync( path.resolve(__dirname, '../prompts/generate-code-prompt.md'), 'utf8').replace("{{DOCUMENTATION}}", documentationText).replace("{{API}}", apiText).replace("{{API_GLOBAL_NAME}}", apiGlobalName || "");
|
|
76
76
|
//console.timeEnd("Loading templates")
|
|
77
77
|
|
|
78
78
|
return createTaskPrompt;
|
|
@@ -99,7 +99,7 @@ export const generateCode = async function(instance:AIMyAPIInstance, queryText:s
|
|
|
99
99
|
console.log("Generating code for query:", queryText);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const newHistoryItems = [];
|
|
102
|
+
const newHistoryItems: any[] = [];
|
|
103
103
|
// checkcode
|
|
104
104
|
const codingTools: AutoParseableTool<any, true>[] = [
|
|
105
105
|
{
|
|
@@ -111,7 +111,15 @@ export const generateCode = async function(instance:AIMyAPIInstance, queryText:s
|
|
|
111
111
|
if(debug) {
|
|
112
112
|
console.log("Running code...", code);
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
const checkRes = await instance.checkCode(code);
|
|
115
|
+
if(!checkRes.success) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
reason: "Code check failed",
|
|
119
|
+
logs: checkRes.logs,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
const res = await instance.runCode(code);
|
|
116
124
|
|
|
117
125
|
if(hideLogsFromAgent) {
|
|
@@ -119,30 +127,30 @@ export const generateCode = async function(instance:AIMyAPIInstance, queryText:s
|
|
|
119
127
|
}
|
|
120
128
|
return res;
|
|
121
129
|
},
|
|
122
|
-
description: "
|
|
130
|
+
description: "Check and run the provided code in a sandboxed environment. The code will have access to the API object. Returns the result of the code execution. Use this for testing and experimentation.",
|
|
123
131
|
parse: zodParseJSON(runCodeSchema),
|
|
124
|
-
parameters:
|
|
132
|
+
parameters: runCodeSchema.toJSONSchema(),
|
|
125
133
|
}
|
|
126
134
|
},
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// {
|
|
136
|
+
// type: "function",
|
|
137
|
+
// function: {
|
|
138
|
+
// name: "checkCode",
|
|
139
|
+
// // @ts-ignore
|
|
140
|
+
// function: async ({code}: {code: string}) => {
|
|
141
|
+
// if(debug) {
|
|
142
|
+
// console.log("Checking code...", code);
|
|
143
|
+
// }
|
|
144
|
+
// const res = await instance.checkCode(code);
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
146
|
+
// return res;
|
|
147
|
+
|
|
148
|
+
// },
|
|
149
|
+
// description: "Check the provided code for errors or issues without running it. Returns the result of the code check.",
|
|
150
|
+
// parse: zodParseJSON(checkCodeSchema),
|
|
151
|
+
// parameters: checkCodeSchema.toJSONSchema(),
|
|
152
|
+
// }
|
|
153
|
+
// },
|
|
146
154
|
{
|
|
147
155
|
type: "function",
|
|
148
156
|
function: {
|
|
@@ -158,14 +166,21 @@ export const generateCode = async function(instance:AIMyAPIInstance, queryText:s
|
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
const res = await instance.runCode(code);
|
|
170
|
+
|
|
171
|
+
if(hideLogsFromAgent) {
|
|
172
|
+
res.logs = [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if(res.success) {
|
|
176
|
+
finalSubmittedCode = code;
|
|
164
177
|
}
|
|
178
|
+
return res;
|
|
179
|
+
|
|
165
180
|
},
|
|
166
|
-
description: "Submit the provided code as the final code
|
|
181
|
+
description: "Submit the provided code as a candidate for the final code. The code will be checked and run and output logs and traces will be provided. Check the logs, if satisifed, respond to the user otherwise submit again.",
|
|
167
182
|
parse: zodParseJSON(submitCodeSchema),
|
|
168
|
-
parameters:
|
|
183
|
+
parameters: submitCodeSchema.toJSONSchema(),
|
|
169
184
|
}
|
|
170
185
|
}
|
|
171
186
|
];
|
|
@@ -193,22 +208,23 @@ export const generateCode = async function(instance:AIMyAPIInstance, queryText:s
|
|
|
193
208
|
})
|
|
194
209
|
.on('message', async (message: ChatCompletionMessageParam) => {
|
|
195
210
|
if(debug) {
|
|
196
|
-
|
|
211
|
+
if("reasoning_content" in message) {
|
|
212
|
+
console.log(message.reasoning_content);
|
|
213
|
+
}
|
|
214
|
+
//console.log("Message:", JSON.stringify(message, null, 2));
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
newHistoryItems.push(message);
|
|
200
218
|
})
|
|
201
219
|
.on('functionToolCall', (functionCall: OpenAI.Chat.Completions.ChatCompletionMessageFunctionToolCall.Function) => {
|
|
202
|
-
if(debug) {
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
// if(debug) {
|
|
221
|
+
// console.log("Function Call:", JSON.stringify(functionCall, null, 2));
|
|
222
|
+
// }
|
|
207
223
|
})
|
|
208
224
|
.on('functionToolCallResult', (functionCall) => {
|
|
209
|
-
if(debug) {
|
|
210
|
-
|
|
211
|
-
}
|
|
225
|
+
// if(debug) {
|
|
226
|
+
// console.log("Function Call Result:", JSON.stringify(functionCall, null, 2));
|
|
227
|
+
// }
|
|
212
228
|
})
|
|
213
229
|
|
|
214
230
|
const finalContent = await runner.finalContent();
|
|
@@ -333,7 +349,8 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
|
|
|
333
349
|
console.error("Unknown failure checking code");
|
|
334
350
|
return {
|
|
335
351
|
success: false,
|
|
336
|
-
logs: [],
|
|
352
|
+
logs: [createLog("Unknown failure checking code", "compiler")],
|
|
353
|
+
|
|
337
354
|
};
|
|
338
355
|
}
|
|
339
356
|
|
|
@@ -386,7 +403,7 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
|
|
|
386
403
|
|
|
387
404
|
// Create sandbox
|
|
388
405
|
// NOTE: not sure why I have to replace the backslash here
|
|
389
|
-
const {vm, runCode:
|
|
406
|
+
const {vm, runCode: sandbox_runCode, isAsyncProcessRunning} = await createSandbox(QuickJS, apiExports ? {
|
|
390
407
|
[apiDefFilePath]: apiExports,
|
|
391
408
|
} : undefined, {
|
|
392
409
|
...apiGlobals,
|
|
@@ -401,7 +418,7 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
|
|
|
401
418
|
console.log("Running Code:\n", code);
|
|
402
419
|
}
|
|
403
420
|
|
|
404
|
-
const res = await
|
|
421
|
+
const res = await sandbox_runCode(wrapCode(apiGlobalName, apiDefFilePath, code));
|
|
405
422
|
|
|
406
423
|
if(!res) {
|
|
407
424
|
console.error("Unknown failure running task");
|
|
@@ -441,29 +458,36 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
|
|
|
441
458
|
// Create prompt
|
|
442
459
|
|
|
443
460
|
if(debug) {
|
|
444
|
-
console.log("Query:", userQuery);
|
|
461
|
+
console.log("[debug] Query:", userQuery);
|
|
445
462
|
}
|
|
446
463
|
|
|
447
464
|
// Generate Code
|
|
448
465
|
console.time("Generate Task")
|
|
466
|
+
const prompt = createTaskPrompt.replace("{{CONTEXT}}", currentContext ? JSON.stringify(currentContext, null, 2) : "" );
|
|
449
467
|
const generatedCode:GenerateCodeResult | null = await generateCode(instance,
|
|
450
468
|
userQuery,
|
|
451
469
|
[],
|
|
452
|
-
|
|
470
|
+
prompt,
|
|
453
471
|
debug,
|
|
454
472
|
hideLogsFromAgent,
|
|
455
473
|
model,
|
|
456
474
|
additionalModelOptions
|
|
457
475
|
);
|
|
458
476
|
console.timeEnd("Generate Task")
|
|
477
|
+
|
|
478
|
+
console.log(`generated code result\nSuccess?: ${generatedCode?.success}\nComments: ${generatedCode?.comments}\nRunning Code...`);
|
|
459
479
|
|
|
460
|
-
|
|
480
|
+
|
|
481
|
+
console.time("Run Code took")
|
|
461
482
|
if(generatedCode?.code) {
|
|
462
|
-
await instance.runCode(generatedCode.code);
|
|
483
|
+
const codeRunResult = await instance.runCode(generatedCode.code);
|
|
484
|
+
if(!codeRunResult.success) {
|
|
485
|
+
console.error("Error running generated code");
|
|
486
|
+
}
|
|
463
487
|
} else {
|
|
464
488
|
console.error("No code generated", generatedCode);
|
|
465
489
|
}
|
|
466
|
-
console.timeEnd("Run Code")
|
|
490
|
+
console.timeEnd("Run Code took")
|
|
467
491
|
|
|
468
492
|
return generatedCode;
|
|
469
493
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
type LogSource = "sandbox" | "compiler" | "runner";
|
|
2
|
+
type LogEntryType = "log" | "error" | "trace";
|
|
3
|
+
|
|
4
|
+
type SandboxLog = {
|
|
5
|
+
type: "log" | "error";
|
|
6
|
+
args: any[];
|
|
7
|
+
source: LogSource
|
|
8
|
+
} | {
|
|
9
|
+
type: "trace";
|
|
10
|
+
fnName: string,
|
|
11
|
+
fnArgs: any[],
|
|
12
|
+
result: any
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const has = Object.prototype.hasOwnProperty;
|
|
16
|
+
|
|
17
|
+
function dequal(foo: any, bar: any) {
|
|
18
|
+
let ctor, len;
|
|
19
|
+
if (foo === bar) return true;
|
|
20
|
+
|
|
21
|
+
if (foo && bar && (ctor=foo.constructor) === bar.constructor) {
|
|
22
|
+
if (ctor === Date) return foo.getTime() === bar.getTime();
|
|
23
|
+
if (ctor === RegExp) return foo.toString() === bar.toString();
|
|
24
|
+
|
|
25
|
+
if (ctor === Array) {
|
|
26
|
+
if ((len=foo.length) === bar.length) {
|
|
27
|
+
while (len-- && dequal(foo[len], bar[len]));
|
|
28
|
+
}
|
|
29
|
+
return len === -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!ctor || typeof foo === 'object') {
|
|
33
|
+
len = 0;
|
|
34
|
+
for (ctor in foo) {
|
|
35
|
+
if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;
|
|
36
|
+
if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false;
|
|
37
|
+
}
|
|
38
|
+
return Object.keys(bar).length === len;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return foo !== foo && bar !== bar;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Return true to pass. Returning a falsy value or throwing fails the check. */
|
|
46
|
+
type ArgumentsEvaluator = (args: any[]) => any;
|
|
47
|
+
/** Return true to pass. Returning a falsy value or throwing fails the check. */
|
|
48
|
+
type ResultEvaluator = (result: any) => any;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One step in a test sequence. Gets the logs and the current position, checks
|
|
52
|
+
* the entries there, and returns the position of the next unchecked entry.
|
|
53
|
+
* Throws TestFailed if the check fails.
|
|
54
|
+
*/
|
|
55
|
+
type TestStep = (logs: SandboxLog[], index: number) => number;
|
|
56
|
+
|
|
57
|
+
class TestFailed extends Error {
|
|
58
|
+
constructor(message: string) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "TestFailed";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function describeEntry(entry: SandboxLog | undefined): string {
|
|
65
|
+
if (!entry) return "end of logs";
|
|
66
|
+
const short = (v: any) => JSON.stringify(v);
|
|
67
|
+
if (entry.type === "trace") {
|
|
68
|
+
return `trace ${entry.fnName}(${entry.fnArgs.map(short).join(", ")}) -> ${short(entry.result)}`;
|
|
69
|
+
}
|
|
70
|
+
return `${entry.type} [${entry.source}] ${entry.args.map(short).join(" ")}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nameMatches(pattern: string, name: string): boolean {
|
|
74
|
+
return pattern === "*" || pattern === name;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Runs an evaluator and throws TestFailed if it returns falsy or throws. */
|
|
78
|
+
function evaluate(evaluator: ((value: any) => any) | undefined, value: any, what: string, index: number, entry: SandboxLog) {
|
|
79
|
+
if (!evaluator) return;
|
|
80
|
+
let passed: any;
|
|
81
|
+
try {
|
|
82
|
+
passed = evaluator(value);
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
throw new TestFailed(`Log #${index}: ${what} check threw: ${e?.message ?? e}. Entry: ${describeEntry(entry)}`);
|
|
85
|
+
}
|
|
86
|
+
if (!passed) {
|
|
87
|
+
throw new TestFailed(`Log #${index}: ${what} check failed. Entry: ${describeEntry(entry)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkTrace(logs: SandboxLog[], index: number, functionName: string, argumentsEvaluator?: ArgumentsEvaluator, resultEvaluator?: ResultEvaluator) {
|
|
92
|
+
const entry = logs[index];
|
|
93
|
+
if (!entry || entry.type !== "trace" || !nameMatches(functionName, entry.fnName)) {
|
|
94
|
+
throw new TestFailed(`Log #${index}: expected trace for "${functionName}", got ${describeEntry(entry)}`);
|
|
95
|
+
}
|
|
96
|
+
evaluate(argumentsEvaluator, entry.fnArgs, `arguments of "${entry.fnName}"`, index, entry);
|
|
97
|
+
evaluate(resultEvaluator, entry.result, `result of "${entry.fnName}"`, index, entry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectEntryType(type: "log" | "error", argumentsEvaluator?: ArgumentsEvaluator): TestStep {
|
|
101
|
+
return (logs, index) => {
|
|
102
|
+
const entry = logs[index];
|
|
103
|
+
if (!entry || entry.type !== type) {
|
|
104
|
+
throw new TestFailed(`Log #${index}: expected ${type}, got ${describeEntry(entry)}`);
|
|
105
|
+
}
|
|
106
|
+
evaluate(argumentsEvaluator, entry.args, `${type} arguments`, index, entry);
|
|
107
|
+
return index + 1;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Checks the logs from a sandbox run against an expected sequence of entries.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* test.sequence(
|
|
116
|
+
* test.expectTrace("getSalesData"),
|
|
117
|
+
* test.expectTrace("display", (args) => args[0].includes("The month with the highest sales was")),
|
|
118
|
+
* );
|
|
119
|
+
*/
|
|
120
|
+
class LogTester {
|
|
121
|
+
private source: SandboxLog[];
|
|
122
|
+
|
|
123
|
+
constructor(source: SandboxLog[]) {
|
|
124
|
+
this.source = source;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get logs(): SandboxLog[] {
|
|
128
|
+
return this.source;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Runs the steps in order, starting at the first log entry. Throws TestFailed
|
|
133
|
+
* on the first failing step. Entries after the last step are ignored.
|
|
134
|
+
* Accepts steps as separate arguments or as one array.
|
|
135
|
+
*/
|
|
136
|
+
sequence(...steps: (TestStep | TestStep[])[]): true {
|
|
137
|
+
const logs = this.logs;
|
|
138
|
+
let index = 0;
|
|
139
|
+
for (const step of steps.flat()) {
|
|
140
|
+
if (typeof step !== "function") {
|
|
141
|
+
throw new TestFailed(`sequence() expects test steps (e.g. test.expectTrace(...)), got ${typeof step}`);
|
|
142
|
+
}
|
|
143
|
+
index = step(logs, index);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Next entry must be a trace for `functionName` ("*" for any function). */
|
|
149
|
+
expectTrace(functionName: string, argumentsEvaluator?: ArgumentsEvaluator, resultEvaluator?: ResultEvaluator): TestStep {
|
|
150
|
+
return (logs, index) => {
|
|
151
|
+
checkTrace(logs, index, functionName, argumentsEvaluator, resultEvaluator);
|
|
152
|
+
return index + 1;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Next entry can be anything, but there must be one. */
|
|
157
|
+
expectAny(): TestStep {
|
|
158
|
+
return (logs, index) => {
|
|
159
|
+
if (index >= logs.length) {
|
|
160
|
+
throw new TestFailed(`Log #${index}: expected any entry, got end of logs`);
|
|
161
|
+
}
|
|
162
|
+
return index + 1;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Skips ahead to just before the next trace for `functionName` ("*" for any). Fails if there is none. */
|
|
167
|
+
skipToTrace(functionName: string): TestStep {
|
|
168
|
+
return (logs, index) => {
|
|
169
|
+
for (let i = index; i < logs.length; i++) {
|
|
170
|
+
const entry = logs[i];
|
|
171
|
+
if (entry.type === "trace" && nameMatches(functionName, entry.fnName)) {
|
|
172
|
+
return i;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new TestFailed(`Log #${index}: no trace for "${functionName}" found from here to the end of the logs`);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Consecutive traces for `functionName` ("*" for any).
|
|
181
|
+
* With `num` > 0, exactly that many must be next (more are left for later steps).
|
|
182
|
+
* With `num` 0 or undefined, takes as many as are next, including none.
|
|
183
|
+
* The evaluators are checked against every matched trace.
|
|
184
|
+
*/
|
|
185
|
+
expectMultipleTraces(functionName: string, num?: number, argumentsEvaluator?: ArgumentsEvaluator, resultEvaluator?: ResultEvaluator): TestStep {
|
|
186
|
+
return (logs, index) => {
|
|
187
|
+
if (num && num > 0) {
|
|
188
|
+
for (let n = 0; n < num; n++) {
|
|
189
|
+
const entry = logs[index + n];
|
|
190
|
+
if (!entry || entry.type !== "trace" || !nameMatches(functionName, entry.fnName)) {
|
|
191
|
+
throw new TestFailed(`Log #${index + n}: expected ${num} traces for "${functionName}" but found ${n}. Got ${describeEntry(entry)}`);
|
|
192
|
+
}
|
|
193
|
+
checkTrace(logs, index + n, functionName, argumentsEvaluator, resultEvaluator);
|
|
194
|
+
}
|
|
195
|
+
return index + num;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let i = index;
|
|
199
|
+
while (i < logs.length) {
|
|
200
|
+
const entry = logs[i];
|
|
201
|
+
if (entry.type !== "trace" || !nameMatches(functionName, entry.fnName)) break;
|
|
202
|
+
checkTrace(logs, i, functionName, argumentsEvaluator, resultEvaluator);
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
return i;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Next entry must be a log (console.log etc). The evaluator gets the logged arguments. */
|
|
210
|
+
expectLog(argumentsEvaluator?: ArgumentsEvaluator): TestStep {
|
|
211
|
+
return expectEntryType("log", argumentsEvaluator);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Next entry must be an error. The evaluator gets the error arguments. */
|
|
215
|
+
expectError(argumentsEvaluator?: ArgumentsEvaluator): TestStep {
|
|
216
|
+
return expectEntryType("error", argumentsEvaluator);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Deep equality. Returns true if equal, otherwise throws TestFailed, so it works inside evaluators. */
|
|
220
|
+
equals(actual: any, expected: any): true {
|
|
221
|
+
if (!dequal(actual, expected)) {
|
|
222
|
+
throw new TestFailed(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
// const test = new LogTester(logs);
|
|
230
|
+
// test.sequence(
|
|
231
|
+
// test.expectTrace("display", (args) => args[0][0].sales == 100)
|
|
232
|
+
// )
|
|
233
|
+
// console.log("success");
|