agentic-workflow-manager 9.22.0 → 9.22.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 +2 -2
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +103 -57
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +18 -6
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +2 -2
- 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
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
- package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +0 -102
- package/dist/tests/structural/sensor-slice-decision-contract.test.js +0 -63
|
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
const fs_1 = __importDefault(require("fs"));
|
|
43
43
|
const os_1 = __importDefault(require("os"));
|
|
44
44
|
const path_1 = __importDefault(require("path"));
|
|
45
|
+
const commit_seeded_registry_1 = require("../helpers/commit-seeded-registry");
|
|
45
46
|
describe('codex provider — isolated home E2E (Task 9)', () => {
|
|
46
47
|
let tmpHome;
|
|
47
48
|
let tmpWork;
|
|
@@ -79,8 +80,10 @@ describe('codex provider — isolated home E2E (Task 9)', () => {
|
|
|
79
80
|
});
|
|
80
81
|
/** Hand-seeds a registry content root with everything a real `awm init` needs:
|
|
81
82
|
* hooks (Claude + Codex), the bootstrap skill, one baseline bundle providing
|
|
82
|
-
* both a skill and a Codex-native agent artifact.
|
|
83
|
-
*
|
|
83
|
+
* both a skill and a Codex-native agent artifact. Content-root-shaped like a
|
|
84
|
+
* real clone, plus a local `.git` commit so `machine.registryCache.present`
|
|
85
|
+
* is true and `stepCache` skips network sync (SG-7 hard-fails any sync error,
|
|
86
|
+
* and `https://example.invalid/...` is intentionally unreachable). */
|
|
84
87
|
function seedPublicRegistryFixture(root) {
|
|
85
88
|
fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
|
|
86
89
|
fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
|
|
@@ -101,6 +104,8 @@ describe('codex provider — isolated home E2E (Task 9)', () => {
|
|
|
101
104
|
name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
|
|
102
105
|
dependsOn: [], skills: ['development-process'], workflows: [], agents: ['development-process'],
|
|
103
106
|
}));
|
|
107
|
+
// Local commit only — no origin. Present+.git makes stepCache skip sync.
|
|
108
|
+
(0, commit_seeded_registry_1.commitSeededRegistry)(root);
|
|
104
109
|
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
105
110
|
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
|
|
106
111
|
}
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
const fs_1 = __importDefault(require("fs"));
|
|
37
37
|
const os_1 = __importDefault(require("os"));
|
|
38
38
|
const path_1 = __importDefault(require("path"));
|
|
39
|
+
const commit_seeded_registry_1 = require("../helpers/commit-seeded-registry");
|
|
39
40
|
describe('copilot provider — isolated home E2E (devCore global-scope guard regression)', () => {
|
|
40
41
|
let tmpHome;
|
|
41
42
|
let tmpWork;
|
|
@@ -67,10 +68,11 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
|
|
|
67
68
|
process.env.AWM_HOME = originalAwmHome;
|
|
68
69
|
});
|
|
69
70
|
/** Same fixture shape as codex-provider-isolated.test.ts's seedPublicRegistryFixture:
|
|
70
|
-
* a real content-root registry
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* a real content-root registry with the bootstrap skill and one baseline bundle,
|
|
72
|
+
* plus a local `.git` commit so SG-7 does not hard-fail init on an unreachable
|
|
73
|
+
* example.invalid remote (stepCache skips when registryCache.present). Copilot
|
|
74
|
+
* needs no hooks (providers/index.ts has no `hooks` config for it) and no native
|
|
75
|
+
* agent renderer (`agent: null`), so the fixture is intentionally smaller than Codex's. */
|
|
74
76
|
function seedPublicRegistryFixture(root) {
|
|
75
77
|
fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
|
|
76
78
|
fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
|
|
@@ -88,6 +90,7 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
|
|
|
88
90
|
name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
|
|
89
91
|
dependsOn: [], skills: ['development-process'], workflows: [], agents: [],
|
|
90
92
|
}));
|
|
93
|
+
(0, commit_seeded_registry_1.commitSeededRegistry)(root);
|
|
91
94
|
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
92
95
|
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
|
|
93
96
|
}
|
|
@@ -75,12 +75,6 @@ describe('active documentation contract', () => {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
|
-
it('documents the two-stage boundary and official bundle scopes (R3, R8)', () => {
|
|
79
|
-
expect(read('docs/installation.md')).toContain('Prepare the machine');
|
|
80
|
-
expect(read('docs/project-setup.md')).toContain('Initialize a project');
|
|
81
|
-
expect(read('docs/configuration.md')).toContain('`dev` and `product`');
|
|
82
|
-
expect(read('docs/project-setup.md')).toContain('`frontend`');
|
|
83
|
-
});
|
|
84
78
|
it('documents every init provider and option (R5, R14.2)', () => {
|
|
85
79
|
const config = read('docs/configuration.md');
|
|
86
80
|
for (const agent of providers_1.AGENT_TARGETS) {
|
|
@@ -90,39 +84,4 @@ describe('active documentation contract', () => {
|
|
|
90
84
|
expect(config).toContain(`\`${flag}\``);
|
|
91
85
|
}
|
|
92
86
|
});
|
|
93
|
-
it('assigns canonical topics instead of copying provider paths (R12)', () => {
|
|
94
|
-
expect(read('docs/configuration.md')).toContain('[Support matrix](support-matrix.md)');
|
|
95
|
-
expect(read('docs/configuration.md')).not.toMatch(/~\/\.(?:claude|codex|cursor|gemini)/);
|
|
96
|
-
});
|
|
97
|
-
it('keeps generated provider support prose in the canonical language (R2)', () => {
|
|
98
|
-
const matrix = read('docs/support-matrix.md');
|
|
99
|
-
expect(matrix).toContain('### Where each artifact is installed');
|
|
100
|
-
expect(matrix).toContain('### Agent profiles, workflows, hooks, and context');
|
|
101
|
-
expect(matrix).not.toContain('Dónde aterriza cada artefacto');
|
|
102
|
-
expect(matrix).not.toContain('Perfiles de agente, workflows, hooks y contexto');
|
|
103
|
-
});
|
|
104
|
-
it('keeps R3 canonical documentation in English and reachable from the hub', () => {
|
|
105
|
-
const hub = read('docs/README.md');
|
|
106
|
-
expect(hub).toContain('decisions.md');
|
|
107
|
-
for (const file of [
|
|
108
|
-
'docs/framework.md', 'docs/configuration.md', 'docs/project-setup.md',
|
|
109
|
-
'docs/runbook.md', 'docs/cli-reference.md', 'docs/support-matrix.md',
|
|
110
|
-
'docs/architecture.md',
|
|
111
|
-
]) {
|
|
112
|
-
expect(read(file)).not.toContain('### Metodología');
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
it('keeps provider tiers, registries, and hook targets technically precise (R2)', () => {
|
|
116
|
-
const matrix = read('docs/support-matrix.md');
|
|
117
|
-
const runbook = read('docs/runbook.md');
|
|
118
|
-
const cliReference = read('docs/cli-reference.md');
|
|
119
|
-
expect(matrix).toContain('Re-anchors process guidance');
|
|
120
|
-
expect(matrix).toContain('Only `awm sensors run` is an enforceable deterministic gate');
|
|
121
|
-
expect(runbook).toContain('A registry needs at least one content directory');
|
|
122
|
-
expect(runbook).toMatch(/`catalog\.json` is required only when the registry\s+publishes bundles/);
|
|
123
|
-
expect(cliReference).toContain('`claude-code` and `codex`');
|
|
124
|
-
expect(cliReference).toContain('`--agent` is an alias for `--target`');
|
|
125
|
-
expect(cliReference).toContain('`dev` and `product` baseline bundles');
|
|
126
|
-
expect(runbook).toContain('bundles/<bundle-name>/bundle.json');
|
|
127
|
-
});
|
|
128
87
|
});
|
|
@@ -36,18 +36,6 @@ describe('bundle skill-reference contract', () => {
|
|
|
36
36
|
expect(skills).toEqual(['mi-proceso']);
|
|
37
37
|
expect(skills.every((skill) => typeof skill === 'string')).toBe(true);
|
|
38
38
|
});
|
|
39
|
-
it('places the CLI 9 migration boundary immediately after the bundle example', () => {
|
|
40
|
-
const guide = read(GUIDE);
|
|
41
|
-
const bundleFence = '```json\n{\n "name": "mi-proceso"';
|
|
42
|
-
const start = guide.indexOf(bundleFence);
|
|
43
|
-
const end = guide.indexOf('```', start) + 3;
|
|
44
|
-
const following = guide.slice(end, guide.indexOf('## 5.', end));
|
|
45
|
-
expect(start).toBeGreaterThanOrEqual(0);
|
|
46
|
-
expect(following).toMatch(/CLI 9[\s\S]*(?:legacy object|objeto legacy)[\s\S]*\{\s*name\s*,\s*onSignal:\s*true\s*\}/i);
|
|
47
|
-
expect(following).toMatch(/ignor(?:e|a)[\s\S]*boolean[\s\S]*onSignal/i);
|
|
48
|
-
expect(following).toMatch(/warning/i);
|
|
49
|
-
expect(following).toMatch(/CLI 10[\s\S]*(?:rejects?|rechaza)[\s\S]*(?:objects?|objetos?)/i);
|
|
50
|
-
});
|
|
51
39
|
it('keeps every JSON fence in the touched guide parseable', () => {
|
|
52
40
|
for (const json of jsonFences(read(GUIDE))) {
|
|
53
41
|
expect(() => JSON.parse(json)).not.toThrow();
|
|
@@ -0,0 +1,94 @@
|
|
|
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 index_1 = require("../../src/index");
|
|
9
|
+
/**
|
|
10
|
+
* The CLI reference is what agents read to drive `awm`, so it must never name a
|
|
11
|
+
* command or a flag the CLI does not have. This is the one documentation check:
|
|
12
|
+
* it compares the docs against the Commander tree instead of pinning prose, so
|
|
13
|
+
* rewording a paragraph never breaks it, and a renamed or removed flag always does.
|
|
14
|
+
*/
|
|
15
|
+
const ROOT = path_1.default.resolve(__dirname, '../../..');
|
|
16
|
+
const DOCS = ['docs/cli-reference.md', 'README.md'];
|
|
17
|
+
function tree(command) {
|
|
18
|
+
const children = new Map();
|
|
19
|
+
for (const sub of command.commands) {
|
|
20
|
+
const node = tree(sub);
|
|
21
|
+
children.set(sub.name(), node);
|
|
22
|
+
for (const alias of sub.aliases())
|
|
23
|
+
children.set(alias, node);
|
|
24
|
+
}
|
|
25
|
+
return { command, children };
|
|
26
|
+
}
|
|
27
|
+
/** Every `awm ...` invocation inside inline code spans or fenced code blocks. */
|
|
28
|
+
function invocations(markdown) {
|
|
29
|
+
const code = [];
|
|
30
|
+
for (const fence of markdown.matchAll(/```[^\n]*\n([\s\S]*?)```/g))
|
|
31
|
+
code.push(...fence[1].split('\n'));
|
|
32
|
+
const prose = markdown.replace(/```[^\n]*\n[\s\S]*?```/g, '');
|
|
33
|
+
for (const span of prose.matchAll(/`([^`\n]+)`/g))
|
|
34
|
+
code.push(span[1]);
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const line of code) {
|
|
37
|
+
for (const match of line.matchAll(/(?:^|[\s(;&|$])awm\s+([^|;&`#\n]*)/g))
|
|
38
|
+
out.push(match[1].trim());
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function longFlags(command) {
|
|
43
|
+
const flags = new Set(['--help']);
|
|
44
|
+
for (let c = command; c; c = c.parent) {
|
|
45
|
+
for (const option of c.options)
|
|
46
|
+
if (option.long)
|
|
47
|
+
flags.add(option.long);
|
|
48
|
+
}
|
|
49
|
+
return flags;
|
|
50
|
+
}
|
|
51
|
+
function check(root, doc, invocation) {
|
|
52
|
+
const tokens = invocation.split(/\s+/).filter(Boolean);
|
|
53
|
+
const first = tokens[0];
|
|
54
|
+
// `awm <command>`, `awm …`, `awm --version` and similar placeholders name no command.
|
|
55
|
+
if (!first || /^[<[A-Z-]/.test(first) || first === '...' || first === '…')
|
|
56
|
+
return [];
|
|
57
|
+
if (!root.children.has(first))
|
|
58
|
+
return [{ doc, invocation, problem: `unknown command "${first}"` }];
|
|
59
|
+
// Options may precede a subcommand (`awm watch --provider x recover-admission`),
|
|
60
|
+
// so descend on every non-flag token that names a child of the current command.
|
|
61
|
+
let node = root;
|
|
62
|
+
const names = [];
|
|
63
|
+
for (const token of tokens) {
|
|
64
|
+
if (token.startsWith('-'))
|
|
65
|
+
continue;
|
|
66
|
+
const next = node.children.get(token);
|
|
67
|
+
if (next) {
|
|
68
|
+
node = next;
|
|
69
|
+
names.push(token);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const known = longFlags(node.command);
|
|
73
|
+
return tokens
|
|
74
|
+
.flatMap((token) => Array.from(token.matchAll(/(?:^|[[(|])(--[a-z][a-z0-9-]*)/g), (m) => m[1]))
|
|
75
|
+
.filter((flag) => !known.has(flag))
|
|
76
|
+
.map((flag) => ({ doc, invocation, problem: `"awm ${names.join(' ')}" has no ${flag}` }));
|
|
77
|
+
}
|
|
78
|
+
describe('documented awm commands and flags exist in the CLI', () => {
|
|
79
|
+
const root = tree(index_1.program);
|
|
80
|
+
it.each(DOCS)('%s names only real commands and flags', (doc) => {
|
|
81
|
+
const markdown = fs_1.default.readFileSync(path_1.default.join(ROOT, doc), 'utf8');
|
|
82
|
+
const found = invocations(markdown);
|
|
83
|
+
expect(found.length).toBeGreaterThan(0);
|
|
84
|
+
const drift = found.flatMap((invocation) => check(root, doc, invocation));
|
|
85
|
+
expect(drift).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
it('detects drift (self-check against a planted invocation)', () => {
|
|
88
|
+
expect(check(root, 'planted', 'sensors run --no-such-flag')).toEqual([
|
|
89
|
+
{ doc: 'planted', invocation: 'sensors run --no-such-flag', problem: '"awm sensors run" has no --no-such-flag' },
|
|
90
|
+
]);
|
|
91
|
+
expect(check(root, 'planted', 'no-such-command --x')).toHaveLength(1);
|
|
92
|
+
expect(check(root, 'planted', 'sensors run --slice --no-cache')).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -13,14 +13,23 @@ const REPO_ROOT = path_1.default.resolve(CLI_ROOT, '..');
|
|
|
13
13
|
function readWorkflow(name) {
|
|
14
14
|
return fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.github', 'workflows', name), 'utf8').replace(/\r\n/g, '\n');
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
16
|
+
// Windows spawns processes about 3x slower than Linux, so its git-heavy suites run as parallel shards
|
|
17
|
+
// (job `windows-shards`, both Windows runners) while the matrix `test` job runs the Windows remainder.
|
|
18
|
+
const SHARDS = [
|
|
19
|
+
{ name: 'watch', pattern: String.raw `[/\\]cli[/\\]tests[/\\]commands[/\\]watch[/\\]` },
|
|
20
|
+
{ name: 'changes-core', pattern: String.raw `[/\\]cli[/\\]tests[/\\]core[/\\]changes[/\\]` },
|
|
21
|
+
{ name: 'changes-commands', pattern: String.raw `[/\\]cli[/\\]tests[/\\]commands[/\\]changes?[/\\]` },
|
|
22
|
+
];
|
|
23
|
+
const SHARD_JOB = 'windows-shards';
|
|
24
|
+
const WINDOWS_RUNNERS = ['windows-2022', 'windows-11-arm'];
|
|
25
|
+
const REMAINDER_IGNORE = ['node_modules', ...SHARDS.map((shard) => shard.pattern)].join('|');
|
|
26
|
+
const SHARD_STEP = 'Test Windows shard';
|
|
27
|
+
const SHARD_RUN = "npx jest --runInBand --bail --testPathPatterns='${{ matrix.shard.pattern }}'";
|
|
28
|
+
const REGULAR_GUARD = "${{ !startsWith(matrix.target, 'win32-') }}";
|
|
29
|
+
const REMAINDER_GUARD = "startsWith(matrix.target, 'win32-')";
|
|
30
|
+
const REMAINDER_COMMENT = ' # On both Windows runners the git-heavy suites run in the windows-shards job (watch, core Change Record, Change Record commands). Keep runInBand within each job; no test file may run twice or be dropped.\n';
|
|
31
|
+
const RELEASE_GATE = ['test', SHARD_JOB];
|
|
32
|
+
const RELEASE_COMMENT = ' # rojo en cualquier plataforma o shard Windows no publica';
|
|
24
33
|
const SHARD_SETUP = [
|
|
25
34
|
{ uses: 'actions/checkout@v4' },
|
|
26
35
|
{ uses: 'actions/setup-node@v4', with: { 'node-version': '22' } },
|
|
@@ -29,7 +38,7 @@ const SHARD_SETUP = [
|
|
|
29
38
|
{ name: 'Build test-only native addon', 'working-directory': 'cli', run: 'npm run native:test-build' },
|
|
30
39
|
];
|
|
31
40
|
// Jest compiles --testPathPatterns with the `i` flag but --testPathIgnorePatterns
|
|
32
|
-
// without it, so a directory differing only in case would run in both
|
|
41
|
+
// without it, so a directory differing only in case would run in both a shard
|
|
33
42
|
// and the remainder. Returns every directory that falls in that gap.
|
|
34
43
|
function caseOnlyShardPaths(directories, pattern) {
|
|
35
44
|
const insensitive = new RegExp(pattern, 'i');
|
|
@@ -39,11 +48,10 @@ function caseOnlyShardPaths(directories, pattern) {
|
|
|
39
48
|
return insensitive.test(candidate) && !sensitive.test(candidate);
|
|
40
49
|
});
|
|
41
50
|
}
|
|
42
|
-
const SHARD_DIRECTORIES = [['core', 'changes'], ['commands', 'change'], ['commands', 'changes']];
|
|
51
|
+
const SHARD_DIRECTORIES = [['commands', 'watch'], ['core', 'changes'], ['commands', 'change'], ['commands', 'changes']];
|
|
43
52
|
// Each real shard directory must exist with its exact case, hold at least one
|
|
44
|
-
// suite and match
|
|
53
|
+
// suite and match exactly one shard, so the case-only walk cannot pass vacuously.
|
|
45
54
|
function shardDirectoryProblems(testsRoot) {
|
|
46
|
-
const shard = new RegExp(CHANGES_PATTERN);
|
|
47
55
|
const problems = [];
|
|
48
56
|
for (const segments of SHARD_DIRECTORIES) {
|
|
49
57
|
const label = segments.join('/');
|
|
@@ -64,8 +72,8 @@ function shardDirectoryProblems(testsRoot) {
|
|
|
64
72
|
if (!fs_1.default.readdirSync(current).some((entry) => entry.endsWith('.test.ts'))) {
|
|
65
73
|
problems.push(`${label}: holds no *.test.ts`);
|
|
66
74
|
}
|
|
67
|
-
if (
|
|
68
|
-
problems.push(`${label}: does not match
|
|
75
|
+
if (SHARDS.filter((shard) => new RegExp(shard.pattern).test(`${current}${path_1.default.sep}`)).length !== 1) {
|
|
76
|
+
problems.push(`${label}: does not match exactly one shard`);
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
return problems;
|
|
@@ -85,7 +93,7 @@ function keysOf(value) {
|
|
|
85
93
|
return Object.keys(value);
|
|
86
94
|
}
|
|
87
95
|
// The release job publishes only when every test job passed: no if, no
|
|
88
|
-
// continue-on-error, and needs is exactly the
|
|
96
|
+
// continue-on-error, and needs is exactly the gating jobs in any order.
|
|
89
97
|
function assertReleaseGate(jobs) {
|
|
90
98
|
const release = getJob(jobs, 'release');
|
|
91
99
|
expect(keysOf(release)).not.toContain('if');
|
|
@@ -96,99 +104,103 @@ function assertReleaseGate(jobs) {
|
|
|
96
104
|
expect(Array.isArray(release.needs)).toBe(true);
|
|
97
105
|
const needs = release.needs;
|
|
98
106
|
expect(needs).toHaveLength(RELEASE_GATE.length);
|
|
99
|
-
expect([...needs].sort()).toEqual(RELEASE_GATE);
|
|
107
|
+
expect([...needs].sort()).toEqual([...RELEASE_GATE].sort());
|
|
108
|
+
}
|
|
109
|
+
/** Every *.test.ts under cli/tests, as the absolute path Jest matches, in both separators. */
|
|
110
|
+
function suitePaths() {
|
|
111
|
+
const testsRoot = path_1.default.join(CLI_ROOT, 'tests');
|
|
112
|
+
const relative = fs_1.default.readdirSync(testsRoot, { recursive: true }).filter((file) => file.endsWith('.test.ts'));
|
|
113
|
+
expect(relative.length).toBeGreaterThan(100);
|
|
114
|
+
return relative.flatMap((file) => {
|
|
115
|
+
const posix = ['repo', 'cli', 'tests', ...file.split(/[/\\]/)];
|
|
116
|
+
return [posix.join('/'), posix.join('\\')];
|
|
117
|
+
});
|
|
100
118
|
}
|
|
101
119
|
// Gate semantics are asserted on the parsed workflow, so key quoting or escaping
|
|
102
120
|
// 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
|
|
104
|
-
function
|
|
121
|
+
// text is the contract: the run lines and the comments.
|
|
122
|
+
function assertWindowsPartition(source, gatesRelease) {
|
|
105
123
|
const workflow = source.replace(/\r\n/g, '\n');
|
|
106
124
|
const jobs = parseJobs(source);
|
|
107
125
|
const names = keysOf(jobs);
|
|
108
126
|
const test = getJob(jobs, 'test');
|
|
109
|
-
const
|
|
110
|
-
|
|
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);
|
|
127
|
+
const shards = getJob(jobs, SHARD_JOB);
|
|
128
|
+
expect(names.indexOf(SHARD_JOB)).toBe(names.indexOf('test') + 1);
|
|
113
129
|
// No gating job tolerates failure, at job or step level.
|
|
114
|
-
for (const job of [test,
|
|
130
|
+
for (const job of [test, shards]) {
|
|
115
131
|
expect(keysOf(job)).not.toContain('continue-on-error');
|
|
116
132
|
for (const step of job.steps) {
|
|
117
133
|
expect(keysOf(step)).not.toContain('continue-on-error');
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
|
-
// The
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
expect(keysOf(step)).not.toContain('if');
|
|
125
|
-
}
|
|
136
|
+
// The shards never skip, at job or step level.
|
|
137
|
+
expect(keysOf(shards)).not.toContain('if');
|
|
138
|
+
for (const step of shards.steps) {
|
|
139
|
+
expect(keysOf(step)).not.toContain('if');
|
|
126
140
|
}
|
|
127
141
|
// In the matrix, only the two test steps are guarded, with complementary targets.
|
|
128
142
|
const guarded = test.steps.filter((step) => keysOf(step).includes('if'));
|
|
129
143
|
expect(guarded).toHaveLength(2);
|
|
130
144
|
const [regular, remainder] = guarded;
|
|
131
145
|
expect(regular.name).toMatch(/^Tests?$/);
|
|
132
|
-
expect(regular.if).toBe(
|
|
146
|
+
expect(regular.if).toBe(REGULAR_GUARD);
|
|
133
147
|
expect(regular['working-directory']).toBe('cli');
|
|
134
148
|
expect(regular.run).toBe('npx jest --runInBand --bail');
|
|
135
|
-
expect(remainder.name).toMatch(/^Tests? \(Windows
|
|
136
|
-
expect(remainder.if).toBe(
|
|
149
|
+
expect(remainder.name).toMatch(/^Tests? \(Windows remainder\)$/);
|
|
150
|
+
expect(remainder.if).toBe(REMAINDER_GUARD);
|
|
137
151
|
expect(remainder['working-directory']).toBe('cli');
|
|
138
152
|
const ignorePattern = String(remainder.run).match(/^npx jest --runInBand --bail --testPathIgnorePatterns='([^']+)'$/)?.[1];
|
|
139
|
-
expect(ignorePattern).toBe(
|
|
153
|
+
expect(ignorePattern).toBe(REMAINDER_IGNORE);
|
|
140
154
|
expect(test.steps.filter((step) => step.uses === 'actions/upload-artifact@v4')).toHaveLength(1);
|
|
141
|
-
//
|
|
155
|
+
// The shards run on exactly the Windows runners of the matrix (each Windows target runs every shard).
|
|
156
|
+
const matrix = test.strategy.matrix.include;
|
|
157
|
+
const windowsRunners = matrix.filter((entry) => entry.target.startsWith('win32-')).map((entry) => entry.os);
|
|
158
|
+
const shardStrategy = shards.strategy;
|
|
159
|
+
expect(shardStrategy['fail-fast']).toBe(false);
|
|
160
|
+
expect([...shardStrategy.matrix.os].sort()).toEqual([...windowsRunners].sort());
|
|
161
|
+
expect([...windowsRunners].sort()).toEqual([...WINDOWS_RUNNERS].sort());
|
|
162
|
+
expect(keysOf(shardStrategy.matrix).sort()).toEqual(['os', 'shard']);
|
|
163
|
+
expect(shardStrategy.matrix.shard).toEqual(SHARDS);
|
|
164
|
+
expect(shards['runs-on']).toBe('${{ matrix.os }}');
|
|
165
|
+
// Comment directly above the remainder step.
|
|
142
166
|
expect(workflow.split(REMAINDER_COMMENT)).toHaveLength(2);
|
|
143
|
-
const aroundRemainder = workflow.split(/ - name: Tests? \(Windows
|
|
167
|
+
const aroundRemainder = workflow.split(/ - name: Tests? \(Windows remainder\)\n/);
|
|
144
168
|
expect(aroundRemainder).toHaveLength(2);
|
|
145
169
|
expect(aroundRemainder[0].endsWith(REMAINDER_COMMENT)).toBe(true);
|
|
146
|
-
//
|
|
147
|
-
expect(
|
|
148
|
-
expect(
|
|
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) });
|
|
170
|
+
// The shard job builds like the matrix and only then runs its shard; it never produces an artifact.
|
|
171
|
+
expect(shards.steps.slice(0, -1)).toEqual(SHARD_SETUP);
|
|
172
|
+
expect(shards.steps.at(-1)).toEqual({ name: SHARD_STEP, 'working-directory': 'cli', run: SHARD_RUN });
|
|
152
173
|
if (gatesRelease) {
|
|
153
|
-
expect(
|
|
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');
|
|
174
|
+
expect(shards.permissions).toEqual({ contents: 'read' });
|
|
158
175
|
}
|
|
159
|
-
|
|
160
|
-
expect(
|
|
161
|
-
expect(
|
|
162
|
-
// The
|
|
176
|
+
expect(shards.steps.some((step) => String(step.uses ?? '').includes('upload-artifact'))).toBe(false);
|
|
177
|
+
expect(JSON.stringify(shards)).not.toContain('secure-fs-win32');
|
|
178
|
+
expect(workflow.split('\n')).toContain(` run: ${SHARD_RUN}`);
|
|
179
|
+
// The partition, checked on every real suite: on Windows each file runs in exactly one place.
|
|
163
180
|
const ignored = new RegExp(ignorePattern);
|
|
181
|
+
const selectors = shardStrategy.matrix.shard.map((shard) => new RegExp(shard.pattern, 'i'));
|
|
182
|
+
const misplaced = suitePaths().filter((file) => {
|
|
183
|
+
const places = selectors.filter((selector) => selector.test(file)).length + (ignored.test(file) ? 0 : 1);
|
|
184
|
+
return places !== 1;
|
|
185
|
+
});
|
|
186
|
+
expect(misplaced).toEqual([]);
|
|
164
187
|
for (const separator of ['/', '\\']) {
|
|
165
188
|
const root = ['repo', 'cli', 'tests'].join(separator);
|
|
166
|
-
for (const
|
|
167
|
-
expect(ignored.test([root, 'commands', 'watch', `${suite}.test.ts`].join(separator))).toBe(true);
|
|
168
|
-
expect(ignored.test([root, 'other', `${suite}.test.ts`].join(separator))).toBe(false);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
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']]) {
|
|
189
|
+
for (const file of [['commands', 'watch', 'track-finalize.test.ts'], ['core', 'changes', 'x.test.ts'], ['commands', 'change', 'new.test.ts'], ['commands', 'changes', 'list.test.ts']]) {
|
|
178
190
|
const candidate = [root, ...file].join(separator);
|
|
179
|
-
expect(
|
|
191
|
+
expect(selectors.filter((selector) => selector.test(candidate))).toHaveLength(1);
|
|
180
192
|
expect(ignored.test(candidate)).toBe(true);
|
|
181
193
|
}
|
|
182
|
-
for (const file of [['core', 'changesets', 'x.test.ts'], ['commands', 'changesets', 'x.test.ts'], ['commands', 'watch', '
|
|
183
|
-
|
|
194
|
+
for (const file of [['core', 'changesets', 'x.test.ts'], ['commands', 'changesets', 'x.test.ts'], ['commands', 'watcher', 'x.test.ts'], ['core', 'watch', 'x.test.ts']]) {
|
|
195
|
+
const candidate = [root, ...file].join(separator);
|
|
196
|
+
expect(selectors.some((selector) => selector.test(candidate))).toBe(false);
|
|
197
|
+
expect(ignored.test(candidate)).toBe(false);
|
|
184
198
|
}
|
|
185
|
-
expect(ignored.test([root, 'core', 'changesets', 'x.test.ts'].join(separator))).toBe(false);
|
|
186
199
|
}
|
|
187
200
|
if (gatesRelease) {
|
|
188
201
|
assertReleaseGate(jobs);
|
|
189
|
-
// CI-2 keeps the comment on the release job's needs line.
|
|
190
202
|
const releaseText = workflow.slice(workflow.search(/^ release:\n/m));
|
|
191
|
-
expect(releaseText.match(/^ needs: [^\n]*$/m)?.[0]).toMatch(
|
|
203
|
+
expect(releaseText.match(/^ needs: [^\n]*$/m)?.[0]).toMatch(new RegExp(`${RELEASE_COMMENT}$`));
|
|
192
204
|
}
|
|
193
205
|
else {
|
|
194
206
|
expect(names).not.toContain('release');
|
|
@@ -207,60 +219,55 @@ function permutations(items) {
|
|
|
207
219
|
return [items];
|
|
208
220
|
return items.flatMap((item, index) => permutations([...items.slice(0, index), ...items.slice(index + 1)]).map((rest) => [item, ...rest]));
|
|
209
221
|
}
|
|
210
|
-
// Every bypass of the partition or the release gate must make
|
|
222
|
+
// Every bypass of the partition or the release gate must make assertWindowsPartition
|
|
211
223
|
// throw: a failed assertion or a mutated text that no longer parses as YAML.
|
|
212
224
|
function assertMutationsRejected(workflow, gates) {
|
|
213
225
|
const jobLevel = (job, line) => workflow.replace(new RegExp(`^ ${job}:\\n`, 'm'), ` ${job}:\n ${line}\n`);
|
|
214
226
|
const inStep = (step, line) => workflow.replace(` - name: ${step}\n`, ` - name: ${step}\n ${line}\n`);
|
|
215
227
|
const mutations = [
|
|
216
228
|
// Swapped matrix guards.
|
|
217
|
-
workflow.replace(
|
|
229
|
+
workflow.replace(REGULAR_GUARD, 'PLACEHOLDER').replace(`if: ${REMAINDER_GUARD}`, `if: ${REGULAR_GUARD}`).replace('PLACEHOLDER', REMAINDER_GUARD),
|
|
218
230
|
// A second producer of the native artifact.
|
|
219
231
|
workflow.replace('uses: actions/upload-artifact@v4', 'uses: actions/upload-artifact@v4\n - uses: actions/upload-artifact@v4'),
|
|
220
232
|
// A remainder that drops more than the shards run.
|
|
221
233
|
workflow.replace(/(--testPathIgnorePatterns='[^']+)(?=')/, '$1|runner\\.test\\.ts$'),
|
|
222
|
-
// A shard pattern that
|
|
223
|
-
workflow.replace(/(
|
|
224
|
-
//
|
|
225
|
-
workflow.replace(/
|
|
234
|
+
// A shard pattern that drifts from the remainder's exclusion.
|
|
235
|
+
workflow.replace(/(pattern: '[^']+commands\[\/\\\\\]changes)\?/, '$1'),
|
|
236
|
+
// A dropped shard, and a dropped Windows runner.
|
|
237
|
+
workflow.replace(/ - name: changes-core\n pattern: '[^']+'\n/, ''),
|
|
238
|
+
workflow.replace('os: [windows-2022, windows-11-arm]', 'os: [windows-11-arm]'),
|
|
239
|
+
// Fail-fast would cancel sibling shards and hide their results.
|
|
240
|
+
workflow.replace(/( windows-shards:\n strategy:\n fail-fast: )false/, '$1true'),
|
|
241
|
+
// Masked shard run.
|
|
226
242
|
workflow.replace(/(run: npx jest --runInBand --bail --testPathPatterns='[^']+')(?=\n)/, '$1; exit 0'),
|
|
227
243
|
// Skipped steps or jobs, in any key quoting.
|
|
228
|
-
inStep(
|
|
229
|
-
inStep(
|
|
230
|
-
inStep(
|
|
231
|
-
inStep(CHANGES_STEP, 'if: false'),
|
|
232
|
-
inStep(CHANGES_STEP, '"if": always()'),
|
|
233
|
-
inStep(CHANGES_STEP, "'if': false"),
|
|
244
|
+
inStep(SHARD_STEP, 'if: false'),
|
|
245
|
+
inStep(SHARD_STEP, '"if": always()'),
|
|
246
|
+
inStep(SHARD_STEP, "'if': false"),
|
|
234
247
|
inStep('Upload native secure-fs artifact', "'if': false"),
|
|
235
|
-
jobLevel(
|
|
236
|
-
jobLevel(
|
|
237
|
-
jobLevel('windows-arm-changes', '"if": always()'),
|
|
238
|
-
jobLevel('windows-arm-changes', "'if': false"),
|
|
248
|
+
jobLevel(SHARD_JOB, '"if": always()'),
|
|
249
|
+
jobLevel(SHARD_JOB, "'if': false"),
|
|
239
250
|
// Tolerated failures, in any key quoting, escaping or expression form.
|
|
240
|
-
jobLevel('test', "continue-on-error: ${{ matrix.target
|
|
251
|
+
jobLevel('test', "continue-on-error: ${{ startsWith(matrix.target, 'win32-') }}"),
|
|
241
252
|
jobLevel('test', '"continue-on-error": true'),
|
|
242
253
|
jobLevel('test', '"continue\\x2Don-error": true'),
|
|
243
|
-
jobLevel(
|
|
244
|
-
jobLevel(
|
|
245
|
-
jobLevel(
|
|
246
|
-
|
|
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"),
|
|
254
|
+
jobLevel(SHARD_JOB, 'continue-on-error: true'),
|
|
255
|
+
jobLevel(SHARD_JOB, 'continue-on-error: ${{ true }}'),
|
|
256
|
+
jobLevel(SHARD_JOB, '"continue-on-error": true'),
|
|
257
|
+
inStep(SHARD_STEP, '"continue-on-error": true'),
|
|
251
258
|
workflow.replace(/^( run: npx jest --runInBand --bail\n)/m, '$1 continue-on-error: true\n'),
|
|
252
259
|
workflow.replace(/(run: npx jest --runInBand --bail --testPathIgnorePatterns='[^']+'\n)/, '$1 continue-on-error: true\n'),
|
|
253
260
|
workflow.replace(/(run: npx jest --runInBand --bail --testPathIgnorePatterns='[^']+'\n)/, '$1 continue-on-error: ${{ true }}\n'),
|
|
254
261
|
];
|
|
255
262
|
if (gates) {
|
|
256
|
-
mutations.push(rewriteReleaseNeeds(workflow, (jobs) => jobs.filter((job) => job !==
|
|
263
|
+
mutations.push(rewriteReleaseNeeds(workflow, (jobs) => jobs.filter((job) => job !== SHARD_JOB)), rewriteReleaseNeeds(workflow, (jobs) => [...jobs, SHARD_JOB]), 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
264
|
// The needs comment is part of CI-2.
|
|
258
265
|
workflow.slice(0, workflow.search(/^ release:\n/m))
|
|
259
|
-
+ workflow.slice(workflow.search(/^ release:\n/m)).replace(
|
|
266
|
+
+ workflow.slice(workflow.search(/^ release:\n/m)).replace(new RegExp(`^( needs: [^\\n]*?)${RELEASE_COMMENT}$`, 'm'), '$1'));
|
|
260
267
|
}
|
|
261
268
|
for (const mutated of mutations) {
|
|
262
269
|
expect(mutated).not.toBe(workflow);
|
|
263
|
-
expect(() =>
|
|
270
|
+
expect(() => assertWindowsPartition(mutated, gates)).toThrow();
|
|
264
271
|
}
|
|
265
272
|
}
|
|
266
273
|
describe('native release artifacts', () => {
|
|
@@ -278,19 +285,19 @@ describe('native release artifacts', () => {
|
|
|
278
285
|
}
|
|
279
286
|
expect(release).toContain('cancel-in-progress: false');
|
|
280
287
|
});
|
|
281
|
-
it('partitions the
|
|
288
|
+
it('partitions the Windows shards and remainder without duplicating the native artifact', () => {
|
|
282
289
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
283
290
|
const workflow = readWorkflow(name);
|
|
284
|
-
|
|
291
|
+
assertWindowsPartition(workflow, name === 'release.yml');
|
|
285
292
|
}
|
|
286
293
|
});
|
|
287
294
|
it('checks the same partition after a Windows CRLF checkout', () => {
|
|
288
295
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
289
296
|
const workflow = readWorkflow(name);
|
|
290
|
-
|
|
297
|
+
assertWindowsPartition(workflow.replace(/\n/g, '\r\n'), name === 'release.yml');
|
|
291
298
|
}
|
|
292
299
|
});
|
|
293
|
-
it('rejects swapped matrix guards, non-gating
|
|
300
|
+
it('rejects swapped matrix guards, non-gating Windows shards, drifted partitions and duplicate artifacts', () => {
|
|
294
301
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
295
302
|
assertMutationsRejected(readWorkflow(name), name === 'release.yml');
|
|
296
303
|
}
|
|
@@ -303,11 +310,11 @@ describe('native release artifacts', () => {
|
|
|
303
310
|
expect(reorderedJobs).toBeDefined();
|
|
304
311
|
const reordered = rewriteReleaseNeeds(workflow, () => reorderedJobs);
|
|
305
312
|
expect(reordered).not.toBe(workflow);
|
|
306
|
-
expect(() =>
|
|
313
|
+
expect(() => assertWindowsPartition(reordered, true)).not.toThrow();
|
|
307
314
|
expect(() => assertMutationsRejected(reordered, true)).not.toThrow();
|
|
308
315
|
});
|
|
309
316
|
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-
|
|
317
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-windows-shard-'));
|
|
311
318
|
try {
|
|
312
319
|
const testsRoot = path_1.default.join(tmp, 'cli', 'tests');
|
|
313
320
|
for (const segments of SHARD_DIRECTORIES) {
|
|
@@ -328,9 +335,11 @@ describe('native release artifacts', () => {
|
|
|
328
335
|
}
|
|
329
336
|
});
|
|
330
337
|
it('flags test directories that only the case-insensitive shard pattern selects', () => {
|
|
331
|
-
|
|
332
|
-
expect(caseOnlyShardPaths(['repo
|
|
333
|
-
expect(caseOnlyShardPaths(['repo
|
|
338
|
+
const [watch, core, commands] = SHARDS.map((shard) => shard.pattern);
|
|
339
|
+
expect(caseOnlyShardPaths(['repo/cli/tests/core/Changes'], core)).toEqual(['repo/cli/tests/core/Changes']);
|
|
340
|
+
expect(caseOnlyShardPaths(['repo\\cli\\tests\\commands\\CHANGE'], commands)).toEqual(['repo\\cli\\tests\\commands\\CHANGE']);
|
|
341
|
+
expect(caseOnlyShardPaths(['repo/cli/tests/commands/Watch'], watch)).toEqual(['repo/cli/tests/commands/Watch']);
|
|
342
|
+
expect(caseOnlyShardPaths(['repo/cli/tests/core/changes', 'repo/cli/tests/commands/change', 'repo/cli/tests/commands/watch', 'repo/cli/tests/core/Changesets'], core)).toEqual([]);
|
|
334
343
|
});
|
|
335
344
|
it('has no test directory selected by the shard but kept by the remainder through case alone', () => {
|
|
336
345
|
const testsRoot = path_1.default.join(CLI_ROOT, 'tests');
|
|
@@ -338,9 +347,9 @@ describe('native release artifacts', () => {
|
|
|
338
347
|
.map((relative) => path_1.default.join(testsRoot, relative))
|
|
339
348
|
.filter((candidate) => fs_1.default.statSync(candidate).isDirectory());
|
|
340
349
|
expect(shardDirectoryProblems(testsRoot)).toEqual([]);
|
|
341
|
-
const flagged = caseOnlyShardPaths(directories,
|
|
350
|
+
const flagged = SHARDS.flatMap((shard) => caseOnlyShardPaths(directories, shard.pattern));
|
|
342
351
|
if (flagged.length > 0) {
|
|
343
|
-
throw new Error(`Case-only
|
|
352
|
+
throw new Error(`Case-only directories would run in both a Windows shard and the remainder: ${flagged.join(', ')}`);
|
|
344
353
|
}
|
|
345
354
|
});
|
|
346
355
|
it('keeps native prebuilds generated instead of versioned source', () => {
|