agentic-workflow-manager 9.20.0 → 9.20.2
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 +2 -2
- package/dist/src/commands/changes/index.js +2 -1
- package/dist/src/core/changes/derive.js +21 -8
- package/dist/src/core/changes/git.js +37 -6
- package/dist/src/core/changes/range.js +28 -13
- package/dist/src/core/process/model.js +153 -10
- package/dist/tests/commands/change/new.test.js +28 -1
- package/dist/tests/commands/change/validate.test.js +3 -3
- package/dist/tests/commands/changes/list.test.js +1 -0
- package/dist/tests/commands/process.test.js +4 -0
- package/dist/tests/core/changes/derive.test.js +75 -2
- package/dist/tests/core/changes/git.test.js +83 -1
- package/dist/tests/core/changes/helpers.js +2 -2
- package/dist/tests/core/changes/list.test.js +6 -5
- package/dist/tests/core/changes/range.test.js +89 -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/structural/change-record-git-and-platform-conventions.test.js +6 -5
- package/dist/tests/structural/native-release-artifacts.test.js +295 -46
- 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
|
@@ -5,25 +5,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
9
11
|
const CLI_ROOT = path_1.default.resolve(__dirname, '..', '..');
|
|
10
12
|
const REPO_ROOT = path_1.default.resolve(CLI_ROOT, '..');
|
|
11
13
|
function readWorkflow(name) {
|
|
12
14
|
return fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.github', 'workflows', name), 'utf8').replace(/\r\n/g, '\n');
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
const HOT_IGNORE = String.raw `node_modules|[/\\]cli[/\\]tests[/\\]commands[/\\]watch[/\\]track-(finalize|freeze)\.test\.ts$`;
|
|
17
|
+
const CHANGES_PATTERN = String.raw `[/\\]cli[/\\]tests[/\\](core[/\\]changes|commands[/\\]changes?)[/\\]`;
|
|
18
|
+
const HOT_STEP = 'Test hot Windows ARM suites';
|
|
19
|
+
const CHANGES_STEP = 'Test Change Record Windows ARM suites';
|
|
20
|
+
const CHANGES_RUN = `npx jest --runInBand --bail --testPathPatterns='${CHANGES_PATTERN}'`;
|
|
21
|
+
const REMAINDER_COMMENT = " # The ARM runner's slow suites run in two separate jobs (watch track-finalize/freeze, and the Change Record suites). Keep runInBand within each job; no test file may run twice or be dropped.\n";
|
|
22
|
+
const HOT_RUN = 'npx jest --runTestsByPath tests/commands/watch/track-finalize.test.ts tests/commands/watch/track-freeze.test.ts --runInBand --bail';
|
|
23
|
+
const RELEASE_GATE = ['test', 'windows-arm-changes', 'windows-arm-hot'];
|
|
24
|
+
const SHARD_SETUP = [
|
|
25
|
+
{ uses: 'actions/checkout@v4' },
|
|
26
|
+
{ uses: 'actions/setup-node@v4', with: { 'node-version': '22' } },
|
|
27
|
+
{ name: 'Install', 'working-directory': 'cli', run: 'npm ci' },
|
|
28
|
+
{ name: 'Build', 'working-directory': 'cli', run: 'npm run native:build && npm run build' },
|
|
29
|
+
{ name: 'Build test-only native addon', 'working-directory': 'cli', run: 'npm run native:test-build' },
|
|
30
|
+
];
|
|
31
|
+
// Jest compiles --testPathPatterns with the `i` flag but --testPathIgnorePatterns
|
|
32
|
+
// without it, so a directory differing only in case would run in both the shard
|
|
33
|
+
// and the remainder. Returns every directory that falls in that gap.
|
|
34
|
+
function caseOnlyShardPaths(directories, pattern) {
|
|
35
|
+
const insensitive = new RegExp(pattern, 'i');
|
|
36
|
+
const sensitive = new RegExp(pattern);
|
|
37
|
+
return directories.filter((directory) => {
|
|
38
|
+
const candidate = `${directory}/`;
|
|
39
|
+
return insensitive.test(candidate) && !sensitive.test(candidate);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const SHARD_DIRECTORIES = [['core', 'changes'], ['commands', 'change'], ['commands', 'changes']];
|
|
43
|
+
// Each real shard directory must exist with its exact case, hold at least one
|
|
44
|
+
// suite and match CHANGES_PATTERN, so the case-only walk cannot pass vacuously.
|
|
45
|
+
function shardDirectoryProblems(testsRoot) {
|
|
46
|
+
const shard = new RegExp(CHANGES_PATTERN);
|
|
47
|
+
const problems = [];
|
|
48
|
+
for (const segments of SHARD_DIRECTORIES) {
|
|
49
|
+
const label = segments.join('/');
|
|
50
|
+
let current = testsRoot;
|
|
51
|
+
let found = true;
|
|
52
|
+
for (const segment of segments) {
|
|
53
|
+
// readdirSync keeps the on-disk case even on case-insensitive file systems.
|
|
54
|
+
if (!fs_1.default.existsSync(current) || !fs_1.default.readdirSync(current).includes(segment)) {
|
|
55
|
+
found = false;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
current = path_1.default.join(current, segment);
|
|
59
|
+
}
|
|
60
|
+
if (!found) {
|
|
61
|
+
problems.push(`${label}: missing with this exact case`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!fs_1.default.readdirSync(current).some((entry) => entry.endsWith('.test.ts'))) {
|
|
65
|
+
problems.push(`${label}: holds no *.test.ts`);
|
|
66
|
+
}
|
|
67
|
+
if (!shard.test(`${current}${path_1.default.sep}`)) {
|
|
68
|
+
problems.push(`${label}: does not match CHANGES_PATTERN`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return problems;
|
|
72
|
+
}
|
|
73
|
+
function parseJobs(source) {
|
|
74
|
+
const parsed = js_yaml_1.default.load(source);
|
|
75
|
+
expect(parsed?.jobs).toBeDefined();
|
|
76
|
+
return parsed.jobs;
|
|
77
|
+
}
|
|
78
|
+
function getJob(jobs, name) {
|
|
79
|
+
const job = jobs[name];
|
|
80
|
+
expect(job).toBeDefined();
|
|
81
|
+
expect(Array.isArray(job.steps)).toBe(true);
|
|
82
|
+
return job;
|
|
83
|
+
}
|
|
84
|
+
function keysOf(value) {
|
|
85
|
+
return Object.keys(value);
|
|
86
|
+
}
|
|
87
|
+
// The release job publishes only when every test job passed: no if, no
|
|
88
|
+
// continue-on-error, and needs is exactly the three gating jobs in any order.
|
|
89
|
+
function assertReleaseGate(jobs) {
|
|
90
|
+
const release = getJob(jobs, 'release');
|
|
91
|
+
expect(keysOf(release)).not.toContain('if');
|
|
92
|
+
expect(keysOf(release)).not.toContain('continue-on-error');
|
|
93
|
+
for (const step of release.steps) {
|
|
94
|
+
expect(keysOf(step)).not.toContain('continue-on-error');
|
|
95
|
+
}
|
|
96
|
+
expect(Array.isArray(release.needs)).toBe(true);
|
|
97
|
+
const needs = release.needs;
|
|
98
|
+
expect(needs).toHaveLength(RELEASE_GATE.length);
|
|
99
|
+
expect([...needs].sort()).toEqual(RELEASE_GATE);
|
|
100
|
+
}
|
|
101
|
+
// Gate semantics are asserted on the parsed workflow, so key quoting or escaping
|
|
102
|
+
// cannot hide an if or a continue-on-error. Text is checked only where the exact
|
|
103
|
+
// text is the contract: the run lines and the A1 comment.
|
|
104
|
+
function assertArmPartition(source, gatesRelease) {
|
|
15
105
|
const workflow = source.replace(/\r\n/g, '\n');
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
expect(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
106
|
+
const jobs = parseJobs(source);
|
|
107
|
+
const names = keysOf(jobs);
|
|
108
|
+
const test = getJob(jobs, 'test');
|
|
109
|
+
const hot = getJob(jobs, 'windows-arm-hot');
|
|
110
|
+
const changes = getJob(jobs, 'windows-arm-changes');
|
|
111
|
+
expect(names.indexOf('windows-arm-hot')).toBeGreaterThan(names.indexOf('test'));
|
|
112
|
+
expect(names.indexOf('windows-arm-changes')).toBe(names.indexOf('windows-arm-hot') + 1);
|
|
113
|
+
// No gating job tolerates failure, at job or step level.
|
|
114
|
+
for (const job of [test, hot, changes]) {
|
|
115
|
+
expect(keysOf(job)).not.toContain('continue-on-error');
|
|
116
|
+
for (const step of job.steps) {
|
|
117
|
+
expect(keysOf(step)).not.toContain('continue-on-error');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// The ARM shards never skip, at job or step level.
|
|
121
|
+
for (const job of [hot, changes]) {
|
|
122
|
+
expect(keysOf(job)).not.toContain('if');
|
|
123
|
+
for (const step of job.steps) {
|
|
124
|
+
expect(keysOf(step)).not.toContain('if');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// In the matrix, only the two test steps are guarded, with complementary targets.
|
|
128
|
+
const guarded = test.steps.filter((step) => keysOf(step).includes('if'));
|
|
129
|
+
expect(guarded).toHaveLength(2);
|
|
130
|
+
const [regular, remainder] = guarded;
|
|
131
|
+
expect(regular.name).toMatch(/^Tests?$/);
|
|
132
|
+
expect(regular.if).toBe("matrix.target != 'win32-arm64'");
|
|
133
|
+
expect(regular['working-directory']).toBe('cli');
|
|
134
|
+
expect(regular.run).toBe('npx jest --runInBand --bail');
|
|
135
|
+
expect(remainder.name).toMatch(/^Tests? \(Windows ARM remainder\)$/);
|
|
136
|
+
expect(remainder.if).toBe("matrix.target == 'win32-arm64'");
|
|
137
|
+
expect(remainder['working-directory']).toBe('cli');
|
|
138
|
+
const ignorePattern = String(remainder.run).match(/^npx jest --runInBand --bail --testPathIgnorePatterns='([^']+)'$/)?.[1];
|
|
139
|
+
expect(ignorePattern).toBe(`${HOT_IGNORE}|${CHANGES_PATTERN}`);
|
|
140
|
+
expect(test.steps.filter((step) => step.uses === 'actions/upload-artifact@v4')).toHaveLength(1);
|
|
141
|
+
// A1 comment, directly above the remainder step.
|
|
142
|
+
expect(workflow.split(REMAINDER_COMMENT)).toHaveLength(2);
|
|
143
|
+
const aroundRemainder = workflow.split(/ - name: Tests? \(Windows ARM remainder\)\n/);
|
|
144
|
+
expect(aroundRemainder).toHaveLength(2);
|
|
145
|
+
expect(aroundRemainder[0].endsWith(REMAINDER_COMMENT)).toBe(true);
|
|
146
|
+
// Both shards share the same setup; only their test step differs.
|
|
147
|
+
expect(hot['runs-on']).toBe('windows-11-arm');
|
|
148
|
+
expect(hot.steps.slice(0, -1)).toEqual(SHARD_SETUP);
|
|
149
|
+
expect(hot.steps.at(-1)).toEqual({ name: HOT_STEP, 'working-directory': 'cli', run: HOT_RUN });
|
|
150
|
+
expect(changes.steps.at(-1)).toEqual({ name: CHANGES_STEP, 'working-directory': 'cli', run: CHANGES_RUN });
|
|
151
|
+
expect({ ...changes, steps: changes.steps.slice(0, -1) }).toEqual({ ...hot, steps: hot.steps.slice(0, -1) });
|
|
152
|
+
if (gatesRelease) {
|
|
153
|
+
expect(changes.permissions).toEqual({ contents: 'read' });
|
|
154
|
+
}
|
|
155
|
+
for (const job of [hot, changes]) {
|
|
156
|
+
expect(job.steps.some((step) => String(step.uses ?? '').includes('upload-artifact'))).toBe(false);
|
|
157
|
+
expect(JSON.stringify(job)).not.toContain('secure-fs-win32-arm64');
|
|
158
|
+
}
|
|
159
|
+
const lines = workflow.split('\n');
|
|
160
|
+
expect(lines).toContain(` run: ${HOT_RUN}`);
|
|
161
|
+
expect(lines).toContain(` run: ${CHANGES_RUN}`);
|
|
162
|
+
// The remainder ignores exactly the hot suites plus what the Change Record shard runs.
|
|
27
163
|
const ignored = new RegExp(ignorePattern);
|
|
28
164
|
for (const separator of ['/', '\\']) {
|
|
29
165
|
const root = ['repo', 'cli', 'tests'].join(separator);
|
|
@@ -32,18 +168,100 @@ function assertArmPartition(source) {
|
|
|
32
168
|
expect(ignored.test([root, 'other', `${suite}.test.ts`].join(separator))).toBe(false);
|
|
33
169
|
}
|
|
34
170
|
}
|
|
35
|
-
const
|
|
36
|
-
expect(
|
|
37
|
-
expect(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
171
|
+
const shardPattern = String(changes.steps.at(-1)?.run).match(/--testPathPatterns='([^']+)'$/)?.[1];
|
|
172
|
+
expect(shardPattern).toBe(CHANGES_PATTERN);
|
|
173
|
+
expect(ignorePattern).toBe(`${HOT_IGNORE}|${shardPattern}`);
|
|
174
|
+
const shard = new RegExp(shardPattern, 'i');
|
|
175
|
+
for (const separator of ['/', '\\']) {
|
|
176
|
+
const root = ['repo', 'cli', 'tests'].join(separator);
|
|
177
|
+
for (const file of [['core', 'changes', 'x.test.ts'], ['commands', 'change', 'new.test.ts'], ['commands', 'changes', 'list.test.ts']]) {
|
|
178
|
+
const candidate = [root, ...file].join(separator);
|
|
179
|
+
expect(shard.test(candidate)).toBe(true);
|
|
180
|
+
expect(ignored.test(candidate)).toBe(true);
|
|
181
|
+
}
|
|
182
|
+
for (const file of [['core', 'changesets', 'x.test.ts'], ['commands', 'changesets', 'x.test.ts'], ['commands', 'watch', 'track-finalize.test.ts']]) {
|
|
183
|
+
expect(shard.test([root, ...file].join(separator))).toBe(false);
|
|
184
|
+
}
|
|
185
|
+
expect(ignored.test([root, 'core', 'changesets', 'x.test.ts'].join(separator))).toBe(false);
|
|
186
|
+
}
|
|
187
|
+
if (gatesRelease) {
|
|
188
|
+
assertReleaseGate(jobs);
|
|
189
|
+
// CI-2 keeps the comment on the release job's needs line.
|
|
190
|
+
const releaseText = workflow.slice(workflow.search(/^ release:\n/m));
|
|
191
|
+
expect(releaseText.match(/^ needs: [^\n]*$/m)?.[0]).toMatch(/ # rojo en cualquier plataforma o shard ARM no publica$/);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
expect(names).not.toContain('release');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Rewrites the release job's inline `needs` list in the workflow text.
|
|
198
|
+
function rewriteReleaseNeeds(workflow, rewrite) {
|
|
199
|
+
const header = workflow.search(/^ release:\n/m);
|
|
200
|
+
expect(header).toBeGreaterThanOrEqual(0);
|
|
201
|
+
const head = workflow.slice(0, header);
|
|
202
|
+
const tail = workflow.slice(header).replace(/^( needs: )\[([^\]\n]*)\]/m, (_line, prefix, list) => `${prefix}[${rewrite(list.split(',').map((job) => job.trim())).join(', ')}]`);
|
|
203
|
+
return head + tail;
|
|
204
|
+
}
|
|
205
|
+
function permutations(items) {
|
|
206
|
+
if (items.length <= 1)
|
|
207
|
+
return [items];
|
|
208
|
+
return items.flatMap((item, index) => permutations([...items.slice(0, index), ...items.slice(index + 1)]).map((rest) => [item, ...rest]));
|
|
209
|
+
}
|
|
210
|
+
// Every bypass of the partition or the release gate must make assertArmPartition
|
|
211
|
+
// throw: a failed assertion or a mutated text that no longer parses as YAML.
|
|
212
|
+
function assertMutationsRejected(workflow, gates) {
|
|
213
|
+
const jobLevel = (job, line) => workflow.replace(new RegExp(`^ ${job}:\\n`, 'm'), ` ${job}:\n ${line}\n`);
|
|
214
|
+
const inStep = (step, line) => workflow.replace(` - name: ${step}\n`, ` - name: ${step}\n ${line}\n`);
|
|
215
|
+
const mutations = [
|
|
216
|
+
// Swapped matrix guards.
|
|
217
|
+
workflow.replace(/if: matrix\.target (?:!=|==) 'win32-arm64'/g, (guard) => guard.includes('!=') ? "if: matrix.target == 'win32-arm64'" : "if: matrix.target != 'win32-arm64'"),
|
|
218
|
+
// A second producer of the native artifact.
|
|
219
|
+
workflow.replace('uses: actions/upload-artifact@v4', 'uses: actions/upload-artifact@v4\n - uses: actions/upload-artifact@v4'),
|
|
220
|
+
// A remainder that drops more than the shards run.
|
|
221
|
+
workflow.replace(/(--testPathIgnorePatterns='[^']+)(?=')/, '$1|runner\\.test\\.ts$'),
|
|
222
|
+
// A shard pattern that differs from the remainder's exclusion.
|
|
223
|
+
workflow.replace(/(--testPathPatterns='[^']+)changes\?/, '$1changes'),
|
|
224
|
+
// Masked shard runs.
|
|
225
|
+
workflow.replace(/(run: npx jest --runTestsByPath [^\n]+ --runInBand --bail)(?=\n)/, '$1; exit 0'),
|
|
226
|
+
workflow.replace(/(run: npx jest --runInBand --bail --testPathPatterns='[^']+')(?=\n)/, '$1; exit 0'),
|
|
227
|
+
// Skipped steps or jobs, in any key quoting.
|
|
228
|
+
inStep(HOT_STEP, 'if: false'),
|
|
229
|
+
inStep(HOT_STEP, '"if": always()'),
|
|
230
|
+
inStep(HOT_STEP, "'if': false"),
|
|
231
|
+
inStep(CHANGES_STEP, 'if: false'),
|
|
232
|
+
inStep(CHANGES_STEP, '"if": always()'),
|
|
233
|
+
inStep(CHANGES_STEP, "'if': false"),
|
|
234
|
+
inStep('Upload native secure-fs artifact', "'if': false"),
|
|
235
|
+
jobLevel('windows-arm-hot', '"if": always()'),
|
|
236
|
+
jobLevel('windows-arm-hot', "'if': false"),
|
|
237
|
+
jobLevel('windows-arm-changes', '"if": always()'),
|
|
238
|
+
jobLevel('windows-arm-changes', "'if': false"),
|
|
239
|
+
// Tolerated failures, in any key quoting, escaping or expression form.
|
|
240
|
+
jobLevel('test', "continue-on-error: ${{ matrix.target == 'win32-arm64' }}"),
|
|
241
|
+
jobLevel('test', '"continue-on-error": true'),
|
|
242
|
+
jobLevel('test', '"continue\\x2Don-error": true'),
|
|
243
|
+
jobLevel('windows-arm-hot', 'continue-on-error: true'),
|
|
244
|
+
jobLevel('windows-arm-hot', 'continue-on-error: ${{ true }}'),
|
|
245
|
+
jobLevel('windows-arm-hot', '"continue-on-error": true'),
|
|
246
|
+
jobLevel('windows-arm-changes', 'continue-on-error: true'),
|
|
247
|
+
jobLevel('windows-arm-changes', 'continue-on-error: ${{ true }}'),
|
|
248
|
+
jobLevel('windows-arm-changes', '"continue-on-error": true'),
|
|
249
|
+
inStep(HOT_STEP, '"continue-on-error": true'),
|
|
250
|
+
inStep(CHANGES_STEP, "'continue-on-error': true"),
|
|
251
|
+
workflow.replace(/^( run: npx jest --runInBand --bail\n)/m, '$1 continue-on-error: true\n'),
|
|
252
|
+
workflow.replace(/(run: npx jest --runInBand --bail --testPathIgnorePatterns='[^']+'\n)/, '$1 continue-on-error: true\n'),
|
|
253
|
+
workflow.replace(/(run: npx jest --runInBand --bail --testPathIgnorePatterns='[^']+'\n)/, '$1 continue-on-error: ${{ true }}\n'),
|
|
254
|
+
];
|
|
255
|
+
if (gates) {
|
|
256
|
+
mutations.push(rewriteReleaseNeeds(workflow, (jobs) => jobs.filter((job) => job !== 'windows-arm-changes')), rewriteReleaseNeeds(workflow, (jobs) => [...jobs, 'windows-arm-changes']), jobLevel('release', 'if: always()'), jobLevel('release', 'if: ${{ !cancelled() }}'), jobLevel('release', '"if": always()'), jobLevel('release', "'if': false"), jobLevel('release', 'continue-on-error: true'), jobLevel('release', '"continue-on-error": true'),
|
|
257
|
+
// The needs comment is part of CI-2.
|
|
258
|
+
workflow.slice(0, workflow.search(/^ release:\n/m))
|
|
259
|
+
+ workflow.slice(workflow.search(/^ release:\n/m)).replace(/^( needs: [^\n]*?) # rojo en cualquier plataforma o shard ARM no publica$/m, '$1'));
|
|
260
|
+
}
|
|
261
|
+
for (const mutated of mutations) {
|
|
262
|
+
expect(mutated).not.toBe(workflow);
|
|
263
|
+
expect(() => assertArmPartition(mutated, gates)).toThrow();
|
|
264
|
+
}
|
|
47
265
|
}
|
|
48
266
|
describe('native release artifacts', () => {
|
|
49
267
|
it('runs each platform suite once without weakening publication', () => {
|
|
@@ -54,44 +272,75 @@ describe('native release artifacts', () => {
|
|
|
54
272
|
expect(workflow).toContain('run: npx jest --runInBand --bail');
|
|
55
273
|
}
|
|
56
274
|
const release = readWorkflow('release.yml');
|
|
57
|
-
|
|
275
|
+
assertReleaseGate(parseJobs(release));
|
|
58
276
|
for (const target of ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'win32-x64', 'win32-arm64']) {
|
|
59
277
|
expect(release).toContain(`target: ${target}`);
|
|
60
278
|
}
|
|
61
279
|
expect(release).toContain('cancel-in-progress: false');
|
|
62
280
|
});
|
|
63
|
-
it('partitions the
|
|
281
|
+
it('partitions the hot and Change Record ARM shards without duplicating the native artifact', () => {
|
|
64
282
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
65
283
|
const workflow = readWorkflow(name);
|
|
66
|
-
assertArmPartition(workflow);
|
|
284
|
+
assertArmPartition(workflow, name === 'release.yml');
|
|
67
285
|
}
|
|
68
286
|
});
|
|
69
287
|
it('checks the same partition after a Windows CRLF checkout', () => {
|
|
70
288
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
71
289
|
const workflow = readWorkflow(name);
|
|
72
|
-
assertArmPartition(workflow.replace(/\n/g, '\r\n'));
|
|
290
|
+
assertArmPartition(workflow.replace(/\n/g, '\r\n'), name === 'release.yml');
|
|
73
291
|
}
|
|
74
292
|
});
|
|
75
|
-
it('rejects swapped matrix guards, non-gating
|
|
293
|
+
it('rejects swapped matrix guards, non-gating ARM shards, drifted partitions and duplicate artifacts', () => {
|
|
76
294
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
295
|
+
assertMutationsRejected(readWorkflow(name), name === 'release.yml');
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
it('keeps the whole self-test green when release needs is reordered', () => {
|
|
299
|
+
const workflow = readWorkflow('release.yml');
|
|
300
|
+
let current = [];
|
|
301
|
+
rewriteReleaseNeeds(workflow, (jobs) => (current = jobs));
|
|
302
|
+
const reorderedJobs = permutations(current).find((jobs) => jobs.join() !== current.join());
|
|
303
|
+
expect(reorderedJobs).toBeDefined();
|
|
304
|
+
const reordered = rewriteReleaseNeeds(workflow, () => reorderedJobs);
|
|
305
|
+
expect(reordered).not.toBe(workflow);
|
|
306
|
+
expect(() => assertArmPartition(reordered, true)).not.toThrow();
|
|
307
|
+
expect(() => assertMutationsRejected(reordered, true)).not.toThrow();
|
|
308
|
+
});
|
|
309
|
+
it('reports a shard directory that is missing, renamed by case or holds no suite', () => {
|
|
310
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-arm-shard-'));
|
|
311
|
+
try {
|
|
312
|
+
const testsRoot = path_1.default.join(tmp, 'cli', 'tests');
|
|
313
|
+
for (const segments of SHARD_DIRECTORIES) {
|
|
314
|
+
fs_1.default.mkdirSync(path_1.default.join(testsRoot, ...segments), { recursive: true });
|
|
315
|
+
fs_1.default.writeFileSync(path_1.default.join(testsRoot, ...segments, 'x.test.ts'), '');
|
|
87
316
|
}
|
|
88
|
-
expect(()
|
|
89
|
-
|
|
90
|
-
expect(()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(()
|
|
94
|
-
|
|
317
|
+
expect(shardDirectoryProblems(testsRoot)).toEqual([]);
|
|
318
|
+
fs_1.default.renameSync(path_1.default.join(testsRoot, 'core', 'changes'), path_1.default.join(testsRoot, 'core', 'Changes'));
|
|
319
|
+
expect(shardDirectoryProblems(testsRoot)).toEqual([expect.stringContaining('core/changes')]);
|
|
320
|
+
fs_1.default.renameSync(path_1.default.join(testsRoot, 'core', 'Changes'), path_1.default.join(testsRoot, 'core', 'changes'));
|
|
321
|
+
fs_1.default.renameSync(path_1.default.join(testsRoot, 'commands', 'change', 'x.test.ts'), path_1.default.join(testsRoot, 'commands', 'change', 'helpers.ts'));
|
|
322
|
+
expect(shardDirectoryProblems(testsRoot)).toEqual([expect.stringContaining('commands/change')]);
|
|
323
|
+
fs_1.default.rmSync(path_1.default.join(testsRoot, 'commands', 'changes'), { recursive: true });
|
|
324
|
+
expect(shardDirectoryProblems(testsRoot)).toHaveLength(2);
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
it('flags test directories that only the case-insensitive shard pattern selects', () => {
|
|
331
|
+
expect(caseOnlyShardPaths(['repo/cli/tests/core/Changes'], CHANGES_PATTERN)).toEqual(['repo/cli/tests/core/Changes']);
|
|
332
|
+
expect(caseOnlyShardPaths(['repo\\cli\\tests\\commands\\CHANGE'], CHANGES_PATTERN)).toEqual(['repo\\cli\\tests\\commands\\CHANGE']);
|
|
333
|
+
expect(caseOnlyShardPaths(['repo/cli/tests/core/changes', 'repo/cli/tests/commands/change', 'repo/cli/tests/core/Changesets'], CHANGES_PATTERN)).toEqual([]);
|
|
334
|
+
});
|
|
335
|
+
it('has no test directory selected by the shard but kept by the remainder through case alone', () => {
|
|
336
|
+
const testsRoot = path_1.default.join(CLI_ROOT, 'tests');
|
|
337
|
+
const directories = fs_1.default.readdirSync(testsRoot, { recursive: true })
|
|
338
|
+
.map((relative) => path_1.default.join(testsRoot, relative))
|
|
339
|
+
.filter((candidate) => fs_1.default.statSync(candidate).isDirectory());
|
|
340
|
+
expect(shardDirectoryProblems(testsRoot)).toEqual([]);
|
|
341
|
+
const flagged = caseOnlyShardPaths(directories, CHANGES_PATTERN);
|
|
342
|
+
if (flagged.length > 0) {
|
|
343
|
+
throw new Error(`Case-only Change Record directories would run in both ARM shards: ${flagged.join(', ')}`);
|
|
95
344
|
}
|
|
96
345
|
});
|
|
97
346
|
it('keeps native prebuilds generated instead of versioned source', () => {
|
|
@@ -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
|