@itmetsam/samurai-framework 0.22.4 → 0.22.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/agent/generate-kind.d.ts +5 -0
  2. package/dist/agent/generate-kind.js +45 -0
  3. package/dist/agent/index.d.ts +13 -3
  4. package/dist/agent/index.js +15 -2
  5. package/dist/agent/plan/front-matter.d.ts +9 -0
  6. package/dist/agent/plan/front-matter.js +18 -0
  7. package/dist/agent/plan/guard.d.ts +7 -0
  8. package/dist/agent/plan/guard.js +32 -0
  9. package/dist/agent/plan/plan-file.d.ts +67 -0
  10. package/dist/agent/plan/plan-file.js +229 -0
  11. package/dist/agent/plan/plan-kind.d.ts +5 -0
  12. package/dist/agent/plan/plan-kind.js +28 -0
  13. package/dist/agent/plan/plan-path.d.ts +8 -0
  14. package/dist/agent/plan/plan-path.js +46 -0
  15. package/dist/agent/plan/prompt.d.ts +3 -0
  16. package/dist/agent/plan/prompt.js +37 -0
  17. package/dist/agent/plan/questions.d.ts +5 -0
  18. package/dist/agent/plan/questions.js +22 -0
  19. package/dist/agent/plan/read-plan.d.ts +5 -0
  20. package/dist/agent/plan/read-plan.js +29 -0
  21. package/dist/agent/plan/recorder.d.ts +30 -0
  22. package/dist/agent/plan/recorder.js +59 -0
  23. package/dist/agent/plan/run.d.ts +8 -0
  24. package/dist/agent/plan/run.js +10 -0
  25. package/dist/agent/plan/tools.d.ts +4 -0
  26. package/dist/agent/plan/tools.js +95 -0
  27. package/dist/agent/plan-link.d.ts +8 -0
  28. package/dist/agent/plan-link.js +25 -0
  29. package/dist/agent/prompt.d.ts +6 -0
  30. package/dist/agent/prompt.js +20 -6
  31. package/dist/agent/recording.d.ts +6 -0
  32. package/dist/agent/recording.js +14 -0
  33. package/dist/agent/run-kind.d.ts +48 -0
  34. package/dist/agent/run-kind.js +1 -0
  35. package/dist/agent/run.d.ts +7 -3
  36. package/dist/agent/run.js +19 -27
  37. package/dist/agent/sdk-tools.js +10 -2
  38. package/dist/agent/tools/context.d.ts +12 -5
  39. package/dist/agent/tools/index.js +2 -0
  40. package/dist/agent/tools/undo.d.ts +1 -0
  41. package/dist/agent/tools/undo.js +23 -0
  42. package/dist/agent/types.d.ts +36 -7
  43. package/dist/cli/agent-run.d.ts +43 -0
  44. package/dist/cli/agent-run.js +132 -0
  45. package/dist/cli/args.d.ts +3 -0
  46. package/dist/cli/args.js +3 -0
  47. package/dist/cli/generate.d.ts +4 -8
  48. package/dist/cli/generate.js +18 -109
  49. package/dist/cli/main.js +3 -0
  50. package/dist/cli/plan.d.ts +6 -0
  51. package/dist/cli/plan.js +31 -0
  52. package/dist/cli/usage.d.ts +1 -1
  53. package/dist/cli/usage.js +9 -1
  54. package/dist/config/config.js +2 -0
  55. package/dist/config/defaults.d.ts +2 -0
  56. package/dist/config/defaults.js +2 -0
  57. package/dist/config/plans-dir.d.ts +3 -0
  58. package/dist/config/plans-dir.js +11 -0
  59. package/dist/config/resolved-config.d.ts +1 -0
  60. package/dist/config/resolved-config.js +2 -1
  61. package/dist/recorder/spec-source.d.ts +2 -0
  62. package/dist/recorder/spec-source.js +9 -0
  63. package/dist/runner/registry.js +6 -2
  64. package/dist/runner/test-info.js +1 -0
  65. package/dist/runner/test-io.d.ts +5 -1
  66. package/dist/runner/test-io.js +21 -0
  67. package/dist/steps/edit.d.ts +3 -1
  68. package/dist/steps/edit.js +1 -1
  69. package/dist/steps/model.d.ts +3 -1
  70. package/dist/steps/parse.js +2 -1
  71. package/dist/steps/test-options.d.ts +5 -3
  72. package/dist/steps/test-options.js +49 -0
  73. package/dist/types/config.d.ts +6 -0
  74. package/dist/types/test.d.ts +4 -0
  75. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { validateForbidOnlyConfig } from "./forbid-only.js";
