@akshatmittal/invoker 0.3.0 → 0.4.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
@@ -98,23 +98,29 @@ scheduler intentionally has no HTTP health endpoint.
98
98
 
99
99
  ## Define a Workflow
100
100
 
101
+ Declare a Task's required parameters once, then bind them from each Workflow.
102
+ TypeScript checks the bindings while inferring the Task's Matrix, setup, and Output:
103
+
101
104
  ```ts
102
105
  // regressions/model/tasks/evaluate-models.ts
103
106
  import { defineTask } from "@akshatmittal/invoker";
104
107
 
105
- export const evaluateModels = defineTask({
108
+ type EvaluateParams = {
109
+ environment: "staging" | "production";
110
+ baseline: string;
111
+ };
112
+
113
+ export const evaluateModels = defineTask<EvaluateParams>()({
106
114
  name: "evaluate-models",
107
- matrix: async () => ({
108
- model: ["gpt-5", "gpt-5-mini"],
115
+ matrix: async ({ params }) => ({
116
+ model: await discoverModels(params.environment),
109
117
  dataset: ["support", "sales"],
110
118
  }),
111
- setup: async ({ cases }) => loadFixtures(cases),
112
- run: async ({ matrix, setup, vitest }) => {
119
+ setup: async ({ params, cases }) => loadFixtures(params.environment, cases),
120
+ run: async ({ params, matrix, setup, vitest }) => {
113
121
  vitest.expect(setup.has(matrix.dataset)).toBe(true);
114
-
115
122
  return {
116
- model: matrix.model,
117
- dataset: matrix.dataset,
123
+ baseline: params.baseline,
118
124
  score: await evaluate(matrix, setup),
119
125
  };
120
126
  },
@@ -131,21 +137,51 @@ import { evaluateModels } from "../tasks/evaluate-models.js";
131
137
 
132
138
  defineWorkflow({
133
139
  name: "model-regressions",
134
- metadata: {
135
- commit: process.env.GITHUB_SHA ?? "local",
136
- baseline: "2026-08-01",
137
- },
138
- tasks: [evaluateModels],
140
+ metadata: { commit: process.env.GITHUB_SHA ?? "local" },
141
+ matrix: async () => ({ environment: ["staging", "production"] }),
142
+ tasks: ({ matrix }) => [
143
+ evaluateModels({
144
+ environment: matrix.environment,
145
+ baseline: "2026-09-01",
146
+ }),
147
+ ],
139
148
  });
140
149
  ```
141
150
 
142
- The Matrix function runs during collection and its returned literal determines the exact
143
- `matrix` type. `setup` determines the exact
144
- shared setup type, and the exact JSON return type is retained on the Task.
145
- Axis names must be non-empty, enumerable strings that are not array indexes.
146
- Omitting `matrix` creates one Case with `{}`. Setup runs once per Task, Cases
147
- within that Task run concurrently, and teardown runs once after successful
148
- setup. Tasks run sequentially in their Workflow.
151
+ Calling `evaluateModels(...)` binds inputs; it does not execute the Task. All
152
+ parameter properties are required and JSON-compatible. Missing fields, optional
153
+ parameter declarations, and incompatible bindings fail typechecking. There is no
154
+ SDK defaults mechanism. Use setup for clients and other non-JSON resources.
155
+
156
+ For a parameterless Task, use `defineTask()({ name, run })` and bind it with no
157
+ argument. Workflow `tasks` is always a synchronous callback returning a nonempty
158
+ Task list. It can select different Tasks per Workflow coordinate; names must be
159
+ unique within each coordinate. Reuse the same Task in other Workflows with new
160
+ bindings.
161
+
162
+ Both Matrix functions run during collection. The Workflow Matrix resolves once;
163
+ Task binding runs once per Workflow coordinate, and each bound Task discovers its
164
+ own Matrix once. Task discovery can run concurrently across coordinates. All
165
+ bindings and matrices are validated before executable suites are registered; a
166
+ discovery failure prevents that Workflow's execution.
167
+
168
+ Workflow axes multiply Task axes. For two environments and three Task models,
169
+ there are six Cases. Execution follows the complete Task sequence for staging,
170
+ then the complete sequence for production. Each bound Task gets its own setup and
171
+ teardown, and its Cases run concurrently under Vitest's limits. Execution failures
172
+ normally allow later Tasks to continue; Vitest's bail configuration controls
173
+ stopping early.
174
+
175
+ Parameters are validated and snapshotted during collection and passed readonly to
176
+ Matrix discovery, setup, run, and teardown. Task `matrix` and setup's `cases`
177
+ contain only Task coordinates; Workflow coordinates enter through explicit
178
+ parameter binding. Retries share that binding's parameter snapshot and setup
179
+ result. Teardown runs once after successful setup.
180
+
181
+ Omitting either Matrix creates one coordinate, `{}`. Axis names must be non-empty,
182
+ enumerable strings that are not array indexes. Empty axes and duplicate values
183
+ are errors. Vitest shows Workflow → Workflow coordinate → Task → Case, with `[1]`
184
+ for empty coordinates and numbered axis values otherwise.
149
185
 
150
186
  ## Configure Vitest
151
187
 
@@ -178,34 +214,43 @@ Define additional Workflows in separate `*.test.ts` files. Vitest discovers
178
214
  them automatically; Invoker does not scan directories or require a central
179
215
  index.
180
216
 
181
- The metadata envelope is stable and JSON-compatible:
217
+ Schema 2 keeps both coordinate scopes and all bound parameters in the JSON report:
182
218
 
183
219
  ```json
184
220
  {
185
- "schema": 1,
186
- "matrix": { "model": "gpt-5", "dataset": "support" },
221
+ "schema": 2,
222
+ "matrix": {
223
+ "workflow": { "environment": "staging" },
224
+ "task": { "model": "model-a", "dataset": "support" }
225
+ },
226
+ "params": { "environment": "staging", "baseline": "2026-09-01" },
187
227
  "metadata": { "commit": "abc123" },
188
- "output": { "model": "gpt-5", "dataset": "support", "score": 0.92 }
228
+ "output": { "baseline": "2026-09-01", "score": 0.92 }
189
229
  }
190
230
  ```
