@farmslot/agent-runtime 0.12.0 → 0.14.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/farmslot-agent.mjs +7 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/native/registry.d.ts.map +1 -1
  8. package/dist/native/registry.js +6 -1
  9. package/dist/native/registry.js.map +1 -1
  10. package/dist/task-init/index.d.ts.map +1 -1
  11. package/dist/task-init/index.js +1 -0
  12. package/dist/task-init/index.js.map +1 -1
  13. package/dist/task-init/task-document.d.ts +16 -2
  14. package/dist/task-init/task-document.d.ts.map +1 -1
  15. package/dist/task-init/task-document.js +21 -8
  16. package/dist/task-init/task-document.js.map +1 -1
  17. package/package.json +7 -3
  18. package/scripts/acceptance-cli.cjs +139 -0
  19. package/scripts/acceptance-ledger.cjs +413 -0
  20. package/scripts/check-task-artifact-contract.mjs +25 -1
  21. package/scripts/checklist-target.cjs +56 -0
  22. package/scripts/mark-checklist-step.cjs +111 -61
  23. package/scripts/mark-io.cjs +118 -0
  24. package/scripts/review-terminal.cjs +6 -1
  25. package/scripts/subtask-unit.cjs +725 -0
  26. package/scripts/task-init-cli.mjs +4 -0
  27. package/scripts/worker-terminal-contract.cjs +25 -1
  28. package/dist/execution-template/execution-template.test.d.ts +0 -2
  29. package/dist/execution-template/execution-template.test.d.ts.map +0 -1
  30. package/dist/execution-template/execution-template.test.js +0 -795
  31. package/dist/execution-template/execution-template.test.js.map +0 -1
  32. package/dist/native/account-observation.test.d.ts +0 -2
  33. package/dist/native/account-observation.test.d.ts.map +0 -1
  34. package/dist/native/account-observation.test.js +0 -256
  35. package/dist/native/account-observation.test.js.map +0 -1
  36. package/dist/native/account-profiles.test.d.ts +0 -2
  37. package/dist/native/account-profiles.test.d.ts.map +0 -1
  38. package/dist/native/account-profiles.test.js +0 -172
  39. package/dist/native/account-profiles.test.js.map +0 -1
  40. package/dist/native/acp.test.d.ts +0 -2
  41. package/dist/native/acp.test.d.ts.map +0 -1
  42. package/dist/native/acp.test.js +0 -257
  43. package/dist/native/acp.test.js.map +0 -1
  44. package/dist/native/affinity.test.d.ts +0 -2
  45. package/dist/native/affinity.test.d.ts.map +0 -1
  46. package/dist/native/affinity.test.js +0 -30
  47. package/dist/native/affinity.test.js.map +0 -1
  48. package/dist/native/cursor-model.test.d.ts +0 -2
  49. package/dist/native/cursor-model.test.d.ts.map +0 -1
  50. package/dist/native/cursor-model.test.js +0 -40
  51. package/dist/native/cursor-model.test.js.map +0 -1
  52. package/dist/native/durability.test.d.ts +0 -2
  53. package/dist/native/durability.test.d.ts.map +0 -1
  54. package/dist/native/durability.test.js +0 -976
  55. package/dist/native/durability.test.js.map +0 -1
  56. package/dist/native/filesystem-policy.test.d.ts +0 -2
  57. package/dist/native/filesystem-policy.test.d.ts.map +0 -1
  58. package/dist/native/filesystem-policy.test.js +0 -136
  59. package/dist/native/filesystem-policy.test.js.map +0 -1
  60. package/dist/native/process-tree.test.d.ts +0 -2
  61. package/dist/native/process-tree.test.d.ts.map +0 -1
  62. package/dist/native/process-tree.test.js +0 -174
  63. package/dist/native/process-tree.test.js.map +0 -1
  64. package/dist/native/process.test.d.ts +0 -2
  65. package/dist/native/process.test.d.ts.map +0 -1
  66. package/dist/native/process.test.js +0 -56
  67. package/dist/native/process.test.js.map +0 -1
  68. package/dist/native/registry.test.d.ts +0 -2
  69. package/dist/native/registry.test.d.ts.map +0 -1
  70. package/dist/native/registry.test.js +0 -13
  71. package/dist/native/registry.test.js.map +0 -1
  72. package/dist/native/review-sandbox.test.d.ts +0 -2
  73. package/dist/native/review-sandbox.test.d.ts.map +0 -1
  74. package/dist/native/review-sandbox.test.js +0 -40
  75. package/dist/native/review-sandbox.test.js.map +0 -1
  76. package/dist/native/worker-history.test.d.ts +0 -2
  77. package/dist/native/worker-history.test.d.ts.map +0 -1
  78. package/dist/native/worker-history.test.js +0 -195
  79. package/dist/native/worker-history.test.js.map +0 -1
  80. package/dist/native/worker-launch.test.d.ts +0 -2
  81. package/dist/native/worker-launch.test.d.ts.map +0 -1
  82. package/dist/native/worker-launch.test.js +0 -61
  83. package/dist/native/worker-launch.test.js.map +0 -1
  84. package/dist/native/worker-relocation.test.d.ts +0 -2
  85. package/dist/native/worker-relocation.test.d.ts.map +0 -1
  86. package/dist/native/worker-relocation.test.js +0 -110
  87. package/dist/native/worker-relocation.test.js.map +0 -1
  88. package/dist/task-init/discover.test.d.ts +0 -2
  89. package/dist/task-init/discover.test.d.ts.map +0 -1
  90. package/dist/task-init/discover.test.js +0 -58
  91. package/dist/task-init/discover.test.js.map +0 -1