8
8
  import { validateRetriesConfig } from "./retries.js";
9
9
  import { validateFullyParallelConfig } from "./fully-parallel.js";
10
10
  import { validateWorkersConfig } from "./workers.js";
11
+ import { validatePlansDirConfig } from "./plans-dir.js";
11
12
  import { validateWebServerConfig } from "./web-server-options.js";
12
13
  let activeProject;
13
14
  /** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
@@ -39,6 +40,7 @@ export function validateConfig(config) {
39
40
  validateGroupUse(config);
40
41
  validateEnvironmentUse(config);
41
42
  validateWebServerConfig(config);
43
+ validatePlansDirConfig(config);
42
44
  }
43
45
  /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
44
46
  export async function loadRawConfig() {
@@ -33,6 +33,8 @@ export declare const DEFAULT_VIEWPORT: {
33
33
  export declare const DEFAULT_DOWNLOADS_DIR = "result/downloads";
34
34
  /** The files folder, relative to the project */
35
35
  export declare const DEFAULT_FILES_DIR = "files";
36
+ /** Where test plans are written, relative to the project */
37
+ export declare const DEFAULT_PLANS_DIR = "plans";
36
38
  /** How many times a failing test is run again */
37
39
  export declare const DEFAULT_RETRIES = 0;
38
40
  /** Which page log entries go in the report: those of failed tests */
@@ -30,6 +30,8 @@ export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
30
30
  export const DEFAULT_DOWNLOADS_DIR = "result/downloads";
31
31
  /** The files folder, relative to the project */
32
32
  export const DEFAULT_FILES_DIR = "files";
33
+ /** Where test plans are written, relative to the project */
34
+ export const DEFAULT_PLANS_DIR = "plans";
33
35
  /** How many times a failing test is run again */
34
36
  export const DEFAULT_RETRIES = 0;
35
37
  /** Which page log entries go in the report: those of failed tests */
@@ -0,0 +1,3 @@
1
+ import type { SamuraiTestConfig } from "../types/config.js";
2
+ /** Checks the `plansDir` key of a config: a folder inside the project. Throws an Error that names the key */
3
+ export declare function validatePlansDirConfig(config: SamuraiTestConfig): void;
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ /** Checks the `plansDir` key of a config: a folder inside the project. Throws an Error that names the key */
3
+ export function validatePlansDirConfig(config) {
4
+ const dir = config.plansDir;
5
+ if (dir === undefined)
6
+ return;
7
+ if (typeof dir !== "string" || dir.trim() === "")
8
+ throw new Error(`plansDir must be a folder name relative to the project, got ${JSON.stringify(dir)}`);
9
+ if (path.isAbsolute(dir) || path.normalize(dir).split(path.sep)[0] === "..")
10
+ throw new Error(`plansDir must be a folder inside the project, got "${dir}"`);
11
+ }
@@ -57,6 +57,7 @@ export interface ResolvedConfig {
57
57
  } | null;
58
58
  downloadsDir: string;
59
59
  filesDir: string;