191
231
 
192
- `output` is present only after a successful, JSON-valid Task return. Vitest's
193
- report remains authoritative for status, failures, timing, hierarchy, and
232
+ `matrix.workflow`, `matrix.task`, and `params` are always present, with `{}` for
233
+ empty scopes. Parameters are persisted in full, including fixed configuration;
234
+ there is no automatic redaction. Static coordinates and parameters survive setup
235
+ failures, skips, and retries. `output` is present only after a successful,
236
+ JSON-valid Task return and is cleared before every retry. Vitest's report remains authoritative for status, failures, timing, hierarchy, and
194
237
  retries.
195
238
 
196
239
  ## Notify Slack
197
240
 
198
241
  Invoker's optional Slack reporter posts one `Invoker Report` parent message per
199
- Vitest run. It places additional Workflow cards in the message thread so every
200
- Task table remains within Slack's row and character limits. Each card includes
201
- aggregate results, Workflow metadata, and a table of Task counts and durations.
202
- A shared footer contains the elapsed span from the first Case start to the final
203
- Case completion, a localized timestamp, and the optional run link. Final
204
- failures are posted in the same thread with one reply per failed Task in each
205
- Workflow. Unhandled run errors are reported once. Delivery failures are
206
- isolated to the affected reply. Ambiguous transport failures are not retried;
207
- an explicit Slack rate-limit rejection is reattempted only after its required
208
- delay.
242
+ Vitest run containing every Workflow card. Each card includes aggregate results,
243
+ Workflow metadata, and a table with counts and durations per Workflow
244
+ coordinate/Task pair. Failures, retries, and skips identify both Matrix scopes.
245
+ Collection failures produce a failed Workflow card even without collected Cases.
246
+ Full parameters are persisted in JSON rather than printed automatically in Slack.
247
+ A shared footer
248
+ contains the elapsed span from the first Case start to the final Case completion,
249
+ a localized timestamp, and the optional run link. Final failures, successful
250
+ retry details, skipped Case reasons, and unhandled run errors are posted in the
251
+ same thread. Delivery failures are isolated to the affected reply. Ambiguous
252
+ transport failures are not retried; an explicit Slack rate-limit rejection is
253
+ reattempted only after its required delay.
209
254
 