@@ -1,795 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import test from 'node:test';
6
- import { createExecutionTemplate } from './create.js';
7
- import { parseMarkdownDocument } from './frontmatter.js';
8
- import { catalogRelativeId, inferFlowFromBasename, inferTemplateMetadata } from './infer.js';
9
- import { lintExecutionTemplates, lintExecutionTemplateText } from './lint.js';
10
- import { listProjectWorkerTemplateOptions, resolveProjectWorkerTemplateForRun, } from './project-worker.js';
11
- import { customTemplateSource, listExecutionTemplates, packageFlowTreeTemplateSource, projectWorkerTemplateSource, } from './resolve.js';
12
- import { listCompatibleExecutionTemplates, selectExecutionTemplate } from './select.js';
13
- import { executionTemplateReference, materializeExecutionTemplate, sha256Text, } from './snapshot.js';
14
- import { resolveConfiguredExecutionTemplateSources } from './source-config.js';
15
- import { ExecutionTemplateSelectionError } from './types.js';
16
- function withTemp(fn) {
17
- const root = mkdtempSync(join(tmpdir(), 'farmslot-exec-tpl-'));
18
- try {
19
- fn(root);
20
- }
21
- finally {
22
- rmSync(root, { recursive: true, force: true });
23
- }
24
- }
25
- async function withTempAsync(fn) {
26
- const root = mkdtempSync(join(tmpdir(), 'farmslot-exec-tpl-'));
27
- try {
28
- await fn(root);
29
- }
30
- finally {
31
- rmSync(root, { recursive: true, force: true });
32
- }
33
- }
34
- test('inferFlowFromBasename uses longest Farmslot flow prefix', () => {
35
- assert.equal(inferFlowFromBasename('fix-bug-autonomous.mobile.md'), 'fix-bug');
36
- assert.equal(inferFlowFromBasename('pr-complete-interactive.md'), 'pr-complete');
37
- assert.equal(inferFlowFromBasename('dev.md'), 'dev');
38
- assert.equal(inferFlowFromBasename('review-pr.md'), 'review-pr');
39
- assert.equal(inferFlowFromBasename('notes.md'), null);
40
- });
41
- test('self-review-fix resolves as its own prefix, not self-review', () => {
42
- assert.equal(inferFlowFromBasename('self-review-fix.md'), 'self-review-fix');
43
- assert.equal(inferFlowFromBasename('self-review.md'), 'self-review');
44
- });
45
- test('catalog ids collide across worker-flat and flow-tree layouts', () => {
46
- // Precedence/shadowing is keyed on id — a project worker-flat override and a
47
- // package flow-tree base MUST normalize to the same id.
48
- assert.equal(catalogRelativeId('fix-bug.core.md', 'worker-flat'), 'fix-bug/core');
49
- assert.equal(catalogRelativeId('fix-bug/core.md', 'flow-tree'), 'fix-bug/core');
50
- assert.equal(catalogRelativeId('dev.md', 'worker-flat'), 'dev/default');
51
- assert.equal(catalogRelativeId('dev-interactive.md', 'worker-flat'), 'dev/interactive');
52
- assert.equal(catalogRelativeId('notes.md', 'worker-flat'), 'notes');
53
- });
54
- test('shared project-worker selector preserves no-config option ordering', async () => {
55
- await withTempAsync(async (root) => {
56
- const worker = join(root, 'templates', 'worker');
57
- mkdirSync(worker, { recursive: true });
58
- writeFileSync(join(worker, 'fix-bug-v2.md'), '# V2\n');
59
- writeFileSync(join(worker, 'fix-bug.md'), '# Default\n');
60
- writeFileSync(join(worker, 'fix-bug-interactive.md'), '# Interactive\n');
61
- writeFileSync(join(worker, 'dev.md'), '# Other flow\n');
62
- const options = await listProjectWorkerTemplateOptions(join(root, 'templates'), 'fix-bug');
63
- assert.deepEqual(options.map((option) => option.fileName), ['fix-bug.md', 'fix-bug-interactive.md', 'fix-bug-v2.md']);
64
- assert.equal(options[0]?.isDefault, true);
65
- assert.equal(options[1]?.variant, 'interactive');
66
- });
67
- });
68
- test('shared project-worker selector preserves explicit, interactive, domain, and fallback precedence', async () => {
69
- await withTempAsync(async (root) => {
70
- const templates = join(root, 'templates');
71
- const worker = join(templates, 'worker');
72
- mkdirSync(worker, { recursive: true });
73
- writeFileSync(join(worker, 'fix-bug.md'), '# Default\n');
74
- writeFileSync(join(worker, 'fix-bug-interactive.md'), '# Interactive\n');
75
- writeFileSync(join(worker, 'fix-bug-trading.md'), '# Trading\n');
76
- writeFileSync(join(worker, 'fix-bug-v2.md'), '# Explicit\n');
77
- const explicit = await resolveProjectWorkerTemplateForRun(templates, 'fix-bug', 'interactive', { fileName: 'fix-bug-v2.md' }, 'trading');
78
- assert.equal(explicit.fileName, 'fix-bug-v2.md');
79
- assert.equal(explicit.selectionSource, 'explicit');
80
- const interactive = await resolveProjectWorkerTemplateForRun(templates, 'fix-bug', 'interactive', null, 'trading');
81
- assert.equal(interactive.fileName, 'fix-bug-interactive.md');
82
- assert.equal(interactive.selectionSource, 'implicit-interactive-fix-bug');
83
- const domain = await resolveProjectWorkerTemplateForRun(templates, 'fix-bug', 'autonomous', null, 'trading');
84
- assert.equal(domain.fileName, 'fix-bug-trading.md');
85
- assert.equal(domain.selectionSource, 'implicit-domain');
86
- const fallback = await resolveProjectWorkerTemplateForRun(templates, 'fix-bug', 'autonomous', null, 'payments');
87
- assert.equal(fallback.fileName, 'fix-bug.md');
88
- assert.equal(fallback.content, '# Default\n');
89
- assert.equal(fallback.selectionSource, 'default');
90
- });
91
- });
92
- test('inferTemplateMetadata prefers frontmatter then filename/heading', () => {
93
- const meta = inferTemplateMetadata({
94
- absolutePath: '/tmp/dev-autonomous.mobile.md',
95
- relativePath: 'dev/dev-autonomous.mobile.md',
96
- source: {
97
- id: 'package:recipe-cook',
98
- kind: 'package',
99
- root: '/tmp',
100
- layout: 'flow-tree',
101
- },
102
- text: `---
103
- id: custom-dev-mobile
104
- title: Custom Dev
105
- description: Choose for autonomous Mobile feature work.
106
- runMode: autonomous
107
- platforms: [mobile]
108
- version: 2
109
- ---
110
-
111
- # Ignored heading
112
-
113
- - [ ] Do the work
114
- `,
115
- });
116
- assert.equal(meta.id, 'custom-dev-mobile');
117
- assert.equal(meta.title, 'Custom Dev');
118
- assert.equal(meta.description, 'Choose for autonomous Mobile feature work.');
119
- assert.equal(meta.flow, 'dev');
120
- assert.equal(meta.version, '2');
121
- // A legacy `runMode:` key parses into raw frontmatter and is ignored: it is
122
- // not promoted to an entry field, because templates have no run mode.
123
- assert.equal(meta.frontmatter?.runMode, 'autonomous');
124
- assert.equal('runMode' in meta, false);
125
- assert.deepEqual(meta.platforms, ['mobile']);
126
- assert.equal(meta.sha256, sha256Text(`---
127
- id: custom-dev-mobile
128
- title: Custom Dev
129
- description: Choose for autonomous Mobile feature work.
130
- runMode: autonomous
131
- platforms: [mobile]
132
- version: 2
133
- ---
134
-
135
- # Ignored heading
136
-
137
- - [ ] Do the work
138
- `));
139
- });
140
- test('source domains add exact domain labels without removing frontmatter labels', () => {
141
- const meta = inferTemplateMetadata({
142
- absolutePath: '/tmp/fix-bug/mobile.md',
143
- relativePath: 'fix-bug/mobile.md',
144
- source: {
145
- id: 'team:trading',
146
- kind: 'workspace',
147
- root: '/tmp',
148
- layout: 'flow-tree',
149
- domains: ['trading'],
150
- },
151
- text: '---\nlabels: [runtime-proof]\n---\n\n# Proof\n\n- [ ] Run\n',
152
- });
153
- assert.deepEqual(meta.labels, ['runtime-proof', 'domain:trading']);
154
- });
155
- test('standalone domain filters keep general and exact-domain templates only', () => {
156
- withTemp((root) => {
157
- const generalRoot = join(root, 'general');
158
- const tradingRoot = join(root, 'trading');
159
- const paymentsRoot = join(root, 'payments');
160
- for (const sourceRoot of [generalRoot, tradingRoot, paymentsRoot]) {
161
- mkdirSync(join(sourceRoot, 'fix-bug'), { recursive: true });
162
- writeFileSync(join(sourceRoot, 'fix-bug', 'mobile.md'), '# Proof\n\n- [ ] Run\n');
163
- }
164
- const trading = packageFlowTreeTemplateSource('trading', tradingRoot);
165
- trading.kind = 'workspace';
166
- trading.domains = ['trading'];
167
- const payments = packageFlowTreeTemplateSource('payments', paymentsRoot);
168
- payments.kind = 'workspace';
169
- payments.domains = ['payments'];
170
- const entries = listExecutionTemplates({
171
- sources: [packageFlowTreeTemplateSource('general', generalRoot), trading, payments],
172
- domain: 'trading',
173
- });
174
- assert.deepEqual(entries.map((entry) => entry.sourceId).sort(), [
175
- 'package:general',
176
- 'package:trading',
177
- ]);
178
- });
179
- });
180
- test('a template is compatible regardless of any legacy runMode frontmatter or filename', () => {
181
- withTemp((root) => {
182
- const source = join(root, 'catalog');
183
- mkdirSync(join(source, 'dev'), { recursive: true });
184
- writeFileSync(join(source, 'dev', 'general.mobile.md'), '# General\n\n- [ ] Run\n');
185
- writeFileSync(join(source, 'dev', 'autonomous.mobile.md'), '---\nrunMode: autonomous\n---\n\n# Exact\n\n- [ ] Run\n');
186
- const sources = [packageFlowTreeTemplateSource('catalog', source)];
187
- // Both are listed for either run mode: the legacy marker filters nothing.
188
- for (const runMode of ['autonomous', 'interactive']) {
189
- assert.deepEqual(listCompatibleExecutionTemplates({ sources, flow: 'dev', platform: 'mobile', runMode })
190
- .map((entry) => entry.id)
191
- .sort(), ['dev/autonomous.mobile', 'dev/general.mobile']);
192
- assert.throws(() => selectExecutionTemplate({ sources, flow: 'dev', platform: 'mobile', runMode }), /ambiguous/);
193
- }
194
- const explicit = selectExecutionTemplate({
195
- sources,
196
- flow: 'dev',
197
- platform: 'mobile',
198
- runMode: 'interactive',
199
- explicitId: 'dev/autonomous.mobile',
200
- });
201
- assert.equal(explicit.entry.title, 'Exact');
202
- assert.equal(explicit.reason, 'explicit');
203
- });
204
- });
205
- test('default rules match the run mode of the run, not any property of a template', () => {
206
- withTemp((root) => {
207
- // Worker-flat pack holding BOTH dev.md and dev-interactive.md: the only
208
- // thing that distinguishes them is a legacy filename token, so selection
209
- // must come from the project default rules and nowhere else.
210
- const worker = join(root, 'templates', 'worker');
211
- mkdirSync(worker, { recursive: true });
212
- writeFileSync(join(worker, 'dev.md'), '# Dev\n\n- [ ] Run\n');
213
- writeFileSync(join(worker, 'dev-interactive.md'), '---\nchecklist: none\n---\n\n# Dev chat\n');
214
- const sources = [projectWorkerTemplateSource('pack', join(root, 'templates'))];
215
- const defaults = [
216
- { when: { flow: 'dev', runMode: 'interactive' }, templateId: 'dev/interactive' },
217
- { when: { flow: 'dev' }, templateId: 'dev/default' },
218
- ];
219
- const interactive = selectExecutionTemplate({
220
- sources,
221
- flow: 'dev',
222
- platform: 'mobile',
223
- runMode: 'interactive',
224
- defaults,
225
- });
226
- assert.equal(interactive.entry.id, 'dev/interactive');
227
- assert.equal(interactive.reason, 'configured-default');
228
- const autonomous = selectExecutionTemplate({
229
- sources,
230
- flow: 'dev',
231
- platform: 'mobile',
232
- runMode: 'autonomous',
233
- defaults,
234
- });
235
- assert.equal(autonomous.entry.id, 'dev/default');
236
- assert.equal(autonomous.reason, 'configured-default');
237
- // Without a rule the two are just two general candidates: the selector
238
- // reports the normal ambiguity instead of silently picking by mode.
239
- assert.throws(() => selectExecutionTemplate({
240
- sources,
241
- flow: 'dev',
242
- platform: 'mobile',
243
- runMode: 'interactive',
244
- }), /ambiguous.*dev\/default, dev\/interactive/s);
245
- // A run with no mode at all falls through the interactive rule to the
246
- // catch-all instead of inventing a default mode.
247
- const noMode = selectExecutionTemplate({
248
- sources,
249
- flow: 'dev',
250
- platform: 'mobile',
251
- defaults,
252
- });
253
- assert.equal(noMode.entry.id, 'dev/default');
254
- });
255
- });
256
- test('configured selection prefers one exact-domain candidate before a general candidate', () => {
257
- withTemp((root) => {
258
- const generalRoot = join(root, 'general');
259
- const tradingRoot = join(root, 'trading');
260
- mkdirSync(join(generalRoot, 'fix-bug'), { recursive: true });
261
- mkdirSync(join(tradingRoot, 'fix-bug'), { recursive: true });
262
- writeFileSync(join(generalRoot, 'fix-bug', 'mobile.md'), '# General\n\n- [ ] Run\n');
263
- writeFileSync(join(tradingRoot, 'fix-bug', 'mobile.md'), '# Trading\n\n- [ ] Run\n');
264
- const tradingSource = packageFlowTreeTemplateSource('trading', tradingRoot);
265
- tradingSource.kind = 'workspace';
266
- tradingSource.domains = ['trading'];
267
- const selected = selectExecutionTemplate({
268
- sources: [packageFlowTreeTemplateSource('canonical', generalRoot), tradingSource],
269
- flow: 'fix-bug',
270
- platform: 'mobile',
271
- runMode: 'autonomous',
272
- domain: 'trading',
273
- });
274
- assert.equal(selected.entry.sourceId, 'package:trading');
275
- assert.equal(selected.reason, 'single-domain-candidate');
276
- const fallback = selectExecutionTemplate({
277
- sources: [packageFlowTreeTemplateSource('canonical', generalRoot), tradingSource],
278
- flow: 'fix-bug',
279
- platform: 'mobile',
280
- runMode: 'autonomous',
281
- domain: 'payments',
282
- });
283
- assert.equal(fallback.entry.sourceId, 'package:canonical');
284
- assert.equal(fallback.reason, 'single-general-candidate');
285
- assert.equal(listExecutionTemplates({ sources: [tradingSource] })[0]?.labels.includes('domain:trading'), true);
286
- });
287
- });
288
- test('explicit id excluded only by the source domain gate names the enabling domain', () => {
289
- withTemp((root) => {
290
- const generalRoot = join(root, 'general');
291
- const perpsRoot = join(root, 'perps');
292
- mkdirSync(join(generalRoot, 'review-pr'), { recursive: true });
293
- mkdirSync(join(perpsRoot, 'review-pr'), { recursive: true });
294
- writeFileSync(join(generalRoot, 'review-pr', 'default.md'), '# General\n\n- [ ] Run\n');
295
- writeFileSync(join(perpsRoot, 'review-pr', 'autonomous.mobile.md'), '# Perps\n\n- [ ] Run\n');
296
- const perpsSource = packageFlowTreeTemplateSource('perps', perpsRoot);
297
- perpsSource.kind = 'workspace';
298
- perpsSource.domains = ['perps'];
299
- const sources = [packageFlowTreeTemplateSource('canonical', generalRoot), perpsSource];
300
- assert.throws(() => selectExecutionTemplate({
301
- sources,
302
- flow: 'review-pr',
303
- platform: 'mobile',
304
- runMode: 'autonomous',
305
- explicitId: 'review-pr/autonomous.mobile',
306
- }), (error) => {
307
- assert.ok(error instanceof ExecutionTemplateSelectionError);
308
- assert.equal(error.code, 'missing-or-incompatible');
309
- assert.match(error.message, /domain-restricted/);
310
- assert.match(error.message, /package:perps \(domain: perps\)/);
311
- return true;
312
- });
313
- const selected = selectExecutionTemplate({
314
- sources,
315
- flow: 'review-pr',
316
- platform: 'mobile',
317
- runMode: 'autonomous',
318
- domain: 'perps',
319
- explicitId: 'review-pr/autonomous.mobile',
320
- });
321
- assert.equal(selected.entry.sourceId, 'package:perps');
322
- assert.equal(selected.reason, 'explicit');
323
- });
324
- });
325
- test('entry-label domain gates are named, wrong domains still point at the right one, and plain misses stay generic', () => {
326
- withTemp((root) => {
327
- const catalogRoot = join(root, 'catalog');
328
- mkdirSync(join(catalogRoot, 'dev'), { recursive: true });
329
- writeFileSync(join(catalogRoot, 'dev', 'special.md'), '---\nlabels: [domain:money-movement]\n---\n\n# Special\n\n- [ ] Run\n');
330
- writeFileSync(join(catalogRoot, 'dev', 'default.md'), '# General\n\n- [ ] Run\n');
331
- const sources = [packageFlowTreeTemplateSource('catalog', catalogRoot)];
332
- const base = {
333
- sources,
334
- flow: 'dev',
335
- platform: 'mobile',
336
- runMode: 'autonomous',
337
- };
338
- assert.throws(() => selectExecutionTemplate({ ...base, explicitId: 'dev/special' }), /domain-restricted.*money-movement/s);
339
- // The wrong requested domain must still name the enabling one.
340
- assert.throws(() => selectExecutionTemplate({ ...base, domain: 'perps', explicitId: 'dev/special' }), /money-movement/);
341
- // A genuinely absent id keeps the generic message — no invented domains.
342
- assert.throws(() => selectExecutionTemplate({ ...base, explicitId: 'dev/nope' }), (error) => {
343
- assert.ok(error instanceof ExecutionTemplateSelectionError);
344
- assert.match(error.message, /is missing or incompatible/);
345
- assert.doesNotMatch(error.message, /domain-restricted/);
346
- return true;
347
- });
348
- });
349
- });
350
- test('mixed source-domain and entry-label gates name only domains that actually work', () => {
351
- withTemp((root) => {
352
- const catalogRoot = join(root, 'catalog');
353
- mkdirSync(join(catalogRoot, 'dev'), { recursive: true });
354
- // Source restricted to perps; the entry also carries a money-movement
355
- // frontmatter label (plus the injected domain:perps). Only perps passes
356
- // BOTH gates — the diagnostic must not advertise money-movement, which the
357
- // source gate would reject.
358
- writeFileSync(join(catalogRoot, 'dev', 'mixed.md'), '---\nlabels: [domain:money-movement]\n---\n\n# Mixed\n\n- [ ] Run\n');
359
- const mixedSource = packageFlowTreeTemplateSource('mixed', catalogRoot);
360
- mixedSource.kind = 'workspace';
361
- mixedSource.domains = ['perps'];
362
- const base = {
363
- sources: [mixedSource],
364
- flow: 'dev',
365
- platform: 'mobile',
366
- runMode: 'autonomous',
367
- explicitId: 'dev/mixed',
368
- };
369
- for (const domain of [undefined, 'money-movement']) {
370
- assert.throws(() => selectExecutionTemplate({ ...base, ...(domain ? { domain } : {}) }), (error) => {
371
- assert.ok(error instanceof ExecutionTemplateSelectionError);
372
- assert.match(error.message, /domain-restricted/);
373
- assert.match(error.message, /\(domain: perps\)/);
374
- assert.doesNotMatch(error.message, /money-movement/);
375
- return true;
376
- });
377
- }
378
- const selected = selectExecutionTemplate({ ...base, domain: 'perps' });
379
- assert.equal(selected.entry.id, 'dev/mixed');
380
- assert.equal(selected.reason, 'explicit');
381
- });
382
- });
383
- test('configured defaults are ordered and incompatible targets fail instead of falling through', () => {
384
- withTemp((root) => {
385
- const source = join(root, 'catalog');
386
- mkdirSync(join(source, 'fix-bug'), { recursive: true });
387
- writeFileSync(join(source, 'fix-bug', 'mobile.md'), '# Mobile\n\n- [ ] Run\n');
388
- writeFileSync(join(source, 'fix-bug', 'default.extension.md'), '# Extension\n\n- [ ] Run\n');
389
- const sources = [packageFlowTreeTemplateSource('catalog', source)];
390
- const selected = selectExecutionTemplate({
391
- sources,
392
- flow: 'fix-bug',
393
- platform: 'mobile',
394
- runMode: 'autonomous',
395
- defaults: [
396
- { when: { flow: 'fix-bug' }, templateId: 'fix-bug/mobile' },
397
- { when: { platform: 'mobile' }, templateId: 'fix-bug/default.extension' },
398
- ],
399
- });
400
- assert.equal(selected.entry.id, 'fix-bug/mobile');
401
- assert.equal(selected.reason, 'configured-default');
402
- assert.throws(() => selectExecutionTemplate({
403
- sources,
404
- flow: 'fix-bug',
405
- platform: 'mobile',
406
- runMode: 'autonomous',
407
- defaults: [
408
- { when: { flow: 'fix-bug' }, templateId: 'fix-bug/default.extension' },
409
- { when: { platform: 'mobile' }, templateId: 'fix-bug/mobile' },
410
- ],
411
- }), /default "fix-bug\/default\.extension" is missing or incompatible/);
412
- });
413
- });
414
- test('portable reference hashes source and rendered Markdown without absolute paths', () => {
415
- const source = '# Source\n\n- [ ] {{task}}\n';
416
- const entry = inferTemplateMetadata({
417
- absolutePath: '/private/machine/catalog/dev/mobile.md',
418
- relativePath: 'dev/mobile.md',
419
- source: {
420
- id: 'package:canonical',
421
- kind: 'package',
422
- root: '/private/machine/catalog',
423
- layout: 'flow-tree',
424
- sourceRevision: 'abc123',
425
- },
426
- text: source,
427
- });
428
- const reference = executionTemplateReference(entry, '# Source\n\n- [ ] prove it\n');
429
- assert.equal(reference.sha256, sha256Text(source));
430
- assert.equal(reference.renderedSha256, sha256Text('# Source\n\n- [ ] prove it\n'));
431
- assert.equal(reference.sourceRevision, 'abc123');
432
- assert.equal(reference.relativePath, 'dev/mobile.md');
433
- assert.equal('path' in reference, false);
434
- assert.equal(reference.version, undefined);
435
- });
436
- test('materializeExecutionTemplate copies one immutable snapshot and records its digest', () => {
437
- withTemp((root) => {
438
- const templates = join(root, 'templates');
439
- const sourcePath = join(templates, 'dev', 'autonomous.mobile.md');
440
- const outputPath = join(root, 'task', 'CHECKLIST.md');
441
- mkdirSync(join(templates, 'dev'), { recursive: true });
442
- writeFileSync(sourcePath, '# Mobile proof\n\n- [ ] Validate it.\n');
443
- const [entry] = listExecutionTemplates({
444
- sources: [packageFlowTreeTemplateSource('canonical', templates)],
445
- });
446
- assert.ok(entry);
447
- const result = materializeExecutionTemplate(entry, outputPath);
448
- assert.equal(readFileSync(outputPath, 'utf8'), '# Mobile proof\n\n- [ ] Validate it.\n');
449
- assert.equal(result.reference.sha256, result.reference.renderedSha256);
450
- assert.equal('path' in result.reference, false);
451
- assert.throws(() => materializeExecutionTemplate(entry, outputPath), /destination already exists/);
452
- });
453
- });
454
- test('configured sources resolve lazily and constrain subpaths below their root', () => {
455
- withTemp((root) => {
456
- const projectPack = join(root, 'project-pack');
457
- const library = join(root, 'library');
458
- const outside = join(root, 'outside');
459
- mkdirSync(join(projectPack, 'local', 'checklists', 'dev'), { recursive: true });
460
- mkdirSync(join(library, 'checklists', 'dev'), { recursive: true });
461
- mkdirSync(outside, { recursive: true });
462
- symlinkSync(outside, join(library, 'escaped'));
463
- const result = resolveConfiguredExecutionTemplateSources({
464
- sources: [
465
- {
466
- id: 'project:local',
467
- kind: 'workspace',
468
- root: { projectPath: 'local' },
469
- subpath: 'checklists',
470
- },
471
- {
472
- id: 'team:available',
473
- kind: 'package',
474
- root: { env: 'TEAM_ROOT' },
475
- subpath: 'checklists',
476
- },
477
- {
478
- id: 'team:unset',
479
- kind: 'package',
480
- root: { env: 'UNSET_ROOT' },
481
- },
482
- {
483
- id: 'team:escaped',
484
- kind: 'package',
485
- root: { env: 'TEAM_ROOT' },
486
- subpath: 'escaped',
487
- },
488
- ],
489
- }, {
490
- projectPackRoot: projectPack,
491
- env: { TEAM_ROOT: library },
492
- });
493
- assert.deepEqual(result.sources.map((source) => source.id), ['project:local', 'team:available']);
494
- assert.deepEqual(result.unavailable, [
495
- { id: 'team:unset', reason: 'missing-environment' },
496
- { id: 'team:escaped', reason: 'invalid-root' },
497
- ]);
498
- });
499
- });
500
- test('listExecutionTemplates reports shadowing across project and package sources', () => {
501
- withTemp((root) => {
502
- const projectWorker = join(root, 'project', 'templates', 'worker');
503
- const packageTpl = join(root, 'skills', 'templates');
504
- mkdirSync(projectWorker, { recursive: true });
505
- mkdirSync(join(packageTpl, 'dev'), { recursive: true });
506
- writeFileSync(join(projectWorker, 'dev.md'), '# Legacy Dev\n\n- [ ] Farm step\n', 'utf8');
507
- // Same explicit id as legacy mapping would not collide; create a colliding id via frontmatter.
508
- writeFileSync(join(packageTpl, 'dev', 'dev-autonomous.mobile.md'), `---
509
- id: dev/default
510
- runMode: autonomous
511
- platforms: [mobile]
512
- ---
513
-
514
- # Shared Dev
515
-
516
- - [ ] Shared step
517
- `, 'utf8');
518
- const entries = listExecutionTemplates({
519
- sources: [
520
- projectWorkerTemplateSource('metamask-mobile-farm', join(root, 'project', 'templates')),
521
- packageFlowTreeTemplateSource('recipe-cook', packageTpl),
522
- ],
523
- includeShadowed: true,
524
- });
525
- const winners = entries.filter((e) => !e.shadowedBy && e.id === 'dev/default');
526
- const shadowed = entries.filter((e) => e.shadowedBy && e.id === 'dev/default');
527
- assert.equal(winners.length, 1);
528
- assert.equal(winners[0]?.sourceKind, 'project');
529
- assert.equal(shadowed.length, 1);
530
- assert.equal(shadowed[0]?.sourceKind, 'package');
531
- assert.equal(shadowed[0]?.shadowedBy, 'project:metamask-mobile-farm');
532
- });
533
- });
534
- test('lintExecutionTemplates accepts valid checkboxes and rejects fix-ticket / bad boxes', () => {
535
- withTemp((root) => {
536
- const ok = join(root, 'dev-autonomous.mobile.md');
537
- writeFileSync(ok, '# Ok\n\n- [ ] one\n- [x] two\n', 'utf8');
538
- const okResult = lintExecutionTemplates(ok);
539
- assert.equal(okResult.ok, true);
540
- const bad = join(root, 'dev-bad.md');
541
- writeFileSync(bad, '# Bad\n\n- [?] broken\nfix-ticket leftover\n', 'utf8');
542
- const badResult = lintExecutionTemplates(bad);
543
- assert.equal(badResult.ok, false);
544
- assert.ok(badResult.issues.some((i) => /fix-ticket/.test(i.message)));
545
- assert.ok(badResult.issues.some((i) => /checkbox/.test(i.message)));
546
- const badDescription = join(root, 'dev-autonomous.mobile.md');
547
- writeFileSync(badDescription, '---\ndescription: 42\n---\n\n# Bad description\n\n- [ ] step\n', 'utf8');
548
- const badDescriptionResult = lintExecutionTemplates(badDescription);
549
- assert.equal(badDescriptionResult.ok, false);
550
- assert.ok(badDescriptionResult.issues.some((i) => /description/.test(i.message)));
551
- });
552
- });
553
- test('createExecutionTemplate writes lint-clean starter with optional frontmatter', () => {
554
- withTemp((root) => {
555
- const target = join(root, 'dev', 'dev-autonomous.mobile.md');
556
- const created = createExecutionTemplate({
557
- path: target,
558
- platforms: ['mobile'],
559
- description: 'Choose when the agent should implement without operator checkpoints.',
560
- });
561
- assert.equal(created.created, true);
562
- const text = readFileSync(target, 'utf8');
563
- // A starter never writes a run mode: the filename token does not become one.
564
- assert.doesNotMatch(text, /runMode/);
565
- assert.match(text, /platforms: \[mobile\]/);
566
- assert.match(text, /description: "Choose when the agent should implement without operator checkpoints\."/);
567
- assert.match(text, /- \[ \] Read the task prompt/);
568
- assert.equal(lintExecutionTemplates(target).ok, true);
569
- });
570
- });
571
- test('createExecutionTemplate quotes description frontmatter safely', () => {
572
- withTemp((root) => {
573
- const target = join(root, 'dev', 'autonomous.mobile.md');
574
- createExecutionTemplate({
575
- path: target,
576
- description: 'Use for "quoted" guidance.\nKeep this in one YAML value.',
577
- });
578
- const text = readFileSync(target, 'utf8');
579
- assert.match(text, /description: "Use for \\"quoted\\" guidance\.\\nKeep this in one YAML value\."/);
580
- assert.equal(lintExecutionTemplates(target).ok, true);
581
- assert.equal(listExecutionTemplates({ sources: [customTemplateSource('test', root)] })[0]?.description, 'Use for "quoted" guidance.\nKeep this in one YAML value.');
582
- });
583
- });
584
- test('quoted numeric description remains a string', () => {
585
- withTemp((root) => {
586
- const target = join(root, 'dev', 'autonomous.md');
587
- createExecutionTemplate({ path: target, description: '42' });
588
- assert.equal(lintExecutionTemplates(target).ok, true);
589
- assert.equal(listExecutionTemplates({ sources: [customTemplateSource('test', root)] })[0]?.description, '42');
590
- });
591
- });
592
- test('custom source outranks package source for the same id', () => {
593
- withTemp((root) => {
594
- const custom = join(root, 'custom');
595
- const pkg = join(root, 'pkg');
596
- mkdirSync(join(custom, 'dev'), { recursive: true });
597
- mkdirSync(join(pkg, 'dev'), { recursive: true });
598
- const body = (label) => `---\nid: shared-dev\n---\n\n# ${label}\n\n- [ ] step\n`;
599
- writeFileSync(join(custom, 'dev', 'dev-autonomous.mobile.md'), body('custom'), 'utf8');
600
- writeFileSync(join(pkg, 'dev', 'dev-autonomous.mobile.md'), body('package'), 'utf8');
601
- const entries = listExecutionTemplates({
602
- sources: [
603
- packageFlowTreeTemplateSource('recipe-cook', pkg),
604
- customTemplateSource('overlay', custom),
605
- ],
606
- });
607
- const winner = entries.find((e) => e.id === 'shared-dev' && !e.shadowedBy);
608
- assert.equal(winner?.sourceKind, 'custom');
609
- assert.equal(winner?.title, 'custom');
610
- });
611
- });
612
- test('shadowing resolves before filters — a filtered winner still shadows its duplicate', () => {
613
- const dir = mkdtempSync(join(tmpdir(), 'et-filter-'));
614
- try {
615
- mkdirSync(join(dir, 'proj', 'worker'), { recursive: true });
616
- mkdirSync(join(dir, 'pkg', 'dev'), { recursive: true });
617
- // Project winner is mobile-only; package duplicate claims extension.
618
- writeFileSync(join(dir, 'proj', 'worker', 'dev.md'), '---\nplatforms: [mobile]\n---\n\n# P\n\n- [ ] step\n');
619
- writeFileSync(join(dir, 'pkg', 'dev', 'default.md'), '---\nplatforms: [extension]\n---\n\n# S\n\n- [ ] step\n');
620
- const entries = listExecutionTemplates({
621
- sources: [
622
- projectWorkerTemplateSource('p', join(dir, 'proj')),
623
- packageFlowTreeTemplateSource('s', join(dir, 'pkg')),
624
- ],
625
- platform: 'extension',
626
- });
627
- // The package copy is shadowed by the (filtered-out) project winner — the
628
- // filter must not resurrect it as effective.
629
- const effective = entries.filter((e) => !e.shadowedBy);
630
- assert.equal(effective.length, 0);
631
- const shadowedEntry = entries.find((e) => e.shadowedBy);
632
- assert.equal(shadowedEntry?.sourceKind, 'package');
633
- }
634
- finally {
635
- rmSync(dir, { recursive: true, force: true });
636
- }
637
- });
638
- test('same-kind precedence follows caller order, not source-id alphabet', () => {
639
- const dir = mkdtempSync(join(tmpdir(), 'et-order-'));
640
- try {
641
- mkdirSync(join(dir, 'zeta', 'dev'), { recursive: true });
642
- mkdirSync(join(dir, 'alpha', 'dev'), { recursive: true });
643
- writeFileSync(join(dir, 'zeta', 'dev', 'default.md'), '# Z\n\n- [ ] step\n');
644
- writeFileSync(join(dir, 'alpha', 'dev', 'default.md'), '# A\n\n- [ ] step\n');
645
- const entries = listExecutionTemplates({
646
- sources: [
647
- customTemplateSource('zeta', join(dir, 'zeta')),
648
- customTemplateSource('alpha', join(dir, 'alpha')),
649
- ],
650
- });
651
- const winner = entries.find((e) => e.id === 'dev/default' && !e.shadowedBy);
652
- assert.equal(winner?.sourceId, 'custom:zeta');
653
- }
654
- finally {
655
- rmSync(dir, { recursive: true, force: true });
656
- }
657
- });
658
- test('create validates before writing: failure leaves no file and force never destroys', () => {
659
- const dir = mkdtempSync(join(tmpdir(), 'et-create-'));
660
- try {
661
- // Un-prefixed filename with explicit flow: rejected BEFORE write.
662
- assert.throws(() => createExecutionTemplate({ path: join(dir, 'evil.md'), flow: 'dev' }), /path must encode the flow/);
663
- assert.equal(existsSync(join(dir, 'evil.md')), false);
664
- // Contradicting --flow: rejected before write.
665
- assert.throws(() => createExecutionTemplate({ path: join(dir, 'dev.md'), flow: 'review-pr' }), /contradicts the path's flow/);
666
- assert.equal(existsSync(join(dir, 'dev.md')), false);
667
- // force on an invalid request must not destroy the existing file.
668
- const keep = join(dir, 'fix-bug.md');
669
- writeFileSync(keep, '# keep\n\n- [ ] precious\n');
670
- assert.throws(() => createExecutionTemplate({ path: keep, flow: 'dev', force: true }), /contradicts the path's flow/);
671
- assert.match(readFileSync(keep, 'utf8'), /precious/);
672
- }
673
- finally {
674
- rmSync(dir, { recursive: true, force: true });
675
- }
676
- });
677
- test('lint rejects traversal-like frontmatter ids and flow/filename contradictions', () => {
678
- const bad = lintExecutionTemplateText('/virtual/dev.md', '---\nid: ../../escape\nflow: review-pr\n---\n\n# T\n\n- [ ] step\n');
679
- assert.ok(bad.some((i) => /safe catalog id/.test(i.message)));
680
- assert.ok(bad.some((i) => /contradicts the path's flow/.test(i.message)));
681
- });
682
- test('only `checklist: none` exempts a template from the checkbox requirement', () => {
683
- const optedOut = lintExecutionTemplateText('/virtual/dev.md', '---\nchecklist: none\n---\n\n# Conversational template\n\nNo checklist by design.\n');
684
- assert.equal(optedOut.filter((i) => i.severity === 'error').length, 0);
685
- // An "interactive" filename is no longer an implicit waiver: the template
686
- // must say so itself.
687
- const implied = lintExecutionTemplateText('/virtual/dev-interactive.md', '# Conversational template\n\nNo checklist by design.\n');
688
- assert.ok(implied.some((i) => /no parseable checkbox/.test(i.message)));
689
- const plain = lintExecutionTemplateText('/virtual/dev.md', '# Missing checklist\n\nprose only\n');
690
- assert.ok(plain.some((i) => /no parseable checkbox/.test(i.message)));
691
- const badValue = lintExecutionTemplateText('/virtual/dev.md', '---\nchecklist: optional\n---\n\n# Bad opt-out\n\n- [ ] step\n');
692
- assert.ok(badValue.some((i) => /checklist must be 'none'/.test(i.message)));
693
- });
694
- test('a legacy runMode frontmatter key is ignored rather than rejected', () => {
695
- const issues = lintExecutionTemplateText('/virtual/dev.md', '---\nrunMode: nonsense\n---\n\n# Legacy\n\n- [ ] step\n');
696
- assert.deepEqual(issues, []);
697
- });
698
- test('lint flags unterminated frontmatter instead of silently treating it as body', () => {
699
- const issues = lintExecutionTemplateText('/virtual/dev.md', '---\nrunMode: autonomous\n\n# Heading\n\n- [ ] step\n');
700
- assert.ok(issues.some((i) => /unterminated frontmatter/.test(i.message)));
701
- });
702
- test('create with no metadata renders without a frontmatter block and round-trips through list', () => {
703
- const dir = mkdtempSync(join(tmpdir(), 'et-roundtrip-'));
704
- try {
705
- mkdirSync(join(dir, 'worker'), { recursive: true });
706
- // Bare title, default platforms — the generated file must still lint clean
707
- // and be discoverable.
708
- createExecutionTemplate({ path: join(dir, 'worker', 'fix-bug.core.md'), title: 'Round trip' });
709
- const text = readFileSync(join(dir, 'worker', 'fix-bug.core.md'), 'utf8');
710
- assert.ok(!text.startsWith('---'), 'no empty frontmatter fence pair');
711
- const entries = listExecutionTemplates({
712
- sources: [projectWorkerTemplateSource('p', dir)],
713
- });
714
- const entry = entries.find((e) => e.id === 'fix-bug/core');
715
- assert.equal(entry?.title, 'Round trip');
716
- }
717
- finally {
718
- rmSync(dir, { recursive: true, force: true });
719
- }
720
- });
721
- test('parser closes an empty frontmatter block', () => {
722
- const parsed = parseMarkdownDocument('---\n---\n\n# T\n\n- [ ] step\n');
723
- assert.notEqual(parsed.frontmatter, null);
724
- assert.equal(parsed.heading, 'T');
725
- });
726
- test('flow-tree paths create and lint via the parent flow directory', () => {
727
- const dir = mkdtempSync(join(tmpdir(), 'et-flowtree-'));
728
- try {
729
- mkdirSync(join(dir, 'fix-bug'), { recursive: true });
730
- // Basename has no flow prefix — the fix-bug/ directory carries it.
731
- createExecutionTemplate({ path: join(dir, 'fix-bug', 'core.md'), title: 'Tree base' });
732
- const issues = lintExecutionTemplates(join(dir, 'fix-bug', 'core.md'));
733
- assert.equal(issues.ok, true);
734
- const entries = listExecutionTemplates({
735
- sources: [packageFlowTreeTemplateSource('s', dir)],
736
- });
737
- assert.equal(entries.find((e) => e.id === 'fix-bug/core')?.title, 'Tree base');
738
- // A contradicting --flow still fails against the directory flow.
739
- assert.throws(() => createExecutionTemplate({ path: join(dir, 'fix-bug', 'other.md'), flow: 'dev' }), /contradicts the path's flow/);
740
- }
741
- finally {
742
- rmSync(dir, { recursive: true, force: true });
743
- }
744
- });
745
- test('parent flow directory wins over a contradicting basename, and lint flags it', () => {
746
- // fix-bug/dev.md must resolve as fix-bug everywhere (catalog parity)…
747
- assert.equal(catalogRelativeId('fix-bug/dev.md', 'flow-tree'), 'fix-bug/dev');
748
- const issues = lintExecutionTemplateText('/tmp/fix-bug/dev.md', '# T\n\n- [ ] step\n');
749
- // …and the misleading name is an authoring error.
750
- assert.ok(issues.some((i) => /contradicts the flow directory/.test(i.message)));
751
- });
752
- test('a line merely starting with --- is not a closing fence', () => {
753
- const parsed = parseMarkdownDocument('---\nrunMode: autonomous\n---not-a-fence\n\n# T\n');
754
- assert.equal(parsed.frontmatter, null);
755
- const issues = lintExecutionTemplateText('/virtual/dev.md', '---\nrunMode: autonomous\n---not-a-fence\n\n# T\n\n- [ ] step\n');
756
- assert.ok(issues.some((i) => /unterminated frontmatter/.test(i.message)));
757
- });
758
- test('directory lint flags files that canonicalize to the same catalog id', () => {
759
- const dir = mkdtempSync(join(tmpdir(), 'et-dup-'));
760
- try {
761
- writeFileSync(join(dir, 'dev-interactive.md'), '# A\n');
762
- writeFileSync(join(dir, 'dev.interactive.md'), '# B\n');
763
- const result = lintExecutionTemplates(dir);
764
- assert.equal(result.ok, false);
765
- assert.ok(result.issues.some((i) => /duplicate catalog id 'dev\/interactive'/.test(i.message)));
766
- }
767
- finally {
768
- rmSync(dir, { recursive: true, force: true });
769
- }
770
- });
771
- test('duplicate detection follows explicit frontmatter ids like the resolver', () => {
772
- const dir = mkdtempSync(join(tmpdir(), 'et-dup2-'));
773
- try {
774
- // Same explicit id in differently-named files: a real collision.
775
- writeFileSync(join(dir, 'dev.md'), '---\nid: shared\n---\n\n# A\n\n- [ ] s\n');
776
- writeFileSync(join(dir, 'fix-bug.md'), '---\nid: shared\n---\n\n# B\n\n- [ ] s\n');
777
- const collision = lintExecutionTemplates(dir);
778
- assert.ok(collision.issues.some((i) => /duplicate catalog id 'shared'/.test(i.message)));
779
- rmSync(join(dir, 'dev.md'));
780
- rmSync(join(dir, 'fix-bug.md'));
781
- // Path-colliding names with DIFFERENT explicit ids: not a collision.
782
- writeFileSync(join(dir, 'dev-interactive.md'), '---\nid: one\n---\n\n# A\n');
783
- writeFileSync(join(dir, 'dev.interactive.md'), '---\nid: two\n---\n\n# B\n');
784
- const distinct = lintExecutionTemplates(dir);
785
- assert.ok(!distinct.issues.some((i) => /duplicate catalog id/.test(i.message)));
786
- }
787
- finally {
788
- rmSync(dir, { recursive: true, force: true });
789
- }
790
- });
791
- test('a bare CR does not terminate a frontmatter fence', () => {
792
- const parsed = parseMarkdownDocument('---\nrunMode: autonomous\n---\rnot-a-fence\n\n# T\n');
793
- assert.equal(parsed.frontmatter, null);
794
- });
795
- //# sourceMappingURL=execution-template.test.js.map