60
+ plansDir: string;
60
61
  logs: {
61
62
  capture: "off" | "failures" | "all";
62
63
  failOnPageError: boolean;
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { browsersFor } from "./browsers-for.js";
3
3
  import { importConfigFile, projectConfig, validateConfig } from "./config.js";
4
- import { DEFAULT_COLLECT_BODIES, DEFAULT_COMMAND_TIMEOUT, DEFAULT_DOWNLOADS_DIR, DEFAULT_FAIL_ON_PAGE_ERROR, DEFAULT_FILES_DIR, DEFAULT_IDLE_TIME, DEFAULT_LOGS_CAPTURE, DEFAULT_MAX_BODY_SIZE, DEFAULT_NAVIGATION_TIMEOUT, DEFAULT_RETRIES, DEFAULT_ROUTE_TIMEOUT, DEFAULT_SRC_DIR, DEFAULT_TRACK_NETWORK, DEFAULT_VIEWPORT, DEFAULT_WAIT_UNTIL, } from "./defaults.js";
4
+ import { DEFAULT_COLLECT_BODIES, DEFAULT_COMMAND_TIMEOUT, DEFAULT_DOWNLOADS_DIR, DEFAULT_FAIL_ON_PAGE_ERROR, DEFAULT_FILES_DIR, DEFAULT_PLANS_DIR, DEFAULT_IDLE_TIME, DEFAULT_LOGS_CAPTURE, DEFAULT_MAX_BODY_SIZE, DEFAULT_NAVIGATION_TIMEOUT, DEFAULT_RETRIES, DEFAULT_ROUTE_TIMEOUT, DEFAULT_SRC_DIR, DEFAULT_TRACK_NETWORK, DEFAULT_VIEWPORT, DEFAULT_WAIT_UNTIL, } from "./defaults.js";
5
5
  import { forbidsOnly } from "./forbid-only.js";
6
6
  import { IMPLICIT_ENVIRONMENT, resolveRunSettings, UnknownEnvironmentError, } from "./run-settings.js";
7
7
  import { resolveWorkers } from "./workers-option.js";
@@ -72,6 +72,7 @@ export function resolveConfig(config, options) {
72
72
  viewport: viewport === null ? null : { ...(viewport ?? DEFAULT_VIEWPORT) },
73
73
  downloadsDir: config.downloadsDir ?? DEFAULT_DOWNLOADS_DIR,
74
74
  filesDir: config.filesDir ?? DEFAULT_FILES_DIR,
75
+ plansDir: config.plansDir ?? DEFAULT_PLANS_DIR,
75
76
  logs: {
76
77
  capture: config.logs?.capture ?? DEFAULT_LOGS_CAPTURE,
77
78
  failOnPageError: config.logs?.failOnPageError ?? DEFAULT_FAIL_ON_PAGE_ERROR,
@@ -34,6 +34,8 @@ export declare class SpecWriter {
34
34
  private readonly testIndex;
35
35
  readonly events: RecorderEvent[];
36
36
  constructor(source: string, file: string, testIndex: number);
37
+ /** Changes the source with `change` and writes the file when it differs; returns the new content */
38
+ rewrite(change: (source: string) => string): string;
37
39
  /** Applies the event, writes the file and returns the file's new content */
38
40
  write(event: RecorderEvent): string;
39
41
  }
@@ -74,6 +74,15 @@ export class SpecWriter {
74
74
  this.testIndex = testIndex;
75
75
  this.events = [];
76
76
  }
77
+ /** Changes the source with `change` and writes the file when it differs; returns the new content */
78
+ rewrite(change) {
79
+ const next = change(this.source);
80
+ if (next !== this.source) {
81
+ this.source = next;
82
+ writeFileSync(this.file, this.source);
83
+ }
84
+ return this.source;
85
+ }
77
86
  /** Applies the event, writes the file and returns the file's new content */
78
87
  write(event) {
79
88
  this.source = applyRecorderEvent(this.source, this.testIndex, event);
@@ -7,7 +7,7 @@ import { validateUseOptions } from "../config/use-options.js";
7
7
  import { currentStepLog, currentTestInfo, infoFor, skipFromCondition, } from "./test-info.js";
8
8
  import { runStep } from "./steps.js";
9
9
  import { markTests } from "./test-annotations.js";
10
- import { validateTestIO } from "./test-io.js";
10
+ import { validateTestAnnotations, validateTestIO } from "./test-io.js";
11
11
  import { addHook, addUse, currentSuite, enterSuite, leaveSuite, modifierOf, resetSuites, } from "./suite.js";
12
12
  import { configureSuite } from "./serial.js";
13
13
  const registered = [];
@@ -26,7 +26,7 @@ function openDescribe(file, title, fn, modifier, reason, mode) {
26
26
  }
27
27
  }
28
28
  function add({ file, line, column }, title, fn, modifier, extras = {}) {
29
- const { reason, inputs, outputs, extension, expectedFail } = extras;
29
+ const { reason, inputs, outputs, annotations, extension, expectedFail } = extras;
30
30
  const suite = currentSuite(file);
31
31
  const titlePath = [...titlesOf(suite), title];
32
32
  const name = titlePath.join(" > ");
@@ -40,6 +40,7 @@ function add({ file, line, column }, title, fn, modifier, extras = {}) {
40
40
  ...modifierOf({ modifier, ...(reason !== undefined && { reason }) }, suite),
41
41
  ...(inputs && { inputs }),
42
42
  ...(outputs && { outputs }),
43
+ ...(annotations && { annotations }),
43
44
  ...(extension && { extension }),
44
45
  ...(expectedFail && { expectedFail }),
45
46
  });
@@ -65,6 +66,9 @@ function bodyAndOptions(call, title, second, third) {
65
66
  fn: fn,
66
67
  ...(reason !== undefined && { reason }),
67
68
  ...(options && call.startsWith("test") && validateTestIO(call, options)),
69
+ ...(options &&
70
+ call.startsWith("test") &&
71
+ validateTestAnnotations(call, options)),
68
72
  };
69
73
  }
70
74
  /** `test()` and `test.only()`: a reason only makes sense with skip and fixme */
@@ -19,6 +19,7 @@ export class TestInfoImpl {
19
19
  const { test } = from;
20
20
  this.title = test.titlePath[test.titlePath.length - 1] ?? test.name;
21
21
  this.titlePath = [...test.titlePath];
22
+ this.annotations = (test.annotations ?? []).map((a) => ({ ...a }));
22
23
  this.file = test.file;
23
24
  if (test.line !== undefined)
24
25
  this.line = test.line;
@@ -1,4 +1,4 @@
1
- import type { TestError, TestOptions, TestOutputs } from "../types/test.js";
1
+ import type { TestAnnotation, TestError, TestOptions, TestOutputs } from "../types/test.js";
2
2
  import type { OutputSink } from "../wait/output-sink.js";
3
3
  /**
4
4
  * Checks the `inputs` and `outputs` of a `test()` options object and returns them, leaving out an empty or absent
@@ -8,6 +8,10 @@ export declare function validateTestIO(call: string, options: TestOptions): {
8
8
  inputs?: Record<string, string>;
9
9
  outputs?: string[];
10
10
  };
11
+ /** Checks the `annotations` of a `test()` options object and returns them, or nothing when absent or empty */
12
+ export declare function validateTestAnnotations(call: string, options: TestOptions): {
13
+ annotations?: TestAnnotation[];
14
+ };
11
15
  /**
12
16
  * The `inputs` fixture: each declared input with the value the run supplies, else its default. Reading a
13
17
  * name the test did not declare throws.
@@ -45,6 +45,27 @@ export function validateTestIO(call, options) {
45
45
  }
46
46
  return result;
47
47
  }
48
+ /** Checks the `annotations` of a `test()` options object and returns them, or nothing when absent or empty */
49
+ export function validateTestAnnotations(call, options) {
50
+ const { annotations } = options;
51
+ if (annotations === undefined)
52
+ return {};
53
+ if (!Array.isArray(annotations))
54
+ throw new Error(`${call}(): annotations must be an array`);
55
+ for (const note of annotations) {
56
+ const entry = note;
57
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
58
+ throw new Error(`${call}(): each annotation must be an object with a type`);
59
+ if (typeof entry.type !== "string" || entry.type === "")
60
+ throw new Error(`${call}(): an annotation needs a non-empty type`);
61
+ if (entry.description !== undefined &&
62
+ typeof entry.description !== "string")
63
+ throw new Error(`${call}(): the description of annotation "${entry.type}" must be a string`);
64
+ }
65
+ return annotations.length > 0
66
+ ? { annotations: annotations.map((a) => ({ ...a })) }
67
+ : {};
68
+ }
48
69
  /**
49
70
  * The `inputs` fixture: each declared input with the value the run supplies, else its default. Reading a
50
71
  * name the test did not declare throws.
@@ -1,3 +1,4 @@
1
+ import type { TestAnnotation } from "../types/test.js";
1
2
  import type { Step, TestInput } from "./model.js";
2
3
  export type StepEdit = {
3
4
  op: "replace";
@@ -34,7 +35,7 @@ export type StepEdit = {
34
35
  test: number;
35
36
  }
36
37
  /**
37
- * Sets the test's `inputs` and `outputs` in its options object: a given list replaces that list, one left out
38
+ * Sets the test's `inputs`, `outputs` and `annotations` in its options object: a given list replaces that list, one left out
38
39
  * stays. When both end up empty the options object goes, unless it holds a `reason`
39
40
  */
40
41
  | {
@@ -42,6 +43,7 @@ export type StepEdit = {
42
43
  test: number;
43
44
  inputs?: TestInput[];
44
45
  outputs?: string[];
46
+ annotations?: TestAnnotation[];
45
47
  };
46
48
  /**
47
49
  * Applies one edit and returns the new source. Only the touched statement changes, plus the test's parameters
@@ -158,7 +158,7 @@ export function applyEdit(source, edit) {
158
158
  return withStep(source, parsed, edit.step, (text, code) => insertCode(text, parsed, edit.index, code));
159
159
  case "declare": {
160
160
  let next = source;
161
- for (const key of ["inputs", "outputs"]) {
161
+ for (const key of ["inputs", "outputs", "annotations"]) {
162
162
  const list = edit[key];
163
163
  if (list)
164
164
  next = declareList(next, pickTest(next, edit.test), key, list);
@@ -1,4 +1,4 @@
1
- import type { Modifier } from "../types/test.js";
1
+ import type { Modifier, TestAnnotation } from "../types/test.js";
2
2
  /**
3
3
  * One call of a locator expression, as written in a spec: `page.getByRole("button", { name: "Save" })`.
4
4
  * A RegExp text or name is tested as written, so it never comes with `exact`, `match` or `ignoreCase`.
@@ -264,6 +264,8 @@ export interface ParsedTest {
264
264
  inputs?: TestInput[];
265
265
  /** The `outputs` of the test's options object; undefined without the key */
266
266
  outputs?: string[];
267
+ /** The `annotations` of the test's options object; undefined without the key or with an empty list */
268
+ annotations?: TestAnnotation[];
267
269
  /** The range of the test's options object (`test(title, { … }, fn)`), when it has one */
268
270
  options?: {
269
271
  start: number;
@@ -137,7 +137,7 @@ export function parseSpecFile(source) {
137
137
  return;
138
138
  }
139
139
  const { callback, options = {}, range } = args;
140
- const { reason, inputs, outputs } = options;
140
+ const { reason, inputs, outputs, annotations } = options;
141
141
  const body = callbackBody(callback);
142
142
  if (!body) {
143
143
  unparsed.push(unreadable(node, kind, "not-inline"));
@@ -161,6 +161,7 @@ export function parseSpecFile(source) {
161
161
  ...effectiveMark(mark, describes),
162
162
  ...(inputs && { inputs }),
163
163
  ...(outputs && { outputs }),
164
+ ...(annotations && { annotations }),
164
165
  ...(range && { options: range }),
165
166
  titleEnd: node.arguments[0].end,
166
167
  });
@@ -1,13 +1,15 @@
1
1
  import ts from "typescript";
2
- import type { Modifier } from "../types/test.js";
2
+ import type { Modifier, TestAnnotation } from "../types/test.js";
3
3
  import type { ParsedTest, TestInput } from "./model.js";
4
4
  /** What a `test()` or `describe()` options object says, as far as the codec reads it */
5
5
  export interface TestOptionsRead {
6
6
  reason?: string;
7
7
  inputs?: TestInput[];
8
8
  outputs?: string[];
9
+ annotations?: TestAnnotation[];
9
10
  }
10
- type ListKey = "inputs" | "outputs";
11
+ type ListKey = "inputs" | "outputs" | "annotations";
12
+ type ListValue = TestInput[] | string[] | TestAnnotation[];
11
13
  /**
12
14
  * The options object of a test or describe call: a describe takes only `reason`, a test also `inputs` and
13
15
  * `outputs`. Undefined when the object isn't a literal the codec can read, so the call is left unparsed. An
@@ -21,5 +23,5 @@ export declare function readTestOptions(node: ts.Expression, kind: "test" | "des
21
23
  * the object, its layout and a `reason`, stays as it is; an unchanged list leaves the source as it is. Throws for
22
24
  * names the runtime rejects, before writing
23
25
  */
24
- export declare function declareList(source: string, parsed: ParsedTest, key: ListKey, list: TestInput[] | string[]): string;
26
+ export declare function declareList(source: string, parsed: ParsedTest, key: ListKey, list: ListValue): string;
25
27
  export {};
@@ -33,6 +33,35 @@ function readOutputs(node) {
33
33
  ? names
34
34
  : undefined;
35
35
  }
36
+ /** `[{ type: "plan", description: "…" }, …]`; undefined for anything but string literals under those two keys */
37
+ function readAnnotations(node) {
38
+ if (!ts.isArrayLiteralExpression(node))
39
+ return undefined;
40
+ const notes = [];
41
+ for (const element of node.elements) {
42
+ if (!ts.isObjectLiteralExpression(element))
43
+ return undefined;
44
+ const note = {};
45
+ for (const property of element.properties) {
46
+ if (!ts.isPropertyAssignment(property))
47
+ return undefined;
48
+ const key = keyOf(property.name);
49
+ const value = stringOf(property.initializer);
50
+ if (value === undefined)
51
+ return undefined;
52
+ if (key === "type")
53
+ note.type = value;
54
+ else if (key === "description")
55
+ note.description = value;
56
+ else
57
+ return undefined;
58
+ }
59
+ if (note.type === undefined || note.type === "")
60
+ return undefined;
61
+ notes.push(note);
62
+ }
63
+ return notes;
64
+ }
36
65
  /**
37
66
  * The options object of a test or describe call: a describe takes only `reason`, a test also `inputs` and
38
67
  * `outputs`. Undefined when the object isn't a literal the codec can read, so the call is left unparsed. An
@@ -67,13 +96,27 @@ export function readTestOptions(node, kind, modifier) {
67
96
  if (outputs.length > 0)
68
97
  read.outputs = outputs;
69
98
  }
99
+ else if (kind === "test" && key === "annotations") {
100
+ const annotations = readAnnotations(value);
101
+ if (!annotations)
102
+ return undefined;
103
+ if (annotations.length > 0)
104
+ read.annotations = annotations;
105
+ }
70
106
  else
71
107
  return undefined;
72
108
  }
73
109
  return read;
74
110
  }
75
111
  const str = (value) => JSON.stringify(value);
112
+ function annotationToSource(note) {
113
+ return note.description === undefined
114
+ ? `{ type: ${str(note.type)} }`
115
+ : `{ type: ${str(note.type)}, description: ${str(note.description)} }`;
116
+ }
76
117
  function listToSource(key, list) {
118
+ if (key === "annotations")
119
+ return `[${list.map(annotationToSource).join(", ")}]`;
77
120
  if (key === "outputs")
78
121
  return `[${list.map(str).join(", ")}]`;
79
122
  const entries = list.map(({ name, default: value }) => `${IDENTIFIER.test(name) ? name : str(name)}: ${str(value)}`);
@@ -82,6 +125,12 @@ function listToSource(key, list) {
82
125
  const sameList = (a, b) => JSON.stringify(a) === JSON.stringify(b);
83
126
  /** Throws for a list the runtime's `test()` would reject: a name that isn't valid, a name twice, a default that isn't text */
84
127
  function checkList(key, list) {
128
+ if (key === "annotations") {
129
+ for (const note of list)
130
+ if (typeof note.type !== "string" || note.type === "")
131
+ throw new Error(`applyEdit(): an annotation needs a non-empty type`);
132
+ return;
133
+ }
85
134
  const kind = key === "inputs" ? "input" : "output";
86
135
  const seen = new Set();
87
136
  for (const entry of list) {
@@ -204,6 +204,12 @@ export interface SamuraiTestConfig {
204
204
  * @default "files"
205
205
  */
206
206
  filesDir?: string;
207
+ /**
208
+ * Folder, relative to the project folder, where test plans are kept; `samurai plan "<goal>"` without `--out`
209
+ * writes `<plansDir>/<slug-of-goal>.plan.md`
210
+ * @default "plans"
211
+ */
212
+ plansDir?: string;
207
213
  /**
208
214
  * The AI agent (`samurai generate`). `model` is a `provider:name` pair, such as `anthropic:claude-sonnet-4-5`;
209
215
  * never a key (keys come from the environment). The `--model` flag and `SAMURAI_AI_MODEL` win over it
@@ -170,6 +170,8 @@ export interface TestOptions extends TestDetails {
170
170
  inputs?: Record<string, string>;
171
171
  /** Names of the text values the test saves for later tests to use */
172
172
  outputs?: string[];
173
+ /** Notes on the test, such as `{ type: "plan", description: "plans/pay.plan.md" }`; the report lists them first */
174
+ annotations?: TestAnnotation[];
173
175
  }
174
176
 
175
177
  /** Runs before or after each test of a suite, in the test's browser, with the test's fixtures */
@@ -225,6 +227,8 @@ export interface RegisteredTestCase extends TestCaseBase {
225
227
  inputs?: Record<string, string>;
226
228
  /** The names of the outputs the test declared */
227
229
  outputs?: string[];
230
+ /** The annotations the test declared; every run of the test starts with them */
231
+ annotations?: TestAnnotation[];
228
232
  /** Registered with `test.fail("title", fn)`: expected to fail; `description` is the `{ reason }` given */
229
233
  expectedFail?: { description?: string };
230
234
  /** The custom fixtures of the `test` it was written with (`test.extend()`); absent for the plain `test` */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.22.4",
3
+ "version": "0.22.6",
4
4
  "description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
5
5
  "main": "dist/api.js",
6
6
  "exports": {