@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 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 literals determine the exact `matrix` type, `setup` determines the exact
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
- const workflow = prepareWorkflow(definition);
109
- describe(workflow.name, { concurrent: false }, () => {
110
- for (const prepared of workflow.tasks) describe(prepared.task.name, { concurrent: false }, () => {
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
- return {
152
- name,
153
- tasks: taskDefinitions.map((value, index) => {
154
- const owner = `Workflow ${JSON.stringify(name)} Task ${index + 1}`;
155
- assertPlainObject(value, owner, "");
156
- if (value[taskDefinitionBrand] !== true) fail(owner, "", "expected a Task created by defineTask");
157
- const task = value;
158
- assertOnlyKeys(task, [
159
- "name",
160
- "matrix",
161
- "setup",
162
- "run",
163
- "teardown"
164
- ], owner);
165
- const taskSnapshot = { ...task };
166
- assertName(taskSnapshot.name, owner, ".name");
167
- if (names.has(taskSnapshot.name)) fail(owner, ".name", `duplicate Task name ${JSON.stringify(taskSnapshot.name)}`);
168
- names.add(taskSnapshot.name);
169
- if (!z.function().safeParse(taskSnapshot.run).success) fail(owner, ".run", "expected a function");
170
- if (taskSnapshot.setup !== void 0 && !z.function().safeParse(taskSnapshot.setup).success) fail(owner, ".setup", "expected a function");
171
- if (taskSnapshot.teardown !== void 0 && !z.function().safeParse(taskSnapshot.teardown).success) fail(owner, ".teardown", "expected a function");
172
- if (taskSnapshot.teardown && !taskSnapshot.setup) fail(owner, ".teardown", "requires setup");
173
- const cases = expandMatrix(taskSnapshot.matrix, `Task ${JSON.stringify(taskSnapshot.name)}`);
174
- return {
175
- task: taskSnapshot,
176
- cases,
177
- names: cases.map(caseName),
178
- metadata: cases.map((matrix) => metadata === void 0 ? {
179
- schema: 1,
180
- matrix
181
- } : {
182
- schema: 1,
183
- matrix,
184
- metadata
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
- if ((diagnostic?.retryCount ?? 0) > 0) task.retried += 1;
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 summaryMessages(reports, runUrl) {
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 reports.flatMap(workflowAttachments).map((attachment, index) => ({
103
- text: index === 0 ? "Invoker Report" : "Invoker Report (continued)",
104
- attachments: [attachment, ...index === 0 ? [{ blocks: [{
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 [parentMessage, ...continuations] = summaryMessages(reports, options.runUrl);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akshatmittal/invoker",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Typed matrix-driven regression workflows for Vitest.",
5
5
  "keywords": [
6
6
  "matrix",