@akshatmittal/invoker 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +44 -44
- package/dist/slack.mjs +69 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,10 +104,10 @@ import { defineTask } from "@akshatmittal/invoker";
|
|
|
104
104
|
|
|
105
105
|
export const evaluateModels = defineTask({
|
|
106
106
|
name: "evaluate-models",
|
|
107
|
-
matrix: {
|
|
107
|
+
matrix: async () => ({
|
|
108
108
|
model: ["gpt-5", "gpt-5-mini"],
|
|
109
109
|
dataset: ["support", "sales"],
|
|
110
|
-
},
|
|
110
|
+
}),
|
|
111
111
|
setup: async ({ cases }) => loadFixtures(cases),
|
|
112
112
|
run: async ({ matrix, setup, vitest }) => {
|
|
113
113
|
vitest.expect(setup.has(matrix.dataset)).toBe(true);
|
|
@@ -139,7 +139,8 @@ defineWorkflow({
|
|
|
139
139
|
});
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
Matrix
|
|
142
|
+
The Matrix function runs during collection and its returned literal determines the exact
|
|
143
|
+
`matrix` type. `setup` determines the exact
|
|
143
144
|
shared setup type, and the exact JSON return type is retained on the Task.
|
|
144
145
|
Axis names must be non-empty, enumerable strings that are not array indexes.
|
|
145
146
|
Omitting `matrix` creates one Case with `{}`. Setup runs once per Task, Cases
|
package/dist/index.d.mts
CHANGED
|
@@ -34,7 +34,7 @@ type TeardownContext<M extends Matrix, Setup> = SetupContext<M> & {
|
|
|
34
34
|
declare const taskDefinitionBrand: unique symbol;
|
|
35
35
|
interface TaskDefinition<Name extends string = string, M extends Matrix = Matrix, Setup = unknown, Output extends JsonValue = JsonValue> {
|
|
36
36
|
readonly name: Name;
|
|
37
|
-
readonly matrix: M
|
|
37
|
+
readonly matrix: () => Promise<M>;
|
|
38
38
|
readonly [taskDefinitionBrand]: true;
|
|
39
39
|
readonly setup?: (context: SetupContext<M>) => Awaitable<Setup>;
|
|
40
40
|
readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
|
|
@@ -42,14 +42,14 @@ interface TaskDefinition<Name extends string = string, M extends Matrix = Matrix
|
|
|
42
42
|
}
|
|
43
43
|
type TaskWithSetup<Name extends string, M extends Matrix, Setup, Output extends JsonValue> = {
|
|
44
44
|
readonly name: Name;
|
|
45
|
-
readonly matrix?: M
|
|
45
|
+
readonly matrix?: () => Promise<M>;
|
|
46
46
|
readonly setup: (context: SetupContext<M>) => Awaitable<Setup>;
|
|
47
47
|
readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
|
|
48
48
|
readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
|
|
49
49
|
};
|
|
50
50
|
type TaskWithoutSetup<Name extends string, M extends Matrix, Output extends JsonValue> = {
|
|
51
51
|
readonly name: Name;
|
|
52
|
-
readonly matrix?: M
|
|
52
|
+
readonly matrix?: () => Promise<M>;
|
|
53
53
|
readonly setup?: never;
|
|
54
54
|
readonly run: (context: TaskContext<CaseCoordinates<M>, undefined>) => Awaitable<Output>;
|
|
55
55
|
readonly teardown?: never;
|
|
@@ -58,7 +58,7 @@ declare function defineTask<const Name extends string, const M extends Matrix =
|
|
|
58
58
|
declare function defineTask<const Name extends string, const M extends Matrix = Record<never, never>, const Output extends JsonValue = JsonValue>(definition: TaskWithoutSetup<Name, M, Output>): TaskDefinition<Name, M, undefined, Output>;
|
|
59
59
|
type AnyTaskDefinition = {
|
|
60
60
|
readonly name: string;
|
|
61
|
-
readonly matrix: Matrix
|
|
61
|
+
readonly matrix: () => Promise<Matrix>;
|
|
62
62
|
readonly [taskDefinitionBrand]: true;
|
|
63
63
|
};
|
|
64
64
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ const taskDefinitionBrand = Symbol("invoker.task");
|
|
|
5
5
|
function defineTask(definition) {
|
|
6
6
|
return {
|
|
7
7
|
...definition,
|
|
8
|
-
matrix: definition.matrix ?? {},
|
|
8
|
+
matrix: definition.matrix ?? (async () => ({})),
|
|
9
9
|
[taskDefinitionBrand]: true
|
|
10
10
|
};
|
|
11
11
|
}
|
|
@@ -71,7 +71,6 @@ function isJsonObject(value) {
|
|
|
71
71
|
//#region src/matrix.ts
|
|
72
72
|
const axisSchema = z.string();
|
|
73
73
|
function expandMatrix(matrix, owner) {
|
|
74
|
-
if (matrix === void 0) return [{}];
|
|
75
74
|
assertPlainObject(matrix, owner, ".matrix");
|
|
76
75
|
let cases = [{}];
|
|
77
76
|
for (const candidate of Reflect.ownKeys(matrix)) {
|
|
@@ -105,9 +104,9 @@ function caseName(matrix, index) {
|
|
|
105
104
|
//#endregion
|
|
106
105
|
//#region src/workflow.ts
|
|
107
106
|
function defineWorkflow(definition) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
for (const prepared of
|
|
107
|
+
describe(definition.name, { concurrent: false }, async () => {
|
|
108
|
+
const tasks = await prepareWorkflow(definition);
|
|
109
|
+
for (const prepared of tasks) describe(prepared.task.name, { concurrent: false }, () => {
|
|
111
110
|
let setup;
|
|
112
111
|
const setupTask = prepared.task.setup;
|
|
113
112
|
if (setupTask) beforeAll(async () => {
|
|
@@ -135,7 +134,7 @@ function defineWorkflow(definition) {
|
|
|
135
134
|
});
|
|
136
135
|
});
|
|
137
136
|
}
|
|
138
|
-
function prepareWorkflow(definition) {
|
|
137
|
+
async function prepareWorkflow(definition) {
|
|
139
138
|
assertPlainObject(definition, "Workflow", "");
|
|
140
139
|
assertOnlyKeys(definition, [
|
|
141
140
|
"name",
|
|
@@ -148,44 +147,45 @@ function prepareWorkflow(definition) {
|
|
|
148
147
|
if (metadata !== void 0) assertPlainObject(metadata, `Workflow ${JSON.stringify(name)}`, ".metadata");
|
|
149
148
|
if (!Array.isArray(taskDefinitions) || taskDefinitions.length === 0) fail("Workflow", ".tasks", "expected a non-empty Task tuple");
|
|
150
149
|
const names = /* @__PURE__ */ new Set();
|
|
151
|
-
|
|
152
|
-
name
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
150
|
+
const tasks = taskDefinitions.map((value, index) => {
|
|
151
|
+
const owner = `Workflow ${JSON.stringify(name)} Task ${index + 1}`;
|
|
152
|
+
assertPlainObject(value, owner, "");
|
|
153
|
+
if (value[taskDefinitionBrand] !== true) fail(owner, "", "expected a Task created by defineTask");
|
|
154
|
+
const task = value;
|
|
155
|
+
assertOnlyKeys(task, [
|
|
156
|
+
"name",
|
|
157
|
+
"matrix",
|
|
158
|
+
"setup",
|
|
159
|
+
"run",
|
|
160
|
+
"teardown"
|
|
161
|
+
], owner);
|
|
162
|
+
const taskSnapshot = { ...task };
|
|
163
|
+
assertName(taskSnapshot.name, owner, ".name");
|
|
164
|
+
if (names.has(taskSnapshot.name)) fail(owner, ".name", `duplicate Task name ${JSON.stringify(taskSnapshot.name)}`);
|
|
165
|
+
names.add(taskSnapshot.name);
|
|
166
|
+
if (!z.function().safeParse(taskSnapshot.run).success) fail(owner, ".run", "expected a function");
|
|
167
|
+
if (!z.function().safeParse(taskSnapshot.matrix).success) fail(owner, ".matrix", "expected a function");
|
|
168
|
+
if (taskSnapshot.setup !== void 0 && !z.function().safeParse(taskSnapshot.setup).success) fail(owner, ".setup", "expected a function");
|
|
169
|
+
if (taskSnapshot.teardown !== void 0 && !z.function().safeParse(taskSnapshot.teardown).success) fail(owner, ".teardown", "expected a function");
|
|
170
|
+
if (taskSnapshot.teardown && !taskSnapshot.setup) fail(owner, ".teardown", "requires setup");
|
|
171
|
+
return taskSnapshot;
|
|
172
|
+
});
|
|
173
|
+
return await Promise.all(tasks.map(async (task) => {
|
|
174
|
+
const cases = expandMatrix(await task.matrix(), `Task ${JSON.stringify(task.name)}`);
|
|
175
|
+
return {
|
|
176
|
+
task,
|
|
177
|
+
cases,
|
|
178
|
+
names: cases.map(caseName),
|
|
179
|
+
metadata: cases.map((matrix) => metadata === void 0 ? {
|
|
180
|
+
schema: 1,
|
|
181
|
+
matrix
|
|
182
|
+
} : {
|
|
183
|
+
schema: 1,
|
|
184
|
+
matrix,
|
|
185
|
+
metadata
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
}));
|
|
189
189
|
}
|
|
190
190
|
//#endregion
|
|
191
191
|
export { defineTask, defineWorkflow };
|
package/dist/slack.mjs
CHANGED
|
@@ -48,7 +48,8 @@ function collectWorkflowReports(modules) {
|
|
|
48
48
|
failed: 0,
|
|
49
49
|
skipped: 0,
|
|
50
50
|
incomplete: 0,
|
|
51
|
-
failures: []
|
|
51
|
+
failures: [],
|
|
52
|
+
retries: []
|
|
52
53
|
};
|
|
53
54
|
workflow.tasks.set(taskSuite, task);
|
|
54
55
|
}
|
|
@@ -56,7 +57,17 @@ function collectWorkflowReports(modules) {
|
|
|
56
57
|
const result = testCase.result();
|
|
57
58
|
const diagnostic = testCase.diagnostic();
|
|
58
59
|
task[result.state === "pending" ? "incomplete" : result.state] += 1;
|
|
59
|
-
|
|
60
|
+
const retryCount = diagnostic?.retryCount ?? 0;
|
|
61
|
+
if (retryCount > 0) {
|
|
62
|
+
task.retried += 1;
|
|
63
|
+
if (result.state === "passed") task.retries.push({
|
|
64
|
+
task: task.name,
|
|
65
|
+
caseName: testCase.name,
|
|
66
|
+
matrix: invoker.matrix,
|
|
67
|
+
count: retryCount,
|
|
68
|
+
messages: (result.errors ?? []).map(errorMessage)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
60
71
|
if (diagnostic) {
|
|
61
72
|
task.startedAt = Math.min(task.startedAt ?? diagnostic.startTime, diagnostic.startTime);
|
|
62
73
|
task.endedAt = Math.max(task.endedAt ?? 0, diagnostic.startTime + diagnostic.duration);
|
|
@@ -70,28 +81,31 @@ function collectWorkflowReports(modules) {
|
|
|
70
81
|
}
|
|
71
82
|
return [...workflows.values()].map((workflow) => {
|
|
72
83
|
const failures = [];
|
|
84
|
+
const retries = [];
|
|
73
85
|
addErrors(failures, workflow.suite.errors());
|
|
74
86
|
addErrors(failures, workflow.module.errors());
|
|
75
87
|
const collectedTasks = [...workflow.tasks.values()];
|
|
76
88
|
for (const task of collectedTasks) {
|
|
77
89
|
failures.push(...task.failures);
|
|
90
|
+
retries.push(...task.retries);
|
|
78
91
|
addErrors(failures, task.suite.errors(), task.name);
|
|
79
92
|
}
|
|
80
93
|
const { startedAt, endedAt } = timeSpan(collectedTasks);
|
|
81
94
|
return {
|
|
82
95
|
name: workflow.name,
|
|
83
96
|
metadata: workflow.metadata,
|
|
84
|
-
tasks: collectedTasks.map(({ suite: _suite, failures: _failures, startedAt, endedAt, ...task }) => ({
|
|
97
|
+
tasks: collectedTasks.map(({ suite: _suite, failures: _failures, retries: _retries, startedAt, endedAt, ...task }) => ({
|
|
85
98
|
...task,
|
|
86
99
|
duration: startedAt === void 0 || endedAt === void 0 ? 0 : endedAt - startedAt
|
|
87
100
|
})),
|
|
88
101
|
failures: deduplicateFailures(failures),
|
|
102
|
+
retries,
|
|
89
103
|
startedAt,
|
|
90
104
|
endedAt
|
|
91
105
|
};
|
|
92
106
|
});
|
|
93
107
|
}
|
|
94
|
-
function
|
|
108
|
+
function summaryMessage(reports, runUrl) {
|
|
95
109
|
const timestamp = Math.floor(Date.now() / 1e3);
|
|
96
110
|
const { startedAt, endedAt } = timeSpan(reports);
|
|
97
111
|
const footer = [
|
|
@@ -99,16 +113,16 @@ function summaryMessages(reports, runUrl) {
|
|
|
99
113
|
`<!date^${timestamp}^{date_short_pretty} at {time}|${(/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString()}>`,
|
|
100
114
|
...runUrl ? [`<${escapeSlackControl(runUrl)}|View run>`] : []
|
|
101
115
|
].join(" • ");
|
|
102
|
-
return
|
|
103
|
-
text:
|
|
104
|
-
attachments: [
|
|
116
|
+
return {
|
|
117
|
+
text: "Invoker Report",
|
|
118
|
+
attachments: [...reports.flatMap(workflowAttachments), { blocks: [{
|
|
105
119
|
type: "context",
|
|
106
120
|
elements: [{
|
|
107
121
|
type: "mrkdwn",
|
|
108
122
|
text: footer
|
|
109
123
|
}]
|
|
110
|
-
}] }]
|
|
111
|
-
}
|
|
124
|
+
}] }]
|
|
125
|
+
};
|
|
112
126
|
}
|
|
113
127
|
function workflowAttachments(report) {
|
|
114
128
|
const tables = taskTables(report.tasks);
|
|
@@ -194,6 +208,50 @@ function failureMessages(report) {
|
|
|
194
208
|
});
|
|
195
209
|
});
|
|
196
210
|
}
|
|
211
|
+
function retryMessages(report) {
|
|
212
|
+
return [...Map.groupBy(report.retries, (retry) => retry.task)].flatMap(([task, retries]) => {
|
|
213
|
+
const title = escapeSlack(truncate(task, NAME_CHARACTER_LIMIT));
|
|
214
|
+
const workflow = `*Workflow:* ${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))}`;
|
|
215
|
+
const metadata = metadataText(report.metadata);
|
|
216
|
+
const context = metadata ? `${workflow} • ${metadata}` : workflow;
|
|
217
|
+
return chunk(retries.map((retry) => {
|
|
218
|
+
const matrix = `\nMatrix: ${escapeSlack(JSON.stringify(retry.matrix))}`;
|
|
219
|
+
const messages = retry.messages.map((message) => `\n• ${escapeSlack(message)}`).join("");
|
|
220
|
+
return `*${escapeSlack(retry.caseName)}*\nRetries: ${retry.count}${matrix}${messages}`;
|
|
221
|
+
}), SECTION_CHARACTER_LIMIT).map((details, index, messages) => {
|
|
222
|
+
const part = messages.length > 1 ? ` (${index + 1}/${messages.length})` : "";
|
|
223
|
+
return {
|
|
224
|
+
text: `${truncate(report.name, NAME_CHARACTER_LIMIT)} › ${truncate(task, NAME_CHARACTER_LIMIT)} — ${retries.length} retried${part}`,
|
|
225
|
+
attachments: [{
|
|
226
|
+
color: "warning",
|
|
227
|
+
blocks: [
|
|
228
|
+
{
|
|
229
|
+
type: "section",
|
|
230
|
+
text: {
|
|
231
|
+
type: "mrkdwn",
|
|
232
|
+
text: `🟡 *${title} — ${retries.length} retried${part}*`
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: "context",
|
|
237
|
+
elements: [{
|
|
238
|
+
type: "mrkdwn",
|
|
239
|
+
text: context
|
|
240
|
+
}]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
type: "section",
|
|
244
|
+
text: {
|
|
245
|
+
type: "mrkdwn",
|
|
246
|
+
text: details
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}]
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
197
255
|
function unhandledErrorMessages(errors) {
|
|
198
256
|
const messages = [...new Set(errors.map(errorMessage))];
|
|
199
257
|
return chunk(messages.map((message) => `• ${escapeSlack(message)}`), SECTION_CHARACTER_LIMIT).map((details, index, chunks) => {
|
|
@@ -382,8 +440,7 @@ function slackReporter(options) {
|
|
|
382
440
|
return { async onTestRunEnd(modules, unhandledErrors) {
|
|
383
441
|
const reports = collectWorkflowReports(modules);
|
|
384
442
|
if (reports.length === 0) return;
|
|
385
|
-
const
|
|
386
|
-
if (!parentMessage) return;
|
|
443
|
+
const parentMessage = summaryMessage(reports, options.runUrl);
|
|
387
444
|
let parentTimestamp;
|
|
388
445
|
try {
|
|
389
446
|
const parentArguments = {
|
|
@@ -398,8 +455,8 @@ function slackReporter(options) {
|
|
|
398
455
|
return;
|
|
399
456
|
}
|
|
400
457
|
const replies = [
|
|
401
|
-
...continuations,
|
|
402
458
|
...reports.flatMap(failureMessages),
|
|
459
|
+
...reports.flatMap(retryMessages),
|
|
403
460
|
...unhandledErrorMessages(unhandledErrors)
|
|
404
461
|
];
|
|
405
462
|
if (replies.length > 0 && !parentTimestamp) {
|