210
255
  ```ts
211
256
  import { slackReporter } from "@akshatmittal/invoker/slack";
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ //#region src/errors.ts
3
+ const errorMessageSchema = z.object({ message: z.string() });
4
+ const errorStackSchema = z.object({ stack: z.string() });
5
+ function errorMessage(cause) {
6
+ const message = errorMessageSchema.safeParse(cause);
7
+ if (message.success) return message.data.message;
8
+ const stack = errorStackSchema.safeParse(cause);
9
+ if (stack.success) return stack.data.stack.split("\n", 1)[0];
10
+ return String(cause);
11
+ }
12
+ function errorWithContext(context, cause) {
13
+ return new Error(`${context}: ${errorMessage(cause)}`, { cause });
14
+ }
15
+ //#endregion
16
+ export { errorWithContext as n, errorMessage as t };
package/dist/index.d.mts CHANGED
@@ -11,63 +11,70 @@ type Matrix = {
11
11
  readonly [axis: string]: readonly JsonValue[];
12
12
  };
13
13
  type CaseCoordinates<M extends Matrix> = { readonly [K in keyof M]: M[K][number]; };
14
- interface InvokerMeta<Coordinates extends JsonObject, Output extends JsonValue, Metadata extends JsonObject = JsonObject> {
15
- schema: 1;
16
- matrix: Coordinates;
14
+ interface InvokerMeta<Coordinates extends JsonObject, Output extends JsonValue, Metadata extends JsonObject = JsonObject, WorkflowCoordinates extends JsonObject = JsonObject, Params extends JsonObject = JsonObject> {
15
+ schema: 2;
16
+ matrix: {
17
+ workflow: WorkflowCoordinates;
18
+ task: Coordinates;
19
+ };
20
+ params: Params;
17
21
  metadata?: Metadata;
18
22
  output?: Output;
19
23
  }
20
- interface TaskContext<Coordinates, Setup> {
24
+ interface TaskContext<Coordinates, Setup, Params = Record<never, never>> extends ParamsContext<Params> {
21
25
  readonly matrix: Coordinates;
22
26
  readonly setup: Setup;
23
27
  readonly vitest: TestContext;
24
28
  }
25
29
  type Awaitable<Value> = Value | PromiseLike<Value>;
26
- type SetupContext<M extends Matrix> = {
30
+ type DeepReadonly<Value> = JsonValue extends Value ? Value : { readonly [Key in keyof Value]: DeepReadonly<Value[Key]>; };
31
+ type ParamsContext<Params> = {
32
+ readonly params: DeepReadonly<Params>;
33
+ };
34
+ type SetupContext<M extends Matrix, Params = Record<never, never>> = ParamsContext<Params> & {
27
35
  readonly cases: readonly CaseCoordinates<M>[];
28
36
  };
29
- type TeardownContext<M extends Matrix, Setup> = SetupContext<M> & {
37
+ type TeardownContext<M extends Matrix, Setup, Params = Record<never, never>> = SetupContext<M, Params> & {
30
38
  readonly setup: Setup;
31
39
  };
32
40
  //#endregion
33
41
  //#region src/task.d.ts
34
- declare const taskDefinitionBrand: unique symbol;
35
- interface TaskDefinition<Name extends string = string, M extends Matrix = Matrix, Setup = unknown, Output extends JsonValue = JsonValue> {
36
- readonly name: Name;
37
- readonly matrix: () => Promise<M>;
38
- readonly [taskDefinitionBrand]: true;
39
- readonly setup?: (context: SetupContext<M>) => Awaitable<Setup>;
40
- readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
41
- readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
42
- }
43
- type TaskWithSetup<Name extends string, M extends Matrix, Setup, Output extends JsonValue> = {
44
- readonly name: Name;
45
- readonly matrix?: () => Promise<M>;
46
- readonly setup: (context: SetupContext<M>) => Awaitable<Setup>;
47
- readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
48
- readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
49
- };
50
- type TaskWithoutSetup<Name extends string, M extends Matrix, Output extends JsonValue> = {
42
+ declare const taskBindingBrand: unique symbol;
43
+ type ParameterArguments<Params> = [Params] extends [Record<string, never>] ? [] : [params: Params];
44
+ type RequiredParameters<Params> = { [Key in keyof Params]-?: Params[Key] extends JsonValue ? RequiredParameters<Params[Key]> : never; };
45
+ type TaskDefinition<Name extends string = string, M extends Matrix = Matrix, Setup = unknown, Output extends JsonValue = JsonValue, Params extends JsonObject = Record<never, never>> = (...args: ParameterArguments<Params>) => BoundTask<Name, M, Setup, Output, Params>;
46
+ type TaskOptions<Name extends string, M extends Matrix, Setup, Output extends JsonValue, Params> = {
51
47
  readonly name: Name;
52
- readonly matrix?: () => Promise<M>;
53
- readonly setup?: never;
54
- readonly run: (context: TaskContext<CaseCoordinates<M>, undefined>) => Awaitable<Output>;
55
- readonly teardown?: never;
48
+ readonly matrix?: (context: ParamsContext<Params>) => Promise<M>;
49
+ readonly setup?: (context: SetupContext<M, Params>) => Awaitable<Setup>;
50
+ readonly run: (context: TaskContext<CaseCoordinates<M>, Setup, Params>) => Awaitable<Output>;
51
+ readonly teardown?: (context: TeardownContext<M, Setup, Params>) => Awaitable<void>;
56
52
  };
57
- declare function defineTask<const Name extends string, const M extends Matrix = Record<never, never>, Setup = unknown, const Output extends JsonValue = JsonValue>(definition: TaskWithSetup<Name, M, Setup, Output>): TaskDefinition<Name, M, Setup, Output>;
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
- type AnyTaskDefinition = {
60
- readonly name: string;
61
- readonly matrix: () => Promise<Matrix>;
62
- readonly [taskDefinitionBrand]: true;
53
+ interface BoundTask<Name extends string, M extends Matrix, Setup, Output extends JsonValue, Params extends JsonObject> extends TaskOptions<Name, M, Setup, Output, Params>, ParamsContext<Params> {
54
+ readonly matrix: (context: ParamsContext<Params>) => Promise<M>;
55
+ readonly [taskBindingBrand]: true;
56
+ }
57
+ declare function defineTask<Params extends Record<keyof Params, JsonValue> & RequiredParameters<Params> = Record<never, never>>(): {
58
+ <const Name extends string, const M extends Matrix = Record<never, never>, Setup = unknown, const Output extends JsonValue = JsonValue>(definition: TaskOptions<Name, M, Setup, Output, Params> & {
59
+ readonly setup: (context: SetupContext<M, Params>) => Awaitable<Setup>;
60
+ }): TaskDefinition<Name, M, Setup, Output, Params>;
61
+ <const Name extends string, const M extends Matrix = Record<never, never>, const Output extends JsonValue = JsonValue>(definition: TaskOptions<Name, M, undefined, Output, Params> & {
62
+ readonly setup?: never;
63
+ readonly teardown?: never;
64
+ }): TaskDefinition<Name, M, undefined, Output, Params>;
63
65
  };
66
+ type AnyTaskBinding = Pick<RuntimeTask, "name" | "params" | typeof taskBindingBrand>;
67
+ type RuntimeTask = BoundTask<string, Matrix, unknown, JsonValue, JsonObject>;
64
68
  //#endregion
65
69
  //#region src/workflow.d.ts
66
- type WorkflowDefinition<Tasks extends readonly [AnyTaskDefinition, ...AnyTaskDefinition[]], Metadata extends JsonObject> = {
70
+ type WorkflowDefinition<M extends Matrix, Metadata extends JsonObject> = {
67
71
  readonly name: string;
68
72
  readonly metadata?: Metadata;
69
- readonly tasks: Tasks;
73
+ readonly matrix?: () => Promise<M>;
74
+ readonly tasks: (context: {
75
+ readonly matrix: CaseCoordinates<M>;
76
+ }) => readonly [AnyTaskBinding, ...AnyTaskBinding[]];
70
77
  };
71
- declare function defineWorkflow<const Tasks extends readonly [AnyTaskDefinition, ...AnyTaskDefinition[]], const Metadata extends JsonObject = JsonObject>(definition: WorkflowDefinition<Tasks, Metadata>): void;
78
+ declare function defineWorkflow<const M extends Matrix = Record<never, never>, const Metadata extends JsonObject = JsonObject>(definition: WorkflowDefinition<M, Metadata>): void;
72
79
  //#endregion
73
80
  export { type CaseCoordinates, type InvokerMeta, type JsonObject, type JsonPrimitive, type JsonValue, type Matrix, type TaskContext, type TaskDefinition, defineTask, defineWorkflow };
package/dist/index.mjs CHANGED
@@ -1,13 +1,20 @@
1
- import { beforeAll, describe, test } from "vitest";
1
+ import { n as errorWithContext } from "./errors-CMczwLz9.mjs";
2
+ import { TestRunner, beforeAll, describe, test } from "vitest";
2
3
  import { z } from "zod";
3
4
  //#region src/task.ts
4
- const taskDefinitionBrand = Symbol("invoker.task");
5
- function defineTask(definition) {
6
- return {
7
- ...definition,
8
- matrix: definition.matrix ?? (async () => ({})),
9
- [taskDefinitionBrand]: true
10
- };
5
+ const taskBindingBrand = Symbol("invoker.task-binding");
6
+ function defineTask() {
7
+ function define(definition) {
8
+ return (...args) => {
9
+ return {
10
+ ...definition,
11
+ params: args.length === 0 ? {} : args[0],
12
+ matrix: definition.matrix ?? (async () => ({})),
13
+ [taskBindingBrand]: true
14
+ };
15
+ };
16
+ }
17
+ return define;
11
18
  }
12
19
  //#endregion
13
20
  //#region src/json.ts
@@ -18,8 +25,8 @@ const scalarSchema = z.union([
18
25
  z.number()
19
26
  ]);
20
27
  const nameSchema = z.string().trim().min(1);
21
- function snapshotJson(value, owner, path, ancestors = /* @__PURE__ */ new Set()) {
22
- return cloneJson(value, owner, path, ancestors);
28
+ function snapshotJson(value, owner, path) {
29
+ return cloneJson(value, owner, path, /* @__PURE__ */ new Set());
23
30
  }
24
31
  function cloneJson(value, owner, path, ancestors) {
25
32
  if (scalarSchema.safeParse(value).success) return value;
@@ -34,13 +41,12 @@ function cloneJson(value, owner, path, ancestors) {
34
41
  }
35
42
  ancestors.delete(value);
36
43
  return snapshot;
37
- } else {
38
- assertPlainObject(value, owner, path);
39
- if (Object.getOwnPropertySymbols(value).length > 0) fail(owner, path, "JSON objects cannot have symbol keys");
40
- const snapshot = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneJson(child, owner, `${path}.${key}`, ancestors)]));
41
- ancestors.delete(value);
42
- return snapshot;
43
44
  }
45
+ assertPlainObject(value, owner, path);
46
+ if (Object.getOwnPropertySymbols(value).length > 0) fail(owner, path, "JSON objects cannot have symbol keys");
47
+ const snapshot = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneJson(child, owner, `${path}.${key}`, ancestors)]));
48
+ ancestors.delete(value);
49
+ return snapshot;
44
50
  }
45
51
  function assertPlainObject(value, owner, path) {
46
52
  if (Object(value) !== value || Array.isArray(value)) fail(owner, path, "expected a plain object");
@@ -103,89 +109,155 @@ function caseName(matrix, index) {
103
109
  }
104
110
  //#endregion
105
111
  //#region src/workflow.ts
112
+ const functionSchema = z.function();
106
113
  function defineWorkflow(definition) {
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 }, () => {
110
- let setup;
111
- const setupTask = prepared.task.setup;
112
- if (setupTask) beforeAll(async () => {
113
- setup = await setupTask({ cases: prepared.cases });
114
- const teardownTask = prepared.task.teardown;
115
- if (teardownTask) return () => teardownTask({
116
- cases: prepared.cases,
117
- setup
118
- });
114
+ const info = { name: definition.name };
115
+ const meta = { invokerWorkflow: info };
116
+ describe(definition.name, {
117
+ concurrent: false,
118
+ shuffle: false,
119
+ meta
120
+ }, async () => {
121
+ const file = TestRunner.getCurrentSuite().file;
122
+ try {
123
+ const coordinates = await prepareWorkflow(definition, info);
124
+ for (const [index, coordinate] of coordinates.entries()) describe(caseName(coordinate.matrix, index), {
125
+ concurrent: false,
126
+ meta: { invokerWorkflow: null }
127
+ }, () => {
128
+ for (const prepared of coordinate.tasks) registerTask(prepared, coordinate.matrix, info.metadata);
129
+ });
130
+ } catch (cause) {
131
+ Object.assign(file.meta, { invokerCollectionError: info });
132
+ throw errorWithContext(`Workflow ${JSON.stringify(info.name)} collection failed`, cause);
133
+ }
134
+ });
135
+ }
136
+ function registerTask(prepared, workflow, metadata) {
137
+ const { task, cases } = prepared;
138
+ const { params } = task;
139
+ describe(task.name, { concurrent: false }, () => {
140
+ let setup;
141
+ const setupTask = task.setup;
142
+ if (setupTask) beforeAll(async () => {
143
+ setup = await setupTask({
144
+ params,
145
+ cases
146
+ });
147
+ const teardownTask = task.teardown;
148
+ if (teardownTask) return () => teardownTask({
149
+ params,
150
+ cases,
151
+ setup
119
152
  });
120
- for (const [index, matrix] of prepared.cases.entries()) {
121
- const name = prepared.names[index];
122
- const invoker = prepared.metadata[index];
123
- test.concurrent(name, { meta: { invoker } }, async (vitest) => {
124
- const meta = vitest.task.meta;
125
- delete meta.invoker.output;
126
- const output = await prepared.task.run({
127
- matrix,
128
- setup,
129
- vitest
130
- });
131
- meta.invoker.output = snapshotJson(output, `Task ${JSON.stringify(prepared.task.name)}`, ".output");
132
- });
133
- }
134
153
  });
154
+ for (const [index, matrix] of cases.entries()) {
155
+ const invoker = {
156
+ schema: 2,
157
+ matrix: {
158
+ workflow,
159
+ task: matrix
160
+ },
161
+ params
162
+ };
163
+ if (metadata !== void 0) invoker.metadata = metadata;
164
+ test.concurrent(caseName(matrix, index), { meta: { invoker } }, async (vitest) => {
165
+ const meta = vitest.task.meta;
166
+ delete meta.invoker.output;
167
+ const output = await task.run({
168
+ params,
169
+ matrix,
170
+ setup,
171
+ vitest
172
+ });
173
+ meta.invoker.output = snapshotJson(output, `Task ${JSON.stringify(task.name)}`, ".output");
174
+ });
175
+ }
135
176
  });
136
177
  }
137
- async function prepareWorkflow(definition) {
178
+ async function prepareWorkflow(definition, info) {
138
179
  assertPlainObject(definition, "Workflow", "");
139
180
  assertOnlyKeys(definition, [
140
181
  "name",
141
182
  "metadata",
183
+ "matrix",
142
184
  "tasks"
143
185
  ], "Workflow");
144
- const { name, metadata: workflowMetadata, tasks: taskDefinitions } = definition;
145
- assertName(name, "Workflow", ".name");
146
- const metadata = workflowMetadata === void 0 ? void 0 : snapshotJson(workflowMetadata, `Workflow ${JSON.stringify(name)}`, ".metadata");
147
- if (metadata !== void 0) assertPlainObject(metadata, `Workflow ${JSON.stringify(name)}`, ".metadata");
148
- if (!Array.isArray(taskDefinitions) || taskDefinitions.length === 0) fail("Workflow", ".tasks", "expected a non-empty Task tuple");
149
- const names = /* @__PURE__ */ new Set();
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;
186
+ assertName(definition.name, "Workflow", ".name");
187
+ const owner = `Workflow ${JSON.stringify(definition.name)}`;
188
+ if (definition.metadata !== void 0) {
189
+ const metadata = snapshotJson(definition.metadata, owner, ".metadata");
190
+ assertPlainObject(metadata, owner, ".metadata");
191
+ info.metadata = metadata;
192
+ }
193
+ if (!functionSchema.safeParse(definition.tasks).success) fail(owner, ".tasks", "expected a synchronous function");
194
+ if (definition.matrix !== void 0 && !functionSchema.safeParse(definition.matrix).success) fail(owner, ".matrix", "expected a function");
195
+ const coordinates = expandMatrix(definition.matrix ? await definition.matrix() : {}, owner).map((coordinate, index) => {
196
+ const coordinateOwner = `${owner} coordinate ${caseName(coordinate, index)}`;
197
+ return {
198
+ matrix: coordinate,
199
+ owner: coordinateOwner,
200
+ tasks: bindTasks(definition, coordinate, coordinateOwner)
201
+ };
172
202
  });
173
- return await Promise.all(tasks.map(async (task) => {
174
- const cases = expandMatrix(await task.matrix(), `Task ${JSON.stringify(task.name)}`);
203
+ return Promise.all(coordinates.map(async (coordinate) => ({
204
+ matrix: coordinate.matrix,
205
+ tasks: await Promise.all(coordinate.tasks.map((task) => discoverTask(task, coordinate.owner)))
206
+ })));
207
+ }
208
+ function bindTasks(definition, matrix, owner) {
209
+ try {
210
+ const bindings = definition.tasks({ matrix });
211
+ if (!Array.isArray(bindings) || bindings.length === 0) fail(owner, ".tasks", "expected a non-empty Task tuple");
212
+ const names = /* @__PURE__ */ new Set();
213
+ return bindings.map((binding) => prepareBinding(binding, owner, names));
214
+ } catch (cause) {
215
+ throw errorWithContext(`${owner} Task binding failed`, cause);
216
+ }
217
+ }
218
+ async function discoverTask(task, owner) {
219
+ const taskOwner = `${owner} Task ${JSON.stringify(task.name)}`;
220
+ try {
175
221
  return {
176
222
  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
- })
223
+ cases: expandMatrix(await task.matrix({ params: task.params }), taskOwner)
187
224
  };
188
- }));
225
+ } catch (cause) {
226
+ throw errorWithContext(`${taskOwner} Matrix discovery failed`, cause);
227
+ }
228
+ }
229
+ function prepareBinding(binding, owner, names) {
230
+ assertPlainObject(binding, owner, ".tasks");
231
+ if (binding[taskBindingBrand] !== true) fail(owner, ".tasks", "expected a bound Task created by defineTask");
232
+ const task = binding;
233
+ assertOnlyKeys(task, [
234
+ "name",
235
+ "params",
236
+ "matrix",
237
+ "setup",
238
+ "run",
239
+ "teardown"
240
+ ], owner);
241
+ assertName(task.name, owner, ".tasks.name");
242
+ if (names.has(task.name)) fail(owner, ".tasks", `duplicate Task name ${JSON.stringify(task.name)}`);
243
+ names.add(task.name);
244
+ const taskOwner = `${owner} Task ${JSON.stringify(task.name)}`;
245
+ for (const key of [
246
+ "matrix",
247
+ "run",
248
+ "setup",
249
+ "teardown"
250
+ ]) {
251
+ if ((key === "setup" || key === "teardown") && task[key] === void 0) continue;
252
+ if (!functionSchema.safeParse(task[key]).success) fail(taskOwner, `.${key}`, "expected a function");
253
+ }
254
+ if (task.teardown && !task.setup) fail(taskOwner, ".teardown", "requires setup");
255
+ const params = snapshotJson(task.params, taskOwner, ".params");
256
+ assertPlainObject(params, taskOwner, ".params");
257
+ return {
258
+ ...task,
259
+ params
260
+ };
189
261
  }
190
262
  //#endregion
191
263
  export { defineTask, defineWorkflow };
package/dist/slack.mjs CHANGED
@@ -1,67 +1,89 @@
1
+ import { t as errorMessage } from "./errors-CMczwLz9.mjs";
1
2
  import { z } from "zod";
2
3
  import { WebAPIRateLimitedError, WebClient } from "@slack/web-api";
3
4
  import { setTimeout } from "node:timers/promises";
4
- //#region src/slack/report.ts
5
- const stringSchema = z.string();
6
- const jsonObjectSchema = z.record(stringSchema, z.json());
7
- const invokerMetaSchema = z.strictObject({
8
- schema: z.literal(1),
9
- matrix: jsonObjectSchema,
5
+ //#region src/slack/collect.ts
6
+ const jsonObjectSchema = z.record(z.string(), z.json());
7
+ const workflowInfoSchema = z.object({
8
+ name: z.string(),
9
+ metadata: jsonObjectSchema.optional()
10
+ });
11
+ const workflowMetaSchema = z.object({ invokerWorkflow: workflowInfoSchema });
12
+ const collectionErrorSchema = z.object({ invokerCollectionError: workflowInfoSchema });
13
+ const testMetaSchema = z.object({ invoker: z.strictObject({
14
+ schema: z.literal(2),
15
+ matrix: z.strictObject({
16
+ workflow: jsonObjectSchema,
17
+ task: jsonObjectSchema
18
+ }),
19
+ params: jsonObjectSchema,
10
20
  metadata: jsonObjectSchema.optional(),
11
21
  output: z.json().optional()
12
- });
13
- const testMetaSchema = z.object({ invoker: invokerMetaSchema.optional() });
14
- const errorMessageSchema = z.object({ message: stringSchema });
15
- const errorStackSchema = z.object({ stack: stringSchema });
16
- const TABLE_ROW_LIMIT = 100;
17
- const TABLE_CHARACTER_LIMIT = 1e4;
18
- const SECTION_CHARACTER_LIMIT = 3e3;
19
- const NAME_CHARACTER_LIMIT = 200;
20
- const METADATA_CHARACTER_LIMIT = 1800;
22
+ }) });
21
23
  function collectWorkflowReports(modules) {
22
- const workflows = /* @__PURE__ */ new Map();
23
- for (const module of modules) for (const testCase of module.children.allTests()) {
24
- const meta = testMetaSchema.safeParse(testCase.meta());
25
- const invoker = meta.success ? meta.data.invoker : void 0;
26
- const taskSuite = testCase.parent;
27
- const workflowSuite = taskSuite.type === "suite" ? taskSuite.parent : void 0;
28
- if (!invoker || taskSuite.type !== "suite" || workflowSuite?.type !== "suite") continue;
29
- let workflow = workflows.get(workflowSuite);
30
- if (!workflow) {
31
- workflow = {
32
- name: workflowSuite.name,
33
- module,
34
- suite: workflowSuite,
35
- metadata: invoker.metadata,
36
- tasks: /* @__PURE__ */ new Map()
37
- };
38
- workflows.set(workflowSuite, workflow);
24
+ const workflows = [];
25
+ for (const module of modules) {
26
+ for (const suite of module.children.allSuites()) {
27
+ const meta = workflowMetaSchema.safeParse(suite.meta());
28
+ if (!meta.success) continue;
29
+ const tasks = [...suite.children.suites()].flatMap((coordinate) => [...coordinate.children.suites()].map((task) => collectTask(task, coordinate.name)));
30
+ const failures = [];
31
+ addErrors(failures, suite.errors());
32
+ for (const coordinate of suite.children.suites()) addErrors(failures, coordinate.errors(), coordinate.name);
33
+ workflows.push({
34
+ ...meta.data.invokerWorkflow,
35
+ tasks: tasks.map((task) => task.report),
36
+ failures: deduplicateFailures([...failures, ...tasks.flatMap((task) => task.failures)]),
37
+ retries: tasks.flatMap((task) => task.retries),
38
+ skips: tasks.flatMap((task) => task.skips),
39
+ ...timeSpan(tasks)
40
+ });
39
41
  }
40
- let task = workflow.tasks.get(taskSuite);
41
- if (!task) {
42
- task = {
43
- name: taskSuite.name,
44
- suite: taskSuite,
45
- total: 0,
46
- passed: 0,
47
- retried: 0,
48
- failed: 0,
49
- skipped: 0,
50
- incomplete: 0,
51
- failures: [],
52
- retries: []
53
- };
54
- workflow.tasks.set(taskSuite, task);
42
+ const collection = collectionErrorSchema.safeParse(module.meta());
43
+ if (collection.success) {
44
+ const failures = [];
45
+ addErrors(failures, module.errors());
46
+ workflows.push({
47
+ ...collection.data.invokerCollectionError,
48
+ tasks: [],
49
+ failures,
50
+ retries: [],
51
+ skips: []
52
+ });
55
53
  }
56
- task.total += 1;
54
+ }
55
+ return workflows;
56
+ }
57
+ function collectTask(suite, coordinateName) {
58
+ const name = `${coordinateName} · ${suite.name}`;
59
+ const report = {
60
+ name,
61
+ total: 0,
62
+ passed: 0,
63
+ retried: 0,
64
+ failed: 0,
65
+ skipped: 0,
66
+ incomplete: 0,
67
+ duration: 0
68
+ };
69
+ const failures = [];
70
+ const retries = [];
71
+ const skips = [];
72
+ let startedAt;
73
+ let endedAt;
74
+ for (const testCase of suite.children.tests()) {
75
+ const meta = testMetaSchema.safeParse(testCase.meta());
76
+ if (!meta.success) continue;
77
+ const { invoker } = meta.data;
57
78
  const result = testCase.result();
58
79
  const diagnostic = testCase.diagnostic();
59
- task[result.state === "pending" ? "incomplete" : result.state] += 1;
80
+ report.total += 1;
81
+ report[result.state === "pending" ? "incomplete" : result.state] += 1;
60
82
  const retryCount = diagnostic?.retryCount ?? 0;
61
83
  if (retryCount > 0) {
62
- task.retried += 1;
63
- if (result.state === "passed") task.retries.push({
64
- task: task.name,
84
+ report.retried += 1;
85
+ if (result.state === "passed") retries.push({
86
+ task: name,
65
87
  caseName: testCase.name,
66
88
  matrix: invoker.matrix,
67
89
  count: retryCount,
@@ -69,42 +91,80 @@ function collectWorkflowReports(modules) {
69
91
  });
70
92
  }
71
93
  if (diagnostic) {
72
- task.startedAt = Math.min(task.startedAt ?? diagnostic.startTime, diagnostic.startTime);
73
- task.endedAt = Math.max(task.endedAt ?? 0, diagnostic.startTime + diagnostic.duration);
94
+ startedAt = Math.min(startedAt ?? diagnostic.startTime, diagnostic.startTime);
95
+ endedAt = Math.max(endedAt ?? 0, diagnostic.startTime + diagnostic.duration);
74
96
  }
75
- if (result.state === "failed") task.failures.push({
76
- task: task.name,
97
+ if (result.state === "failed") failures.push({
98
+ task: name,
77
99
  caseName: testCase.name,
78
100
  matrix: invoker.matrix,
79
101
  messages: result.errors.map(errorMessage)
80
102
  });
103
+ else if (result.state === "skipped") skips.push({
104
+ task: name,
105
+ caseName: testCase.name,
106
+ matrix: invoker.matrix,
107
+ reason: result.note || "No reason provided"
108
+ });
81
109
  }
82
- return [...workflows.values()].map((workflow) => {
83
- const failures = [];
84
- const retries = [];
85
- addErrors(failures, workflow.suite.errors());
86
- addErrors(failures, workflow.module.errors());
87
- const collectedTasks = [...workflow.tasks.values()];
88
- for (const task of collectedTasks) {
89
- failures.push(...task.failures);
90
- retries.push(...task.retries);
91
- addErrors(failures, task.suite.errors(), task.name);
92
- }
93
- const { startedAt, endedAt } = timeSpan(collectedTasks);
94
- return {
95
- name: workflow.name,
96
- metadata: workflow.metadata,
97
- tasks: collectedTasks.map(({ suite: _suite, failures: _failures, retries: _retries, startedAt, endedAt, ...task }) => ({
98
- ...task,
99
- duration: startedAt === void 0 || endedAt === void 0 ? 0 : endedAt - startedAt
100
- })),
101
- failures: deduplicateFailures(failures),
102
- retries,
103
- startedAt,
104
- endedAt
105
- };
110
+ addErrors(failures, suite.errors(), name);
111
+ report.duration = startedAt === void 0 || endedAt === void 0 ? 0 : endedAt - startedAt;
112
+ return {
113
+ report,
114
+ failures,
115
+ retries,
116
+ skips,
117
+ startedAt,
118
+ endedAt
119
+ };
120
+ }
121
+ function addErrors(failures, errors, task) {
122
+ const messages = errors.map(errorMessage);
123
+ if (messages.length > 0) failures.push({
124
+ task,
125
+ messages
126
+ });
127
+ }
128
+ function deduplicateFailures(failures) {
129
+ const seen = /* @__PURE__ */ new Set();
130
+ return failures.flatMap((failure) => {
131
+ const messages = failure.messages.filter((message) => {
132
+ const key = JSON.stringify([
133
+ failure.task,
134
+ failure.caseName,
135
+ failure.matrix,
136
+ message
137
+ ]);
138
+ if (seen.has(key)) return false;
139
+ seen.add(key);
140
+ return true;
141
+ });
142
+ return messages.length > 0 ? [{
143
+ ...failure,
144
+ messages
145
+ }] : [];
106
146
  });
107
147
  }
148
+ function timeSpan(values) {
149
+ let startedAt;
150
+ let endedAt;
151
+ for (const value of values) {
152
+ if (value.startedAt !== void 0) startedAt = Math.min(startedAt ?? value.startedAt, value.startedAt);
153
+ if (value.endedAt !== void 0) endedAt = Math.max(endedAt ?? value.endedAt, value.endedAt);
154
+ }
155
+ return {
156
+ startedAt,
157
+ endedAt
158
+ };
159
+ }
160
+ //#endregion
161
+ //#region src/slack/report.ts
162
+ const stringSchema = z.string();
163
+ const TABLE_ROW_LIMIT = 100;
164
+ const TABLE_CHARACTER_LIMIT = 1e4;
165
+ const SECTION_CHARACTER_LIMIT = 3e3;
166
+ const NAME_CHARACTER_LIMIT = 200;
167
+ const METADATA_CHARACTER_LIMIT = 1800;
108
168
  function summaryMessage(reports, runUrl) {
109
169
  const timestamp = Math.floor(Date.now() / 1e3);
110
170
  const { startedAt, endedAt } = timeSpan(reports);
@@ -133,7 +193,8 @@ function workflowAttachment(report, rows, index, pages) {
133
193
  const status = workflowStatus(report);
134
194
  const metadata = metadataText(report.metadata);
135
195
  const page = pages > 1 ? ` (${index + 1}/${pages})` : "";
136
- const headline = `${status.emoji} *${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))} — ${totals.passed}/${totals.total} passed${page}*`;
196
+ const result = report.tasks.length === 0 ? "Collection failed" : `${totals.passed}/${totals.total} passed`;
197
+ const headline = `${status.emoji} *${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))} — ${result}${page}*`;
137
198
  return {
138
199
  color: status.color,
139
200
  blocks: [
@@ -168,88 +229,66 @@ function workflowAttachment(report, rows, index, pages) {
168
229
  }
169
230
  function failureMessages(report) {
170
231
  return [...Map.groupBy(report.failures, (failure) => failure.task)].flatMap(([task, failures]) => {
171
- const title = escapeSlack(truncate(task ?? "Workflow errors", NAME_CHARACTER_LIMIT));
172
- const workflow = `*Workflow:* ${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))}`;
173
- const metadata = metadataText(report.metadata);
174
- const context = metadata ? `${workflow} • ${metadata}` : workflow;
175
- return chunk(failures.map((failure) => {
232
+ return detailMessages(report, task, "failed", failures.map((failure) => {
176
233
  return `${failure.caseName ? `*${escapeSlack(failure.caseName)}*` : task ? "*Task error*" : "*Workflow error*"}${failure.matrix ? `\nMatrix: ${escapeSlack(JSON.stringify(failure.matrix))}` : ""}${failure.messages.map((message) => `\n• ${escapeSlack(message)}`).join("")}`;
177
- }), 3e3).map((details, index, messages) => {
178
- const part = messages.length > 1 ? ` (${index + 1}/${messages.length})` : "";
179
- return {
180
- text: `${truncate(report.name, NAME_CHARACTER_LIMIT)} › ${truncate(task ?? "Workflow", NAME_CHARACTER_LIMIT)} — ${failures.length} failed${part}`,
181
- attachments: [{
182
- color: "danger",
183
- blocks: [
184
- {
185
- type: "section",
186
- text: {
187
- type: "mrkdwn",
188
- text: `🔴 *${title} — ${failures.length} failed${part}*`
189
- }
190
- },
191
- {
192
- type: "context",
193
- elements: [{
194
- type: "mrkdwn",
195
- text: context
196
- }]
197
- },
198
- {
199
- type: "section",
200
- text: {
201
- type: "mrkdwn",
202
- text: details
203
- }
204
- }
205
- ]
206
- }]
207
- };
208
- });
234
+ }));
209
235
  });
210
236
  }
211
237
  function retryMessages(report) {
212
238
  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) => {
239
+ return detailMessages(report, task, "retried", retries.map((retry) => {
218
240
  const matrix = `\nMatrix: ${escapeSlack(JSON.stringify(retry.matrix))}`;
219
241
  const messages = retry.messages.map((message) => `\n• ${escapeSlack(message)}`).join("");
220
242
  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
- }
243
+ }));
244
+ });
245
+ }
246
+ function skipMessages(report) {
247
+ return [...Map.groupBy(report.skips, (skip) => skip.task)].flatMap(([task, skips]) => {
248
+ return detailMessages(report, task, "skipped", skips.map((skip) => {
249
+ const matrix = `\nMatrix: ${escapeSlack(JSON.stringify(skip.matrix))}`;
250
+ return `*${escapeSlack(skip.caseName)}*${matrix}\nReason: ${escapeSlack(skip.reason)}`;
251
+ }));
252
+ });
253
+ }
254
+ function detailMessages(report, task, outcome, entries) {
255
+ const title = escapeSlack(truncate(task ?? "Workflow errors", NAME_CHARACTER_LIMIT));
256
+ const workflow = `*Workflow:* ${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))}`;
257
+ const metadata = metadataText(report.metadata);
258
+ const context = metadata ? `${workflow} • ${metadata}` : workflow;
259
+ const emoji = outcome === "failed" ? "🔴" : "🟡";
260
+ return chunk(entries, SECTION_CHARACTER_LIMIT).map((details, index, messages) => {
261
+ const part = messages.length > 1 ? ` (${index + 1}/${messages.length})` : "";
262
+ const count = `${entries.length} ${outcome}${part}`;
263
+ return {
264
+ text: `${truncate(report.name, NAME_CHARACTER_LIMIT)} › ${truncate(task ?? "Workflow", NAME_CHARACTER_LIMIT)} — ${count}`,
265
+ attachments: [{
266
+ color: outcome === "failed" ? "danger" : "warning",
267
+ blocks: [
268
+ {
269
+ type: "section",
270
+ text: {
271
+ type: "mrkdwn",
272
+ text: `${emoji} *${title} — ${count}*`
248
273
  }
249
- ]
250
- }]
251
- };
252
- });
274
+ },
275
+ {
276
+ type: "context",
277
+ elements: [{
278
+ type: "mrkdwn",
279
+ text: context
280
+ }]
281
+ },
282
+ {
283
+ type: "section",
284
+ text: {
285
+ type: "mrkdwn",
286
+ text: details
287
+ }
288
+ }
289
+ ]
290
+ }]
291
+ };
253
292
  });
254
293
  }
255
294
  function unhandledErrorMessages(errors) {
@@ -277,40 +316,6 @@ function unhandledErrorMessages(errors) {
277
316
  };
278
317
  });
279
318
  }
280
- function addErrors(failures, errors, task) {
281
- const messages = errors.map(errorMessage);
282
- if (messages.length > 0) failures.push({
283
- task,
284
- messages
285
- });
286
- }
287
- function errorMessage(cause) {
288
- const message = errorMessageSchema.safeParse(cause);
289
- if (message.success) return message.data.message;
290
- const stack = errorStackSchema.safeParse(cause);
291
- if (stack.success) return stack.data.stack.split("\n", 1)[0];
292
- return String(cause);
293
- }
294
- function deduplicateFailures(failures) {
295
- const seen = /* @__PURE__ */ new Set();
296
- return failures.flatMap((failure) => {
297
- const messages = failure.messages.filter((message) => {
298
- const key = JSON.stringify([
299
- failure.task,
300
- failure.caseName,
301
- failure.matrix,
302
- message
303
- ]);
304
- if (seen.has(key)) return false;
305
- seen.add(key);
306
- return true;
307
- });
308
- return messages.length > 0 ? [{
309
- ...failure,
310
- messages
311
- }] : [];
312
- });
313
- }
314
319
  function escapeSlack(value) {
315
320
  return escapeSlackControl(value).replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "∼").replaceAll("`", "ˋ");
