agentic-workflow-manager 9.19.0 → 9.20.1
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/dist/src/commands/change/index.js +492 -0
- package/dist/src/commands/changes/index.js +84 -0
- package/dist/src/commands/sensors/index.js +9 -0
- package/dist/src/commands/sensors/run.js +1 -0
- package/dist/src/core/changes/command.js +38 -0
- package/dist/src/core/changes/derive.js +120 -0
- package/dist/src/core/changes/fs-safe.js +47 -0
- package/dist/src/core/changes/git-env.js +68 -0
- package/dist/src/core/changes/git.js +216 -0
- package/dist/src/core/changes/list-parse.js +67 -0
- package/dist/src/core/changes/list.js +408 -0
- package/dist/src/core/changes/parse.js +72 -0
- package/dist/src/core/changes/range.js +53 -0
- package/dist/src/core/changes/receipt.js +274 -0
- package/dist/src/core/changes/record-io.js +149 -0
- package/dist/src/core/changes/render-list.js +109 -0
- package/dist/src/core/changes/render.js +178 -0
- package/dist/src/core/changes/text-safety.js +116 -0
- package/dist/src/core/changes/trailers.js +76 -0
- package/dist/src/core/changes/types.js +26 -0
- package/dist/src/core/changes/validate-shape.js +219 -0
- package/dist/src/core/changes/worktree.js +53 -0
- package/dist/src/core/process/model.js +153 -10
- package/dist/src/index.js +4 -0
- package/dist/src/release/orchestrator.js +3 -1
- package/dist/tests/commands/change/new.test.js +1126 -0
- package/dist/tests/commands/change/validate.test.js +1145 -0
- package/dist/tests/commands/changes/list.test.js +423 -0
- package/dist/tests/commands/process.test.js +4 -0
- package/dist/tests/commands/sensors/index.test.js +1 -1
- package/dist/tests/core/changes/derive.test.js +455 -0
- package/dist/tests/core/changes/fs-safe.test.js +82 -0
- package/dist/tests/core/changes/git-env.test.js +143 -0
- package/dist/tests/core/changes/git.test.js +442 -0
- package/dist/tests/core/changes/helpers.js +269 -0
- package/dist/tests/core/changes/list-fixtures.js +122 -0
- package/dist/tests/core/changes/list-long-history.test.js +52 -0
- package/dist/tests/core/changes/list-paging.test.js +511 -0
- package/dist/tests/core/changes/list-parse.test.js +120 -0
- package/dist/tests/core/changes/list-tree.test.js +44 -0
- package/dist/tests/core/changes/list.test.js +821 -0
- package/dist/tests/core/changes/parse.test.js +142 -0
- package/dist/tests/core/changes/predicates.test.js +83 -0
- package/dist/tests/core/changes/range.test.js +92 -0
- package/dist/tests/core/changes/receipt.test.js +947 -0
- package/dist/tests/core/changes/record-io.test.js +307 -0
- package/dist/tests/core/changes/record.test.js +549 -0
- package/dist/tests/core/changes/render-list.test.js +212 -0
- package/dist/tests/core/changes/render.test.js +202 -0
- package/dist/tests/core/changes/text-safety.test.js +226 -0
- package/dist/tests/core/changes/trailers.test.js +181 -0
- package/dist/tests/core/changes/worktree.test.js +259 -0
- package/dist/tests/core/dashboard/write-html.test.js +2 -3
- package/dist/tests/core/evidence/store.test.js +2 -3
- package/dist/tests/core/ledger/store.test.js +3 -9
- package/dist/tests/core/process/discover.test.js +21 -0
- package/dist/tests/core/process/model.test.js +338 -0
- package/dist/tests/helpers/platform.js +27 -0
- package/dist/tests/release/orchestrator-trailer.test.js +72 -0
- package/dist/tests/release/orchestrator.test.js +1 -0
- package/dist/tests/structural/change-commands-are-registered.test.js +29 -0
- package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +78 -0
- package/dist/tests/structural/process-model-description-contract.test.js +34 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
3
7
|
const model_1 = require("../../../src/core/process/model");
|
|
4
8
|
const valid = `---
|
|
5
9
|
awm: process-model
|
|
@@ -80,4 +84,338 @@ describe('parseProcessFrontmatter', () => {
|
|
|
80
84
|
expect(r.model).toBeUndefined();
|
|
81
85
|
expect(r.diagnostics.join(' ')).toMatch(new RegExp(`"${field}" is required`));
|
|
82
86
|
});
|
|
87
|
+
describe('description (optional)', () => {
|
|
88
|
+
const withDescription = (line) => valid.replace('created:', `${line}\ncreated:`);
|
|
89
|
+
it('is optional: a model without it parses unchanged and has no description key', () => {
|
|
90
|
+
const r = (0, model_1.parseProcessFrontmatter)(valid, 'p');
|
|
91
|
+
expect(r.diagnostics).toEqual([]);
|
|
92
|
+
expect(r.model).toBeDefined();
|
|
93
|
+
expect(Object.prototype.hasOwnProperty.call(r.model, 'description')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
it.each([
|
|
96
|
+
['plain', 'description: Use when tracing a work session', 'Use when tracing a work session'],
|
|
97
|
+
['double-quoted', 'description: "Use when: tracing \\"sessions\\""', 'Use when: tracing "sessions"'],
|
|
98
|
+
['single-quoted', "description: 'It''s the entry point'", "It's the entry point"],
|
|
99
|
+
])('accepts a %s single-line value and exposes it resolved', (_kind, line, expected) => {
|
|
100
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(line), 'p');
|
|
101
|
+
expect(r.diagnostics).toEqual([]);
|
|
102
|
+
expect(r.model?.description).toBe(expected);
|
|
103
|
+
});
|
|
104
|
+
it('accepts exactly 1024 characters and rejects 1025', () => {
|
|
105
|
+
expect((0, model_1.parseProcessFrontmatter)(withDescription(`description: ${'a'.repeat(1024)}`), 'p').model?.description).toHaveLength(1024);
|
|
106
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${'a'.repeat(1025)}`), 'p');
|
|
107
|
+
expect(r.model).toBeUndefined();
|
|
108
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must be at most 1024 characters/);
|
|
109
|
+
});
|
|
110
|
+
it.each(['description:', 'description: ""', "description: ''", 'description: " "'])('rejects an empty value: %s', (line) => {
|
|
111
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(line), 'p');
|
|
112
|
+
expect(r.model).toBeUndefined();
|
|
113
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must not be empty/);
|
|
114
|
+
});
|
|
115
|
+
it.each(['description: >', 'description: >-', 'description: |', 'description: |2+', 'description: >-basura'])('rejects a block scalar header: %s', (line) => {
|
|
116
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`${line}\n folded text`), 'p');
|
|
117
|
+
expect(r.model).toBeUndefined();
|
|
118
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must be a single-line string \(block scalars are not supported in a process model\)/);
|
|
119
|
+
});
|
|
120
|
+
it('still rejects every other unknown field next to a valid description', () => {
|
|
121
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: ok\nsummary: nope'), 'p');
|
|
122
|
+
expect(r.model).toBeUndefined();
|
|
123
|
+
expect(r.diagnostics.join(' ')).toMatch(/unknown field "summary"/);
|
|
124
|
+
expect(r.diagnostics.join(' ')).not.toMatch(/unknown field "description"/);
|
|
125
|
+
});
|
|
126
|
+
it('rejects a duplicated description', () => {
|
|
127
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: a\ndescription: b'), 'p');
|
|
128
|
+
expect(r.model).toBeUndefined();
|
|
129
|
+
expect(r.diagnostics.join(' ')).toMatch(/duplicate field "description"/);
|
|
130
|
+
});
|
|
131
|
+
it.each([
|
|
132
|
+
['a block scalar', 'description: >-\n SECRETVALUE here'],
|
|
133
|
+
['a plain multi-line value', 'description: first\n SECRETVALUE second'],
|
|
134
|
+
['a quote closed on a later line', 'description: "hola\n SECRETVALUE mundo"'],
|
|
135
|
+
])('reports %s by line position and never echoes its content', (_kind, lines) => {
|
|
136
|
+
// Bloque del fixture: awm(1) … terminates_to(6); la línea description
|
|
137
|
+
// queda en 7 y su continuación en 8.
|
|
138
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(lines), 'p');
|
|
139
|
+
expect(r.model).toBeUndefined();
|
|
140
|
+
for (const d of r.diagnostics)
|
|
141
|
+
expect(d).not.toContain('SECRETVALUE');
|
|
142
|
+
expect(r.diagnostics.join(' ')).toContain('frontmatter line 8 is not a "key: value" pair (multi-line values are not supported)');
|
|
143
|
+
});
|
|
144
|
+
it.each([
|
|
145
|
+
['a plain value', 'description: Use for\n SECRETKEY: when X', false],
|
|
146
|
+
['a block scalar', 'description: >-\n SECRETKEY: x', true],
|
|
147
|
+
])('treats an indented "KEY: x" after %s as a continuation, never as a field', (_kind, lines, isBlock) => {
|
|
148
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(lines), 'p');
|
|
149
|
+
expect(r.model).toBeUndefined();
|
|
150
|
+
for (const d of r.diagnostics)
|
|
151
|
+
expect(d).not.toContain('SECRETKEY');
|
|
152
|
+
expect(r.diagnostics.join(' ')).toContain('frontmatter line 8 is not a "key: value" pair (multi-line values are not supported)');
|
|
153
|
+
if (isBlock) {
|
|
154
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must be a single-line string \(block scalars are not supported in a process model\)/);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
const PARSE_ERROR = /"description" cannot be read: the frontmatter is not valid YAML/;
|
|
158
|
+
const NOT_A_STRING = /"description" must be a string \(quote values YAML reads as a boolean, a number, a date or a collection\)/;
|
|
159
|
+
it.each([
|
|
160
|
+
'- foo', '? foo', 'a:\tb', 'Use when: tracing', '"hola', "'hola", "'hola # x", '"hola # x', '"a" junk',
|
|
161
|
+
'ends with colon:', '@x', '\x1b',
|
|
162
|
+
])('rejects a value YAML cannot parse as a single-line string: %j', (value) => {
|
|
163
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
164
|
+
expect(r.model).toBeUndefined();
|
|
165
|
+
expect(r.diagnostics.join(' ')).toMatch(PARSE_ERROR);
|
|
166
|
+
});
|
|
167
|
+
it.each(['true', '123', '2026-01-01', '[a, b]'])('rejects a value YAML reads as a non-string: %s', (value) => {
|
|
168
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
169
|
+
expect(r.model).toBeUndefined();
|
|
170
|
+
expect(r.diagnostics.join(' ')).toMatch(NOT_A_STRING);
|
|
171
|
+
});
|
|
172
|
+
it.each(['null', '~', '# x'])('rejects a value YAML reads as null as empty: %s', (value) => {
|
|
173
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
174
|
+
expect(r.model).toBeUndefined();
|
|
175
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must not be empty/);
|
|
176
|
+
});
|
|
177
|
+
it('never echoes the value or the YAML error text into a diagnostic', () => {
|
|
178
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: SECRETVALUE: x'), 'p');
|
|
179
|
+
expect(r.diagnostics.join(' ')).toMatch(PARSE_ERROR);
|
|
180
|
+
for (const d of r.diagnostics)
|
|
181
|
+
expect(d).not.toContain('SECRETVALUE');
|
|
182
|
+
});
|
|
183
|
+
it('resolves YAML double-quoted escapes', () => {
|
|
184
|
+
expect((0, model_1.parseProcessFrontmatter)(withDescription('description: "\\x41"'), 'p').model?.description).toBe('A');
|
|
185
|
+
});
|
|
186
|
+
it('stores the YAML string untrimmed', () => {
|
|
187
|
+
expect((0, model_1.parseProcessFrontmatter)(withDescription('description: " padded "'), 'p').model?.description).toBe(' padded ');
|
|
188
|
+
});
|
|
189
|
+
it.each([
|
|
190
|
+
['description: "Use when: tracing"', 'Use when: tracing'],
|
|
191
|
+
["description: 'It''s'", "It's"],
|
|
192
|
+
])('accepts the quoted form %s', (line, expected) => {
|
|
193
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(line), 'p');
|
|
194
|
+
expect(r.diagnostics).toEqual([]);
|
|
195
|
+
expect(r.model?.description).toBe(expected);
|
|
196
|
+
});
|
|
197
|
+
const blockOf = (source) => source.slice(4, source.indexOf('\n---', 4));
|
|
198
|
+
/** Modelo válido con description `ok` cuyo bloque de frontmatter mide exactamente `target`. */
|
|
199
|
+
const withBlockLength = (target) => {
|
|
200
|
+
const base = withDescription('description: ok');
|
|
201
|
+
const pad = target - blockOf(base).length - 3;
|
|
202
|
+
const source = base.replace('\ncreated:', `\n# ${'x'.repeat(pad)}\ncreated:`);
|
|
203
|
+
expect(blockOf(source)).toHaveLength(target);
|
|
204
|
+
return source;
|
|
205
|
+
};
|
|
206
|
+
it('parses a frontmatter block of exactly 65536 characters', () => {
|
|
207
|
+
const r = (0, model_1.parseProcessFrontmatter)(withBlockLength(65536), 'p');
|
|
208
|
+
expect(r.diagnostics).toEqual([]);
|
|
209
|
+
expect(r.model?.description).toBe('ok');
|
|
210
|
+
});
|
|
211
|
+
it('rejects a frontmatter block over 65536 characters with the 1024 message without parsing it', () => {
|
|
212
|
+
const source = withBlockLength(65537);
|
|
213
|
+
const load = jest.spyOn(js_yaml_1.default, 'load');
|
|
214
|
+
try {
|
|
215
|
+
const r = (0, model_1.parseProcessFrontmatter)(source, 'p');
|
|
216
|
+
expect(r.model).toBeUndefined();
|
|
217
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must be at most 1024 characters/);
|
|
218
|
+
expect(load).not.toHaveBeenCalled();
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
load.mockRestore();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
it('accepts a short value followed by a 20000-character comment', () => {
|
|
225
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ok # ${'x'.repeat(20000)}`), 'p');
|
|
226
|
+
expect(r.diagnostics).toEqual([]);
|
|
227
|
+
expect(r.model?.description).toBe('ok');
|
|
228
|
+
});
|
|
229
|
+
it('accepts an escape-heavy value that resolves to 1024 code points', () => {
|
|
230
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: "${'\\x41'.repeat(1024)}"`), 'p');
|
|
231
|
+
expect(r.diagnostics).toEqual([]);
|
|
232
|
+
expect(r.model?.description).toBe('A'.repeat(1024));
|
|
233
|
+
});
|
|
234
|
+
it('preserves NBSP, U+3000 and BOM exactly as YAML reads them', () => {
|
|
235
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: \u00A0hello\u3000world\uFEFF'), 'p');
|
|
236
|
+
expect(r.diagnostics).toEqual([]);
|
|
237
|
+
expect(r.model?.description).toBe('\u00A0hello\u3000world\uFEFF');
|
|
238
|
+
});
|
|
239
|
+
it('rejects description:value with no space after the colon', () => {
|
|
240
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description:value'), 'p');
|
|
241
|
+
expect(r.model).toBeUndefined();
|
|
242
|
+
expect(r.diagnostics.join(' ')).toMatch(PARSE_ERROR);
|
|
243
|
+
});
|
|
244
|
+
const CONTROL = /"description" must not contain control, line-break or bidirectional-override characters/;
|
|
245
|
+
it.each([
|
|
246
|
+
'"a\\nb"', '"a\\rb"', '"\\0"', '"\\e[31m"', '"\\x7f"', '"\\x85"', '"\\x9f"',
|
|
247
|
+
'"a\\u2028b"', '"a\\u2029b"', '"\\u202A"', '"\\u202E"', '"\\u2066"', '"\\u2069"',
|
|
248
|
+
'"\\uD800"', '"\\uDC00"', 'a\u2028b', 'a\u2029b', 'a\u202Eb', 'a\u0085b',
|
|
249
|
+
])('rejects control, line-break, bidi or lone-surrogate content: %j', (value) => {
|
|
250
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
251
|
+
expect(r.model).toBeUndefined();
|
|
252
|
+
expect(r.diagnostics.join(' ')).toMatch(CONTROL);
|
|
253
|
+
});
|
|
254
|
+
it.each([
|
|
255
|
+
['an escaped tab', '"a\\tb"', 'a\tb'],
|
|
256
|
+
['a surrogate pair', '"\\uD83D\\uDE00"', '\u{1F600}'],
|
|
257
|
+
])('accepts %s', (_kind, value, expected) => {
|
|
258
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
259
|
+
expect(r.diagnostics).toEqual([]);
|
|
260
|
+
expect(r.model?.description).toBe(expected);
|
|
261
|
+
});
|
|
262
|
+
it.each(['"\\u200B"', '"\\u200B\\u200C\\u200D"', '"\\u2060\\uFEFF "'])('rejects a value made only of zero-width characters as empty: %j', (value) => {
|
|
263
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
264
|
+
expect(r.model).toBeUndefined();
|
|
265
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must not be empty/);
|
|
266
|
+
});
|
|
267
|
+
it.each(['"a\\bb"', '"a\\x1fb"', '"a\\u200Eb"', '"a\\u200Fb"', '"a\\u061Cb"'])('rejects backspace, US and LRM/RLM/ALM: %j', (value) => {
|
|
268
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${value}`), 'p');
|
|
269
|
+
expect(r.model).toBeUndefined();
|
|
270
|
+
expect(r.diagnostics.join(' ')).toMatch(CONTROL);
|
|
271
|
+
});
|
|
272
|
+
const DIVERGES = /"description" cannot be read: the frontmatter does not read the same as YAML \(check quotes and line breaks\)/;
|
|
273
|
+
it('rejects a bare CR that smuggles a second top-level key', () => {
|
|
274
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: hello\rallowed-tools: x'), 'p');
|
|
275
|
+
expect(r.model).toBeUndefined();
|
|
276
|
+
expect(r.diagnostics.join(' ')).toMatch(DIVERGES);
|
|
277
|
+
for (const d of r.diagnostics)
|
|
278
|
+
expect(d).not.toContain('allowed-tools');
|
|
279
|
+
});
|
|
280
|
+
it('rejects an unclosed quote that YAML folds into the following name line', () => {
|
|
281
|
+
const r = (0, model_1.parseProcessFrontmatter)(valid.replace('name: ejemplo-proceso', 'description: "hola\nname: ejemplo-proceso"'), 'p');
|
|
282
|
+
expect(r.model).toBeUndefined();
|
|
283
|
+
expect(r.diagnostics.join(' ')).toMatch(DIVERGES);
|
|
284
|
+
});
|
|
285
|
+
it('validates a description that follows a comment on a bare CR', () => {
|
|
286
|
+
const bad = (0, model_1.parseProcessFrontmatter)(withDescription('#\rdescription: 123'), 'p');
|
|
287
|
+
expect(bad.model).toBeUndefined();
|
|
288
|
+
expect(bad.diagnostics.join(' ')).toMatch(NOT_A_STRING);
|
|
289
|
+
const ok = (0, model_1.parseProcessFrontmatter)(withDescription('#\rdescription: ok'), 'p');
|
|
290
|
+
expect(ok.diagnostics).toEqual([]);
|
|
291
|
+
expect(ok.model?.description).toBe('ok');
|
|
292
|
+
});
|
|
293
|
+
it('reads NBSP followed by > as a string, not as a block-scalar header', () => {
|
|
294
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: \u00A0>'), 'p');
|
|
295
|
+
expect(r.diagnostics).toEqual([]);
|
|
296
|
+
expect(r.model?.description).toBe('\u00A0>');
|
|
297
|
+
});
|
|
298
|
+
it.each([['NEL', '\u0085'], ['LS', '\u2028'], ['PS', '\u2029']])('rejects a %s inside a comment line by position', (_kind, ch) => {
|
|
299
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(`# SECRET${ch}name: smuggled`), 'p');
|
|
300
|
+
expect(r.model).toBeUndefined();
|
|
301
|
+
expect(r.diagnostics.join(' ')).toContain('frontmatter line 7 contains a YAML 1.1 line break (NEL, LS or PS)');
|
|
302
|
+
for (const d of r.diagnostics)
|
|
303
|
+
expect(d).not.toContain('SECRET');
|
|
304
|
+
});
|
|
305
|
+
it.each([['NEL', '\u0085'], ['LS', '\u2028'], ['PS', '\u2029']])('rejects a %s inside a value line of any field by position', (_kind, ch) => {
|
|
306
|
+
const inDescription = (0, model_1.parseProcessFrontmatter)(withDescription(`description: a${ch}b`), 'p');
|
|
307
|
+
expect(inDescription.model).toBeUndefined();
|
|
308
|
+
expect(inDescription.diagnostics.join(' ')).toContain('frontmatter line 7 contains a YAML 1.1 line break (NEL, LS or PS)');
|
|
309
|
+
const inName = (0, model_1.parseProcessFrontmatter)(valid.replace('name: ejemplo-proceso', `name: ejemplo${ch}proceso`), 'p');
|
|
310
|
+
expect(inName.model).toBeUndefined();
|
|
311
|
+
expect(inName.diagnostics.join(' ')).toContain('frontmatter line 3 contains a YAML 1.1 line break (NEL, LS or PS)');
|
|
312
|
+
});
|
|
313
|
+
it.each(['description: "foo\n# bar"', "description: 'foo\n# bar'"])('rejects a quoted value that YAML folds across a following comment line: %j', (lines) => {
|
|
314
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(lines), 'p');
|
|
315
|
+
expect(r.model).toBeUndefined();
|
|
316
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must be a single-line string \(block scalars are not supported in a process model\)/);
|
|
317
|
+
for (const d of r.diagnostics)
|
|
318
|
+
expect(d).not.toContain('bar');
|
|
319
|
+
});
|
|
320
|
+
it.each([
|
|
321
|
+
['description: "Use when: tracing a work session"', 'Use when: tracing a work session'],
|
|
322
|
+
['description: Use when tracing a work session # nota', 'Use when tracing a work session'],
|
|
323
|
+
["description: 'It''s the entry point' # nota", "It's the entry point"],
|
|
324
|
+
['description: "Traza la sesión \\u2014 sin perder pasos"\n# comentario aparte', 'Traza la sesión \u2014 sin perder pasos'],
|
|
325
|
+
])('keeps accepting a realistic description: %j', (lines, expected) => {
|
|
326
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription(lines), 'p');
|
|
327
|
+
expect(r.diagnostics).toEqual([]);
|
|
328
|
+
expect(r.model?.description).toBe(expected);
|
|
329
|
+
});
|
|
330
|
+
it('lists description in the unknown-field message', () => {
|
|
331
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('summary: nope'), 'p');
|
|
332
|
+
expect(r.diagnostics.join(' ')).toMatch(/the contract admits only [^;]*\bdescription\b/);
|
|
333
|
+
});
|
|
334
|
+
it('accepts a closed quote followed by a comment', () => {
|
|
335
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: "a" # note'), 'p');
|
|
336
|
+
expect(r.diagnostics).toEqual([]);
|
|
337
|
+
expect(r.model?.description).toBe('a');
|
|
338
|
+
});
|
|
339
|
+
it('rejects a comment-only value as empty and never echoes it', () => {
|
|
340
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: # x'), 'p');
|
|
341
|
+
expect(r.model).toBeUndefined();
|
|
342
|
+
expect(r.diagnostics.join(' ')).toMatch(/"description" must not be empty/);
|
|
343
|
+
for (const d of r.diagnostics)
|
|
344
|
+
expect(d).not.toContain('# x');
|
|
345
|
+
});
|
|
346
|
+
it('counts the 1024 limit in code points, not UTF-16 units', () => {
|
|
347
|
+
const ok = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${'😀'.repeat(1024)}`), 'p');
|
|
348
|
+
expect(ok.diagnostics).toEqual([]);
|
|
349
|
+
expect([...(ok.model?.description ?? '')]).toHaveLength(1024);
|
|
350
|
+
const tooLong = (0, model_1.parseProcessFrontmatter)(withDescription(`description: ${'😀'.repeat(1025)}`), 'p');
|
|
351
|
+
expect(tooLong.model).toBeUndefined();
|
|
352
|
+
expect(tooLong.diagnostics.join(' ')).toMatch(/"description" must be at most 1024 characters/);
|
|
353
|
+
});
|
|
354
|
+
it('keeps skipping an indented comment line after a valid description', () => {
|
|
355
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: ok\n # comentario'), 'p');
|
|
356
|
+
expect(r.diagnostics).toEqual([]);
|
|
357
|
+
expect(r.model?.description).toBe('ok');
|
|
358
|
+
});
|
|
359
|
+
it('accepts a plain value followed by a comment, without the comment', () => {
|
|
360
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: plain text # note'), 'p');
|
|
361
|
+
expect(r.diagnostics).toEqual([]);
|
|
362
|
+
expect(r.model?.description).toBe('plain text');
|
|
363
|
+
});
|
|
364
|
+
it('accepts escaped quotes inside a closed double-quoted value', () => {
|
|
365
|
+
const r = (0, model_1.parseProcessFrontmatter)(withDescription('description: "a \\"b\\""'), 'p');
|
|
366
|
+
expect(r.diagnostics).toEqual([]);
|
|
367
|
+
expect(r.model?.description).toBe('a "b"');
|
|
368
|
+
});
|
|
369
|
+
it('never throws on hostile description values', () => {
|
|
370
|
+
for (const line of ['description: "unclosed', "description: '''", 'description: \x00', `description: ${'"'.repeat(5000)}`]) {
|
|
371
|
+
expect(() => (0, model_1.parseProcessFrontmatter)(withDescription(line), 'p')).not.toThrow();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
describe('indented frontmatter keys', () => {
|
|
376
|
+
const indentAll = (source) => source.replace(/^---\n([\s\S]*?)\n---/, (_m, block) => `---\n${block.split('\n').map((l) => ` ${l}`).join('\n')}\n---`);
|
|
377
|
+
it('reports a model whose keys are all indented instead of ignoring it', () => {
|
|
378
|
+
const r = (0, model_1.parseProcessFrontmatter)(indentAll(valid), 'p');
|
|
379
|
+
expect(r.model).toBeUndefined();
|
|
380
|
+
expect(r.diagnostics).toEqual(['p: invalid process model — frontmatter keys must not be indented (frontmatter line 1)']);
|
|
381
|
+
});
|
|
382
|
+
it('allows whitespace around the colon and counts comment lines', () => {
|
|
383
|
+
const r = (0, model_1.parseProcessFrontmatter)('---\n# nota\n awm : process-model\n name: x\n---\n', 'p');
|
|
384
|
+
expect(r.diagnostics).toEqual(['p: invalid process model — frontmatter keys must not be indented (frontmatter line 2)']);
|
|
385
|
+
});
|
|
386
|
+
it.each([
|
|
387
|
+
['another indented value', '---\nname: x\nmetadata:\n awm: other\n---\n'],
|
|
388
|
+
['a mention nested under metadata', '---\nname: x\nmetadata:\n awm: process-model\n---\n'],
|
|
389
|
+
['a mention inside a block-scalar description', '---\nname: x\ndescription: >\n awm: process-model\n---\n'],
|
|
390
|
+
['a near-miss discriminator', '---\n awm: process-model-x\n name: x\n---\n'],
|
|
391
|
+
])('stays silent for a non-model with %s', (_kind, source) => {
|
|
392
|
+
expect((0, model_1.parseProcessFrontmatter)(source, 'p')).toEqual({ diagnostics: [] });
|
|
393
|
+
});
|
|
394
|
+
it.each([
|
|
395
|
+
['only the awm line indented', valid.replace('awm: process-model', ' awm: process-model')],
|
|
396
|
+
['a tab-indented awm line', valid.replace('awm: process-model', '\tawm: process-model')],
|
|
397
|
+
])('reports %s even though YAML cannot load the block', (_kind, source) => {
|
|
398
|
+
expect((0, model_1.parseProcessFrontmatter)(source, 'p')).toEqual({
|
|
399
|
+
diagnostics: ['p: invalid process model — frontmatter keys must not be indented (frontmatter line 1)'],
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
it('gives no indented diagnostic to a top-level-awm model with a nested metadata.awm', () => {
|
|
403
|
+
const r = (0, model_1.parseProcessFrontmatter)(valid.replace('created:', 'metadata:\n awm: process-model\ncreated:'), 'p');
|
|
404
|
+
expect(r.model).toBeUndefined();
|
|
405
|
+
expect(r.diagnostics.join(' ')).not.toMatch(/must not be indented/);
|
|
406
|
+
});
|
|
407
|
+
it('reports an indented discriminator in a block over 65536 characters without loading it', () => {
|
|
408
|
+
const source = indentAll(valid.replace('\ncreated:', `\n# ${'x'.repeat(65536)}\ncreated:`));
|
|
409
|
+
const load = jest.spyOn(js_yaml_1.default, 'load');
|
|
410
|
+
try {
|
|
411
|
+
expect((0, model_1.parseProcessFrontmatter)(source, 'p')).toEqual({
|
|
412
|
+
diagnostics: ['p: invalid process model — frontmatter keys must not be indented (frontmatter line 1)'],
|
|
413
|
+
});
|
|
414
|
+
expect(load).not.toHaveBeenCalled();
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
load.mockRestore();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
83
421
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.symlinkTest = exports.canCreateSymlinks = exports.posixOnly = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
/** `test` where symlinks and POSIX modes exist; reported as skipped (not as passed) on Windows. */
|
|
11
|
+
exports.posixOnly = process.platform === 'win32' ? test.skip : test;
|
|
12
|
+
/** True when this process can create a symlink (Windows needs the privilege or developer mode). */
|
|
13
|
+
exports.canCreateSymlinks = (() => {
|
|
14
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-symlink-probe-'));
|
|
15
|
+
try {
|
|
16
|
+
fs_1.default.symlinkSync(path_1.default.join(dir, 'missing-target'), path_1.default.join(dir, 'link'));
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
/** `test` that needs to create symlinks; reported as skipped (not as passed) where it cannot. */
|
|
27
|
+
exports.symlinkTest = exports.canCreateSymlinks ? test : test.skip;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// cli/tests/release/orchestrator-trailer.test.ts
|
|
7
|
+
//
|
|
8
|
+
// R6.1 (#217): the bump commit must be read as a Change Record opt-out by the code that reads it
|
|
9
|
+
// (`hasOptOut`), not just contain the right text in a mocked argv. The orchestrator runs against a
|
|
10
|
+
// real tmp git repo: `git add` and `git commit` execute for real with the argv the orchestrator
|
|
11
|
+
// passes (never retyped here); every other command is faked.
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const child_process_1 = require("child_process");
|
|
15
|
+
const orchestrator_1 = require("../../src/release/orchestrator");
|
|
16
|
+
const core_1 = require("../../src/release/core");
|
|
17
|
+
const trailers_1 = require("../../src/core/changes/trailers");
|
|
18
|
+
const helpers_1 = require("../core/changes/helpers");
|
|
19
|
+
describe('release — the bump commit opts out of the Change Record (R6.1)', () => {
|
|
20
|
+
let repo;
|
|
21
|
+
beforeEach(() => { repo = (0, helpers_1.makeRepo)(); });
|
|
22
|
+
afterEach(() => repo.cleanup());
|
|
23
|
+
it('the commit the orchestrator creates is read as an opt-out and keeps its subject byte-identical', () => {
|
|
24
|
+
// Explicit environment for the child git: identity and config isolation never come from process.env alone.
|
|
25
|
+
const gitEnv = {
|
|
26
|
+
...process.env, HOME: repo.home, GIT_CONFIG_GLOBAL: path_1.default.join(repo.home, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1',
|
|
27
|
+
GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t',
|
|
28
|
+
};
|
|
29
|
+
repo.commit({
|
|
30
|
+
'cli/package.json': '{"version":"2.1.1"}\n',
|
|
31
|
+
'cli/package-lock.json': '{"version":"2.1.1"}\n',
|
|
32
|
+
'CHANGELOG.md': '# Changelog\n',
|
|
33
|
+
}, 'chore: seed release files');
|
|
34
|
+
repo.git('config', 'commit.gpgsign', 'false');
|
|
35
|
+
expect((0, trailers_1.hasOptOut)(repo.root, ['-1', 'HEAD'])).toBe(false);
|
|
36
|
+
let pkgVersion = '2.1.1';
|
|
37
|
+
let changelog = '# Changelog\n';
|
|
38
|
+
const write = (rel, content) => fs_1.default.writeFileSync(path_1.default.join(repo.root, rel), content);
|
|
39
|
+
const commitArgv = [];
|
|
40
|
+
const io = {
|
|
41
|
+
run(cmd, args) {
|
|
42
|
+
if (cmd === 'git' && (args[0] === 'add' || args[0] === 'commit')) {
|
|
43
|
+
if (args[0] === 'commit')
|
|
44
|
+
commitArgv.push(args);
|
|
45
|
+
return (0, child_process_1.execFileSync)('git', args, { cwd: repo.root, encoding: 'utf8', env: gitEnv });
|
|
46
|
+
}
|
|
47
|
+
if (cmd === 'git' && args[0] === 'rev-parse')
|
|
48
|
+
return 'main';
|
|
49
|
+
if (cmd === 'git' && args[0] === 'log')
|
|
50
|
+
return `feat: nueva${core_1.US}${core_1.RS}`;
|
|
51
|
+
return ''; // status, tag, npm view/publish: nothing there, nothing dirty
|
|
52
|
+
},
|
|
53
|
+
readPackageVersion: () => pkgVersion,
|
|
54
|
+
writePackageVersion: (v) => { pkgVersion = v; write('cli/package.json', `{"version":"${v}"}\n`); },
|
|
55
|
+
writeLockVersion: (v) => write('cli/package-lock.json', `{"version":"${v}"}\n`),
|
|
56
|
+
readChangelog: () => changelog,
|
|
57
|
+
writeChangelog: (c) => { changelog = c; write('CHANGELOG.md', c); },
|
|
58
|
+
writeNpmrc: () => { },
|
|
59
|
+
removeNpmrc: () => { },
|
|
60
|
+
today: () => '2026-06-25',
|
|
61
|
+
log: () => { },
|
|
62
|
+
env: { NPM_TOKEN: 'tok' },
|
|
63
|
+
};
|
|
64
|
+
const res = (0, orchestrator_1.release)({ dryRun: false, force: null, push: false, branch: 'main', cliDir: '/cli' }, io);
|
|
65
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
66
|
+
expect(commitArgv).toHaveLength(1); // the argv under test was really executed, once
|
|
67
|
+
expect(repo.git('log', '-1', '--format=%s')).toBe('chore(release): v2.2.0 [skip ci]');
|
|
68
|
+
expect((0, trailers_1.hasOptOut)(repo.root, ['-1', 'HEAD'])).toBe(true);
|
|
69
|
+
// The opt-out is a trailer paragraph of its own: the full message is subject, blank line, trailer.
|
|
70
|
+
expect(repo.git('log', '-1', '--format=%B')).toBe('chore(release): v2.2.0 [skip ci]\n\nChange-Record: none');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -51,6 +51,7 @@ describe('release — happy path', () => {
|
|
|
51
51
|
expect(calls).toContain('WRITE_PKG 2.2.0');
|
|
52
52
|
const joined = calls.join('\n');
|
|
53
53
|
expect(joined).toMatch(/git commit .*chore\(release\): v2.2.0/);
|
|
54
|
+
expect(joined).toMatch(/git commit .*chore\(release\): v2\.2\.0 \[skip ci\] -m Change-Record: none/);
|
|
54
55
|
expect(joined).toMatch(/git tag -a v2.2.0/);
|
|
55
56
|
expect(joined).toMatch(/npm publish/);
|
|
56
57
|
expect(joined).toMatch(/git push origin main/);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// The Change Record commands are reachable from the real program: `awm change new|validate` and `awm changes list`. Every command
|
|
4
|
+
// module is tested by registering it on a fresh Commander program, which would stay green if src/index.ts forgot the register call.
|
|
5
|
+
const index_1 = require("../../src/index");
|
|
6
|
+
const find = (parent, name) => parent.commands.find((c) => c.name() === name);
|
|
7
|
+
const flags = (command) => (command?.options ?? []).map((o) => o.long ?? o.flags).sort();
|
|
8
|
+
describe('the real awm program registers the Change Record commands', () => {
|
|
9
|
+
test('`awm change` has `new <slug>` and `validate [path]` with their options', () => {
|
|
10
|
+
const change = find(index_1.program, 'change');
|
|
11
|
+
expect(change).toBeDefined();
|
|
12
|
+
const created = find(change, 'new');
|
|
13
|
+
const validated = find(change, 'validate');
|
|
14
|
+
expect(created?.usage()).toContain('<slug>');
|
|
15
|
+
expect(flags(created)).toEqual(['--base', '--issue', '--model', '--plan', '--refresh', '--session']);
|
|
16
|
+
expect(validated?.usage()).toContain('[path]');
|
|
17
|
+
expect(flags(validated)).toEqual(['--base', '--json', '--strict']);
|
|
18
|
+
});
|
|
19
|
+
test('`awm changes` has `list` with --since, --base and --format', () => {
|
|
20
|
+
const changes = find(index_1.program, 'changes');
|
|
21
|
+
expect(changes).toBeDefined();
|
|
22
|
+
expect(flags(find(changes, 'list'))).toEqual(['--base', '--format', '--since']);
|
|
23
|
+
});
|
|
24
|
+
test('both are in the top-level help', () => {
|
|
25
|
+
const help = index_1.program.helpInformation();
|
|
26
|
+
expect(help).toMatch(/^\s+change\b/m);
|
|
27
|
+
expect(help).toMatch(/^\s+changes\b/m);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Structural guards for two defect classes that recurred through the Change Record cycle (#217,
|
|
3
|
+
// harness-retro 2026-10-17; see the branch ledger signatures `s3-cq-dirty-duplicates-gitstate`,
|
|
4
|
+
// `git-dir-env-not-scrubbed` and `silent-return-instead-of-skip`):
|
|
5
|
+
//
|
|
6
|
+
// 1. Every git child of the change-record code runs through `execGit`/`runGit`/`gitState`
|
|
7
|
+
// (core/changes/git-env.ts), which scrubs inherited GIT_* location/pathspec variables, closes
|
|
8
|
+
// stdin, bounds time and output and sets GIT_NO_LAZY_FETCH. A bare `execFileSync('git', ...)`
|
|
9
|
+
// in these directories silently loses all of that, and reviewers found such leaks four times.
|
|
10
|
+
// 2. A test that returns early on Windows is reported as PASSED by the 3-platform release gate.
|
|
11
|
+
// Every suite under cli/tests uses a visible skip instead: `posixOnly`, or `symlinkTest` when the
|
|
12
|
+
// test only needs symlinks (cli/tests/helpers/platform.ts).
|
|
13
|
+
//
|
|
14
|
+
// The detectors are exercised on inline samples below, so each guard demonstrably fires.
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.bareGitSpawns = bareGitSpawns;
|
|
20
|
+
exports.silentWin32Returns = silentWin32Returns;
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const CLI_ROOT = path_1.default.join(__dirname, '../..');
|
|
24
|
+
function tsFiles(dir) {
|
|
25
|
+
if (!fs_1.default.existsSync(dir))
|
|
26
|
+
return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
29
|
+
const full = path_1.default.join(dir, entry.name);
|
|
30
|
+
if (entry.isDirectory())
|
|
31
|
+
out.push(...tsFiles(full));
|
|
32
|
+
else if (entry.isFile() && entry.name.endsWith('.ts'))
|
|
33
|
+
out.push(full);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
// Naive comment stripping, as in exec-invocation-explicit-stdio.test.ts: a false positive fails loud.
|
|
38
|
+
function stripComments(source) {
|
|
39
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
40
|
+
}
|
|
41
|
+
const BARE_GIT_SPAWN = /\b(?:execFileSync|execFile|spawnSync|spawn)\(\s*['"`]git['"`]/;
|
|
42
|
+
const SILENT_WIN32_RETURN = /\bif\s*\(\s*(?:process\.platform\s*===\s*['"]win32['"]|win32|isWindows)\s*\)\s*(?:\{\s*)?return\b/;
|
|
43
|
+
function bareGitSpawns(source) {
|
|
44
|
+
return BARE_GIT_SPAWN.test(stripComments(source));
|
|
45
|
+
}
|
|
46
|
+
function silentWin32Returns(source) {
|
|
47
|
+
return SILENT_WIN32_RETURN.test(stripComments(source));
|
|
48
|
+
}
|
|
49
|
+
describe('change-record conventions (structural)', () => {
|
|
50
|
+
test('the detectors fire on the offending shapes and not on the sanctioned ones', () => {
|
|
51
|
+
expect(bareGitSpawns("execFileSync('git', ['status'])")).toBe(true);
|
|
52
|
+
expect(bareGitSpawns('spawnSync("git", args, opts)')).toBe(true);
|
|
53
|
+
expect(bareGitSpawns("runGit(root, ['status'])")).toBe(false);
|
|
54
|
+
expect(bareGitSpawns("// execFileSync('git', [])\nexecGit(root, [])")).toBe(false);
|
|
55
|
+
expect(silentWin32Returns("if (process.platform === 'win32') return;")).toBe(true);
|
|
56
|
+
expect(silentWin32Returns('if (win32) { return; }')).toBe(true);
|
|
57
|
+
expect(silentWin32Returns('posixOnly(() => { /* body */ });')).toBe(false);
|
|
58
|
+
expect(silentWin32Returns("// if (process.platform === 'win32') return;")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
test('git is spawned only by git-env.ts under core/changes and the change commands', () => {
|
|
61
|
+
const dirs = ['src/core/changes', 'src/commands/change', 'src/commands/changes'].map(d => path_1.default.join(CLI_ROOT, d));
|
|
62
|
+
const allowed = path_1.default.join(CLI_ROOT, 'src/core/changes/git-env.ts');
|
|
63
|
+
const offenders = dirs
|
|
64
|
+
.flatMap(tsFiles)
|
|
65
|
+
.filter(file => file !== allowed)
|
|
66
|
+
.filter(file => bareGitSpawns(fs_1.default.readFileSync(file, 'utf8')))
|
|
67
|
+
.map(file => path_1.default.relative(CLI_ROOT, file));
|
|
68
|
+
expect(offenders).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
test('no test under cli/tests returns early on Windows instead of skipping visibly', () => {
|
|
71
|
+
// This file is excluded: its detector samples are offending shapes on purpose.
|
|
72
|
+
const offenders = tsFiles(path_1.default.join(CLI_ROOT, 'tests'))
|
|
73
|
+
.filter(file => file !== __filename)
|
|
74
|
+
.filter(file => silentWin32Returns(fs_1.default.readFileSync(file, 'utf8')))
|
|
75
|
+
.map(file => path_1.default.relative(CLI_ROOT, file));
|
|
76
|
+
expect(offenders).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const DESIGN = path_1.default.resolve(__dirname, '../../../docs/plans/2026-08-23-process-lifecycle-design.md');
|
|
9
|
+
function artifactSection() {
|
|
10
|
+
const text = fs_1.default.readFileSync(DESIGN, 'utf8');
|
|
11
|
+
const start = text.indexOf('### El artefacto');
|
|
12
|
+
expect(start).toBeGreaterThan(-1);
|
|
13
|
+
const end = text.indexOf('\n### ', start + 1);
|
|
14
|
+
return text.slice(start, end === -1 ? undefined : end);
|
|
15
|
+
}
|
|
16
|
+
describe('process-model contract documents the optional description (PD-4)', () => {
|
|
17
|
+
it('lists description in the frontmatter block as optional', () => {
|
|
18
|
+
const section = artifactSection();
|
|
19
|
+
expect(section).toMatch(/^description: <texto de una línea> {2}# opcional$/m);
|
|
20
|
+
});
|
|
21
|
+
it('states the rules the parser enforces and why the field exists', () => {
|
|
22
|
+
/** Whitespace-normalized so re-wrapping the prose never breaks a pinned phrase. */
|
|
23
|
+
const section = artifactSection().replace(/\s+/g, ' ');
|
|
24
|
+
expect(section).toContain('#### Enmienda 2026-10-18 — `description` opcional');
|
|
25
|
+
expect(section).toContain('una sola línea');
|
|
26
|
+
expect(section).toContain('1024 caracteres');
|
|
27
|
+
expect(section).toContain('Codex');
|
|
28
|
+
expect(section).toContain('único campo opcional');
|
|
29
|
+
expect(section).toContain('parser YAML');
|
|
30
|
+
expect(section).not.toContain('no reemplaza a un parser YAML');
|
|
31
|
+
expect(section).toContain('code points');
|
|
32
|
+
expect(section).toContain('caracteres de control');
|
|
33
|
+
});
|
|
34
|
+
});
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|