316
321
  }
@@ -398,7 +403,7 @@ function formatDuration(milliseconds) {
398
403
  function chunk(entries, limit) {
399
404
  const chunks = [];
400
405
  for (const entry of entries) {
401
- const value = entry.length > limit ? `${entry.slice(0, limit - 1)}…` : entry;
406
+ const value = truncate(entry, limit);
402
407
  const previous = chunks.at(-1);
403
408
  if (previous && previous.length + value.length + 2 <= limit) chunks[chunks.length - 1] = `${previous}\n\n${value}`;
404
409
  else chunks.push(value);
@@ -408,18 +413,6 @@ function chunk(entries, limit) {
408
413
  function truncate(value, limit) {
409
414
  return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
410
415
  }
411
- function timeSpan(values) {
412
- let startedAt;
413
- let endedAt;
414
- for (const value of values) {
415
- if (value.startedAt !== void 0) startedAt = Math.min(startedAt ?? value.startedAt, value.startedAt);
416
- if (value.endedAt !== void 0) endedAt = Math.max(endedAt ?? value.endedAt, value.endedAt);
417
- }
418
- return {
419
- startedAt,
420
- endedAt
421
- };
422
- }
423
416
  //#endregion
424
417
  //#region src/slack/reporter.ts
425
418
  function slackReporter(options) {
@@ -457,6 +450,7 @@ function slackReporter(options) {
457
450
  const replies = [
458
451
  ...reports.flatMap(failureMessages),
459
452
  ...reports.flatMap(retryMessages),
453
+ ...reports.flatMap(skipMessages),
460
454
  ...unhandledErrorMessages(unhandledErrors)
461
455
  ];
462
456
  if (replies.length > 0 && !parentTimestamp) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akshatmittal/invoker",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Typed matrix-driven regression workflows for Vitest.",
5
5
  "keywords": [
6
6
  "matrix",