agentic-workflow-manager 8.1.1 → 8.1.3
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/scripts/sensor-support-matrix.js +2 -2
- package/dist/src/commands/sensors/compatibility/contract.js +3 -2
- package/dist/src/commands/sensors/compatibility/discovery.js +2 -1
- package/dist/src/commands/sensors/compatibility/live.js +1 -0
- package/dist/src/commands/sensors/compatibility/probe.js +6 -1
- package/dist/src/commands/sensors/compatibility/resolve.js +14 -1
- package/dist/tests/commands/sensors/compatibility/contract.test.js +5 -3
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +2 -1
- package/dist/tests/commands/sensors/compatibility/probe.test.js +9 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +26 -0
- package/dist/tests/commands/sensors/init.test.js +1 -0
- package/dist/tests/commands/watch/runner.test.js +25 -1
- package/dist/tests/integration/published-r3-acceptance.e2e.test.js +118 -0
- package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +67 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +5 -5
- package/package.json +2 -2
|
@@ -30,8 +30,8 @@ exports.R3_PREPUBLICATION_FIXTURE_RELATIVE_PATH = 'tests/fixtures/sensor-support
|
|
|
30
30
|
* never an invented description of an unpublished registry release.
|
|
31
31
|
*/
|
|
32
32
|
exports.R3_PREPUBLICATION_FIXTURE_PURPOSE = 'R3 pre-publication contract fixture';
|
|
33
|
-
exports.R3_PUBLISHED_REGISTRY_TAG = 'v2.0.
|
|
34
|
-
exports.R3_PUBLISHED_REGISTRY_COMMIT = '
|
|
33
|
+
exports.R3_PUBLISHED_REGISTRY_TAG = 'v2.0.1';
|
|
34
|
+
exports.R3_PUBLISHED_REGISTRY_COMMIT = '6f40632006fc65300ac633c5a54f2635cf0eb8e9';
|
|
35
35
|
/**
|
|
36
36
|
* Extract the registry-owned certification matrix without copying or rewording it.
|
|
37
37
|
* `SUPPORT.md` is generated by the registry from its manifests and frozen tool pins;
|
|
@@ -206,7 +206,7 @@ function parseVariant(input, source, location) {
|
|
|
206
206
|
invalid(source, `${location} requires requirements and probe when policyRef is absent`);
|
|
207
207
|
const requirements = policy ? undefined : record(value.requirements, source, `${location}.requirements`);
|
|
208
208
|
if (requirements)
|
|
209
|
-
fields(requirements, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'configFiles'], source, `${location}.requirements`);
|
|
209
|
+
fields(requirements, ['tool', 'toolRange', 'runtime', 'runtimeRange', 'configFiles', 'packageJsonFields'], source, `${location}.requirements`);
|
|
210
210
|
const toolRange = policy?.toolRange ?? text(requirements.toolRange, source, `${location}.requirements.toolRange`);
|
|
211
211
|
const runtimeRange = policy?.runtimeRange ?? text(requirements.runtimeRange, source, `${location}.requirements.runtimeRange`);
|
|
212
212
|
if (semver_1.default.validRange(toolRange) === null || semver_1.default.validRange(runtimeRange) === null)
|
|
@@ -223,7 +223,8 @@ function parseVariant(input, source, location) {
|
|
|
223
223
|
certifiedRange,
|
|
224
224
|
requirements: policy
|
|
225
225
|
? { tool: policy.tool, toolRange, runtime: policy.runtime, runtimeRange }
|
|
226
|
-
: { tool: text(requirements.tool, source, `${location}.requirements.tool`), toolRange, runtime: text(requirements.runtime, source, `${location}.requirements.runtime`), runtimeRange, ...('configFiles' in requirements ? { configFiles: stringArray(requirements.configFiles, source, `${location}.requirements.configFiles`).map((file, index) => asset(file, source, `${location}.requirements.configFiles[${index}]`)) } : {}) },
|
|
226
|
+
: { tool: text(requirements.tool, source, `${location}.requirements.tool`), toolRange, runtime: text(requirements.runtime, source, `${location}.requirements.runtime`), runtimeRange, ...('configFiles' in requirements ? { configFiles: stringArray(requirements.configFiles, source, `${location}.requirements.configFiles`).map((file, index) => asset(file, source, `${location}.requirements.configFiles[${index}]`)) } : {}), ...('packageJsonFields' in requirements ? { packageJsonFields: stringArray(requirements.packageJsonFields, source, `${location}.requirements.packageJsonFields`).map((field, index) => { if (!/^[A-Za-z][A-Za-z0-9]*$/.test(field))
|
|
227
|
+
invalid(source, `${location}.requirements.packageJsonFields[${index}] must be a stable package.json field`); return field; }) } : {}) },
|
|
227
228
|
assets: assetArray(value.assets, source, `${location}.assets`, true),
|
|
228
229
|
formatter: text(value.formatter, source, `${location}.formatter`),
|
|
229
230
|
probe: { kind: policy?.probe ?? probe.kind },
|
|
@@ -190,6 +190,7 @@ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
|
|
|
190
190
|
}
|
|
191
191
|
const configFiles = [...configCandidates].filter(file => safeFile(root, file)).sort();
|
|
192
192
|
const scripts = Object.keys(stringMap(pkg?.scripts)).sort();
|
|
193
|
+
const packageJsonFields = Object.keys(pkg ?? {}).filter(field => /^[A-Za-z][A-Za-z0-9]*$/.test(field)).sort();
|
|
193
194
|
const declaredToolRanges = { ...stringMap(pkg?.dependencies), ...stringMap(pkg?.devDependencies), ...stringMap(pkg?.peerDependencies) };
|
|
194
195
|
const tools = new Set();
|
|
195
196
|
if ('schemaVersion' in pack)
|
|
@@ -202,6 +203,6 @@ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
|
|
|
202
203
|
return {
|
|
203
204
|
cwd: root, os: targetPlatform, runtimeVersions: { node: process.versions.node ?? null, ...(environment ? { python: environment.runtimeVersion } : {}) }, pythonEnvironmentRoot: environment?.rootParts[0] ?? null, declaredToolRanges, toolVersions,
|
|
204
205
|
packageManager: declaredManager ?? (lockManagers.size === 1 ? [...lockManagers][0] : null), packageManagerConflict: lockManagers.size > 1,
|
|
205
|
-
scripts, configFiles, paths: [...new Set([...(safeFile(root, 'package.json') ? ['package.json'] : []), ...locks, ...configFiles])].sort(),
|
|
206
|
+
scripts, configFiles, packageJsonFields, paths: [...new Set([...(safeFile(root, 'package.json') ? ['package.json'] : []), ...locks, ...configFiles])].sort(),
|
|
206
207
|
};
|
|
207
208
|
}
|
|
@@ -82,6 +82,7 @@ async function resolveParsedPackCompatibility(cwd, pack, options = {}) {
|
|
|
82
82
|
toolExecutable: variant.command.executable,
|
|
83
83
|
toolResolution: variant.command.resolution,
|
|
84
84
|
pythonEnvironmentRoot: variant.command.pythonEnvironmentRoot,
|
|
85
|
+
environment: variant.command.environment,
|
|
85
86
|
configFiles: evidence.configFiles,
|
|
86
87
|
scripts: evidence.scripts,
|
|
87
88
|
})
|
|
@@ -16,7 +16,12 @@ function commandFor(kind, evidence) {
|
|
|
16
16
|
return null;
|
|
17
17
|
if (kind === 'config-present')
|
|
18
18
|
return null;
|
|
19
|
-
const command = {
|
|
19
|
+
const command = {
|
|
20
|
+
executable,
|
|
21
|
+
resolution,
|
|
22
|
+
...(resolution === 'python-environment' && evidence.pythonEnvironmentRoot ? { pythonEnvironmentRoot: evidence.pythonEnvironmentRoot } : {}),
|
|
23
|
+
...(evidence.environment ? { environment: evidence.environment } : {}),
|
|
24
|
+
};
|
|
20
25
|
if (kind === 'version')
|
|
21
26
|
return { ...command, args: ['--version'] };
|
|
22
27
|
if (kind === 'eslint-print-config')
|
|
@@ -45,6 +45,17 @@ function toolFor(variant, evidence) {
|
|
|
45
45
|
function runtimeFor(variant, evidence) {
|
|
46
46
|
return evidence.runtimeVersions === undefined ? evidence.runtimeVersion : evidence.runtimeVersions[variant.requirements.runtime];
|
|
47
47
|
}
|
|
48
|
+
/** A variant may name alternative native configuration files that make it
|
|
49
|
+
* applicable. They are alternatives (for example `.eslintrc.*`), not a list
|
|
50
|
+
* that a project must contain in full. A variant without this requirement is
|
|
51
|
+
* configuration-agnostic. */
|
|
52
|
+
function hasRequiredConfig(variant, evidence) {
|
|
53
|
+
const requiredFiles = variant.requirements.configFiles ?? [];
|
|
54
|
+
const requiredFields = variant.requirements.packageJsonFields ?? [];
|
|
55
|
+
return (requiredFiles.length === 0 && requiredFields.length === 0)
|
|
56
|
+
|| requiredFiles.some(file => evidence.paths?.includes(file))
|
|
57
|
+
|| requiredFields.some(field => evidence.packageJsonFields?.includes(field));
|
|
58
|
+
}
|
|
48
59
|
/** Pure precedence resolver. It consumes discovered evidence and neither probes nor executes commands. */
|
|
49
60
|
function resolveSensorCompatibility(sensor, evidence, context) {
|
|
50
61
|
if (!sensor || typeof sensor !== 'object' || !evidence || typeof evidence !== 'object')
|
|
@@ -66,7 +77,9 @@ function resolveSensorCompatibility(sensor, evidence, context) {
|
|
|
66
77
|
const operational = v2.variants.filter(variant => validVersion(toolFor(variant, evidence)) && validVersion(runtimeFor(variant, evidence)));
|
|
67
78
|
if (operational.length === 0)
|
|
68
79
|
return result('unverifiable', 'invalid-or-missing-version-evidence', null, evidence);
|
|
69
|
-
const matches = operational.filter(variant => semver_1.default.satisfies(toolFor(variant, evidence), variant.requirements.toolRange)
|
|
80
|
+
const matches = operational.filter(variant => semver_1.default.satisfies(toolFor(variant, evidence), variant.requirements.toolRange)
|
|
81
|
+
&& semver_1.default.satisfies(runtimeFor(variant, evidence), variant.requirements.runtimeRange)
|
|
82
|
+
&& hasRequiredConfig(variant, evidence));
|
|
70
83
|
if (matches.length === 0)
|
|
71
84
|
return result('incompatible', 'no-operational-variant', null, evidence);
|
|
72
85
|
matches.sort((a, b) => b.priority - a.priority || specificity(b) - specificity(a) || a.id.localeCompare(b.id));
|
|
@@ -150,9 +150,11 @@ describe('sensor pack v2 contract', () => {
|
|
|
150
150
|
expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { applicability: { allFiles: [3] }, variants: [variant] } } }, 'pack')).toThrow('allFiles[0]');
|
|
151
151
|
expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...variant, probe: { kind: 'version', extra: true } }] } } }, 'pack')).toThrow('unknown field');
|
|
152
152
|
});
|
|
153
|
-
it('preserves
|
|
154
|
-
const variant = { ...validPack().sensors.lint.variants[0], requirements: { ...validPack().sensors.lint.variants[0].requirements, configFiles: ['eslint.config.js'] } };
|
|
155
|
-
expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [variant] } } }, 'pack')).toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ requirements: { configFiles: ['eslint.config.js'] } }] } } } });
|
|
153
|
+
it('preserves config selectors and validates package.json field names', () => {
|
|
154
|
+
const variant = { ...validPack().sensors.lint.variants[0], requirements: { ...validPack().sensors.lint.variants[0].requirements, configFiles: ['eslint.config.js'], packageJsonFields: ['eslintConfig'] } };
|
|
155
|
+
expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [variant] } } }, 'pack')).toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ requirements: { configFiles: ['eslint.config.js'], packageJsonFields: ['eslintConfig'] } }] } } } });
|
|
156
|
+
const malformed = { ...variant, requirements: { ...variant.requirements, packageJsonFields: ['eslint-config'] } };
|
|
157
|
+
expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [malformed] } } }, 'pack')).toThrow('packageJsonFields[0]');
|
|
156
158
|
const complete = validPack().sensors.lint.variants[0];
|
|
157
159
|
expect(() => (0, contract_1.assertNoEqualPriorityOverlap)([{ ...complete, id: '../bad' }])).toThrow('stable lowercase id');
|
|
158
160
|
expect(() => (0, contract_1.assertNoEqualPriorityOverlap)([{ ...complete, command: undefined }])).toThrow('command');
|
|
@@ -11,7 +11,7 @@ describe('discoverProjectEvidence', () => {
|
|
|
11
11
|
it('returns only local, relative project evidence and detects conflicting lockfiles', () => {
|
|
12
12
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-discovery-'));
|
|
13
13
|
try {
|
|
14
|
-
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '10.0.0' } }));
|
|
14
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, eslintConfig: { env: { node: true } }, devDependencies: { eslint: '10.0.0' } }));
|
|
15
15
|
fs_1.default.writeFileSync(path_1.default.join(root, 'package-lock.json'), '{}');
|
|
16
16
|
fs_1.default.writeFileSync(path_1.default.join(root, 'pnpm-lock.yaml'), 'lockfileVersion: 9');
|
|
17
17
|
fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'export default []');
|
|
@@ -24,6 +24,7 @@ describe('discoverProjectEvidence', () => {
|
|
|
24
24
|
expect(evidence.toolVersions.eslint).toBe('10.4.1');
|
|
25
25
|
expect(evidence.scripts).toContain('lint');
|
|
26
26
|
expect(evidence.configFiles).toContain('eslint.config.js');
|
|
27
|
+
expect(evidence.packageJsonFields).toContain('eslintConfig');
|
|
27
28
|
expect(evidence.paths.every((item) => !path_1.default.isAbsolute(item) && !item.includes('..'))).toBe(true);
|
|
28
29
|
}
|
|
29
30
|
finally {
|
|
@@ -20,6 +20,15 @@ describe('runCompatibilityProbe', () => {
|
|
|
20
20
|
await (0, probe_1.runCompatibilityProbe)({ kind: 'eslint-print-config' }, evidence, fakeExecutor);
|
|
21
21
|
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: 'eslint', resolution: 'node-modules-bin' }), expect.any(Object));
|
|
22
22
|
});
|
|
23
|
+
it('propagates the selected ESLint mode to its compatibility probe', async () => {
|
|
24
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'eslint-print-config' }, {
|
|
25
|
+
...evidence,
|
|
26
|
+
toolExecutable: 'eslint',
|
|
27
|
+
toolResolution: 'node-modules-bin',
|
|
28
|
+
environment: { ESLINT_USE_FLAT_CONFIG: 'true' },
|
|
29
|
+
}, fakeExecutor);
|
|
30
|
+
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ environment: { ESLINT_USE_FLAT_CONFIG: 'true' } }), expect.any(Object));
|
|
31
|
+
});
|
|
23
32
|
it('binds Semgrep validation to the contained Python environment', async () => {
|
|
24
33
|
await (0, probe_1.runCompatibilityProbe)({ kind: 'semgrep-validate' }, evidence, fakeExecutor);
|
|
25
34
|
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: 'semgrep', resolution: 'python-environment' }), expect.any(Object));
|
|
@@ -50,6 +50,32 @@ describe('resolveSensorCompatibility', () => {
|
|
|
50
50
|
const resolved = (0, resolve_1.resolveSensorCompatibility)({ applicability: { allFiles: ['package.json'] }, variants }, evidence({ toolVersions: { eslint: '10.4.1', biome: null }, runtimeVersions: { node: '22.0.0' }, toolVersion: '8.0.0', runtimeVersion: '8.0.0' }), context);
|
|
51
51
|
expect(resolved).toMatchObject({ state: 'certified', variantId: 'eslint', toolVersion: '10.4.1', runtimeVersion: '22.0.0' });
|
|
52
52
|
});
|
|
53
|
+
it('selects the ESLint 8 flat variant from native config evidence before priority', () => {
|
|
54
|
+
const eslintrc = {
|
|
55
|
+
...variant('eslint-8-eslintrc', 40, '>=8 <9', '=8.57.1'),
|
|
56
|
+
requirements: { ...variant('eslint-8-eslintrc').requirements, toolRange: '>=8 <9', configFiles: ['.eslintrc.json'] },
|
|
57
|
+
};
|
|
58
|
+
const flat = {
|
|
59
|
+
...variant('eslint-8-flat', 30, '>=8 <9', '=8.57.1'),
|
|
60
|
+
requirements: { ...variant('eslint-8-flat').requirements, toolRange: '>=8 <9', configFiles: ['eslint.config.mjs'] },
|
|
61
|
+
};
|
|
62
|
+
const resolved = (0, resolve_1.resolveSensorCompatibility)({ applicability: { allFiles: ['package.json'] }, variants: [eslintrc, flat] }, evidence({ toolVersion: '8.57.1', paths: ['package.json', 'eslint.config.mjs'] }), context);
|
|
63
|
+
expect(resolved).toMatchObject({ state: 'certified', variantId: 'eslint-8-flat' });
|
|
64
|
+
});
|
|
65
|
+
it('selects the ESLint 8 eslintrc variant from package.json eslintConfig evidence', () => {
|
|
66
|
+
const eslintrc = {
|
|
67
|
+
...variant('eslint-8-eslintrc', 40, '>=8 <9', '=8.57.1'),
|
|
68
|
+
requirements: { ...variant('eslint-8-eslintrc').requirements, toolRange: '>=8 <9', packageJsonFields: ['eslintConfig'] },
|
|
69
|
+
};
|
|
70
|
+
const flat = {
|
|
71
|
+
...variant('eslint-8-flat', 30, '>=8 <9', '=8.57.1'),
|
|
72
|
+
requirements: { ...variant('eslint-8-flat').requirements, toolRange: '>=8 <9', configFiles: ['eslint.config.mjs'] },
|
|
73
|
+
};
|
|
74
|
+
const resolved = (0, resolve_1.resolveSensorCompatibility)({ applicability: { allFiles: ['package.json'] }, variants: [eslintrc, flat] }, evidence({ toolVersion: '8.57.1', paths: ['package.json'], packageJsonFields: ['eslintConfig'] }), context);
|
|
75
|
+
expect(resolved).toMatchObject({ state: 'certified', variantId: 'eslint-8-eslintrc' });
|
|
76
|
+
expect((0, resolve_1.resolveSensorCompatibility)({ applicability: { allFiles: ['package.json'] }, variants: [eslintrc, flat] }, evidence({ toolVersion: '8.57.1', paths: ['package.json'], packageJsonFields: [] }), context))
|
|
77
|
+
.toMatchObject({ state: 'incompatible', variantId: null });
|
|
78
|
+
});
|
|
53
79
|
it('does not reuse scalar evidence for a missing key in a version map', () => {
|
|
54
80
|
const biome = { ...variant('biome'), requirements: { ...variant('biome').requirements, tool: 'biome', runtime: 'bun' } };
|
|
55
81
|
expect((0, resolve_1.resolveSensorCompatibility)({ applicability: { allFiles: ['package.json'] }, variants: [biome] }, evidence({ toolVersions: { biome: null }, runtimeVersions: { bun: null }, toolVersion: '10.4.1', runtimeVersion: '22.0.0' }), context))
|
|
@@ -305,6 +305,7 @@ describe('initSensors', () => {
|
|
|
305
305
|
const automatic = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry, configure: false });
|
|
306
306
|
expect(automatic.manifest).toMatchObject({ schemaVersion: 2, pack: 'generic', sensors: {} });
|
|
307
307
|
expect(automatic.manifest.packSelection).toBeUndefined();
|
|
308
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'generic.config'), 'fixture\n');
|
|
308
309
|
const explicit = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry, pack: 'generic' });
|
|
309
310
|
const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8'));
|
|
310
311
|
expect(explicit.manifest).toMatchObject({ schemaVersion: 2, pack: 'generic', packSelection: 'explicit', sensors: { security: { variantId: 'eslint-10' } } });
|
|
@@ -90,7 +90,28 @@ describe('runner concurrente', () => {
|
|
|
90
90
|
expect(done.wrapperRef.pid).toBeGreaterThan(0);
|
|
91
91
|
});
|
|
92
92
|
test('job largo pasa por running con identidad real; jamas se mata por duracion (R3.5)', async () => {
|
|
93
|
-
|
|
93
|
+
// El hijo vive hasta que EL TEST lo libera, no hasta que se cumple un reloj.
|
|
94
|
+
//
|
|
95
|
+
// Con una duracion fija (era 1500 ms) esto era una carrera imposible de ganar:
|
|
96
|
+
// el test espera OBSERVAR el estado transitorio `running`, y en win32 cada
|
|
97
|
+
// consulta de vitalidad spawnea tasklist/WMI — cientos de ms bajo carga, el mismo
|
|
98
|
+
// costo que documenta el `jest.setTimeout` de arriba. Si la primera observacion
|
|
99
|
+
// llegaba despues de que el hijo ya habia muerto, el estado saltaba directo a
|
|
100
|
+
// `exited` y `running` NO VOLVIA A OCURRIR NUNCA: la condicion quedaba
|
|
101
|
+
// permanentemente falsa y `until` giraba hasta agotar su presupuesto. Subir el
|
|
102
|
+
// timeout no lo arregla — no falta tiempo, falta que el estado sea observable.
|
|
103
|
+
// Fallo real: run 32211063150, solo windows-latest, con Ubuntu y macOS en verde.
|
|
104
|
+
//
|
|
105
|
+
// El centinela invierte el control: el hijo sigue vivo mientras el test lo
|
|
106
|
+
// necesite, asi que `running` es observable con cualquier latencia de plataforma.
|
|
107
|
+
// Refuerza R3.5 en vez de aflojarlo — "jamas se mata por duracion" se prueba
|
|
108
|
+
// mejor con un job que vive tanto como haga falta que con uno de 1,5 s. El
|
|
109
|
+
// setTimeout de 60 s es solo una red: si el test muere antes de liberar, el hijo
|
|
110
|
+
// no queda huerfano reteniendo handles sobre el tmpdir (ver rmSyncRetryingEbusy).
|
|
111
|
+
const sentinel = path_1.default.join(repo, 'release-r35.sentinel');
|
|
112
|
+
seedJob(repo, {
|
|
113
|
+
argv: ['node', '-e', `const fs=require('fs'),f=${JSON.stringify(sentinel)};setTimeout(()=>process.exit(0),60000);setInterval(()=>{if(fs.existsSync(f))process.exit(0)},50)`],
|
|
114
|
+
});
|
|
94
115
|
(0, runner_1.spawnPendingWrappers)(repo, 'rama', fakeSpawner);
|
|
95
116
|
await until(() => {
|
|
96
117
|
(0, runner_1.collectAndReconcile)(repo, 'rama');
|
|
@@ -105,6 +126,9 @@ describe('runner concurrente', () => {
|
|
|
105
126
|
// exec-wrapper.test.ts) — este test no puede exigir mas certeza de
|
|
106
127
|
// la que la plataforma real puede dar.
|
|
107
128
|
expect(running.processRef.psArgsDigest).toMatch(/^([0-9a-f]{16}|unknown)$/);
|
|
129
|
+
// Recien ahora se libera al hijo: la transicion a `exited` la decide el test,
|
|
130
|
+
// no el reloj, asi que tampoco esa mitad depende de la latencia de la plataforma.
|
|
131
|
+
fs_1.default.writeFileSync(sentinel, '');
|
|
108
132
|
await until(() => {
|
|
109
133
|
(0, runner_1.collectAndReconcile)(repo, 'rama');
|
|
110
134
|
return (0, store_1.readJournal)(repo, 'rama').state.jobs['j1'].executionState === 'exited';
|
|
@@ -0,0 +1,118 @@
|
|
|
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 crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const publishedCliRoot = process.env.AWM_PUBLISHED_CLI_ROOT;
|
|
12
|
+
// GitHub's Windows workspace expression can combine backslashes with a trailing
|
|
13
|
+
// forward-slash path segment. The CLI rightly requires normalized provenance;
|
|
14
|
+
// normalize the CI fixture boundary before passing it as user input.
|
|
15
|
+
const publishedRegistryRoot = process.env.AWM_PUBLISHED_REGISTRY_ROOT
|
|
16
|
+
? path_1.default.resolve(process.env.AWM_PUBLISHED_REGISTRY_ROOT)
|
|
17
|
+
: undefined;
|
|
18
|
+
// This gate is pinned to the npm release that contains the native ESLint
|
|
19
|
+
// configuration selection fix merged for R3.
|
|
20
|
+
const expectedVersion = process.env.AWM_PUBLISHED_CLI_VERSION ?? '8.1.2';
|
|
21
|
+
const enabled = Boolean(publishedCliRoot && publishedRegistryRoot);
|
|
22
|
+
const acceptance = enabled ? describe : describe.skip;
|
|
23
|
+
function hashTree(root) {
|
|
24
|
+
const hash = crypto_1.default.createHash('sha256');
|
|
25
|
+
const walk = (directory) => {
|
|
26
|
+
for (const entry of fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
27
|
+
const item = path_1.default.join(directory, entry.name);
|
|
28
|
+
hash.update(path_1.default.relative(root, item));
|
|
29
|
+
if (entry.isDirectory())
|
|
30
|
+
walk(item);
|
|
31
|
+
else if (entry.isFile())
|
|
32
|
+
hash.update(fs_1.default.readFileSync(item));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk(root);
|
|
36
|
+
return hash.digest('hex');
|
|
37
|
+
}
|
|
38
|
+
function createFixture(kind) {
|
|
39
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-published-r3-'));
|
|
40
|
+
const project = path_1.default.join(root, 'project');
|
|
41
|
+
const awmHome = path_1.default.join(root, 'awm-home');
|
|
42
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
43
|
+
fs_1.default.mkdirSync(path_1.default.join(awmHome, 'registries'), { recursive: true });
|
|
44
|
+
fs_1.default.cpSync(publishedRegistryRoot, path_1.default.join(awmHome, 'registries', 'baseline'), { recursive: true });
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'published-acceptance' }]));
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ name: `published-${kind}`, version: '1.0.0', scripts: { test: 'node -e "process.exit(0)"' } }));
|
|
47
|
+
if (kind === 'legacy') {
|
|
48
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'), { recursive: true });
|
|
49
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
|
|
50
|
+
pack: 'js-ts', sensors: { lint: { enabled: false, fast: true } },
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
if (kind === 'future') {
|
|
54
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
|
|
55
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ name: 'eslint', version: '11.0.0' }));
|
|
56
|
+
}
|
|
57
|
+
return { root, project, awmHome };
|
|
58
|
+
}
|
|
59
|
+
function command(fixture, ...args) {
|
|
60
|
+
const bin = path_1.default.join(publishedCliRoot, 'dist', 'src', 'index.js');
|
|
61
|
+
return (0, child_process_1.spawnSync)(process.execPath, [bin, ...args], {
|
|
62
|
+
cwd: fixture.project,
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function json(result) {
|
|
68
|
+
if (result.stdout.trim() === '')
|
|
69
|
+
throw new Error(`expected JSON output; status=${result.status}; stderr=${result.stderr}`);
|
|
70
|
+
return JSON.parse(result.stdout);
|
|
71
|
+
}
|
|
72
|
+
acceptance('published R3 acceptance', () => {
|
|
73
|
+
beforeAll(() => {
|
|
74
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(publishedCliRoot, 'package.json'), 'utf8'));
|
|
75
|
+
expect(packageJson.version).toBe(expectedVersion);
|
|
76
|
+
expect(fs_1.default.existsSync(path_1.default.join(publishedCliRoot, 'dist', 'src', 'index.js'))).toBe(true);
|
|
77
|
+
expect(fs_1.default.existsSync(path_1.default.join(publishedRegistryRoot, 'sensor-packs', 'js-ts', 'pack.json'))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
test.each(['new', 'legacy', 'future'])('uses the npm-installed CLI for the %s compatibility case', (kind) => {
|
|
80
|
+
const fixture = createFixture(kind);
|
|
81
|
+
try {
|
|
82
|
+
if (kind !== 'legacy') {
|
|
83
|
+
const init = command(fixture, 'sensors', 'init', '--registry-root', publishedRegistryRoot, '--pack', 'js-ts', '--no-configure');
|
|
84
|
+
expect(init.status).toBe(0);
|
|
85
|
+
}
|
|
86
|
+
const beforeReadOnly = hashTree(fixture.project);
|
|
87
|
+
const status = command(fixture, 'sensors', 'status');
|
|
88
|
+
expect([0, 1]).toContain(status.status);
|
|
89
|
+
const preflight = command(fixture, 'preflight', '--json');
|
|
90
|
+
expect([0, 1]).toContain(preflight.status);
|
|
91
|
+
expect(json(preflight)).toHaveProperty('status');
|
|
92
|
+
const run = command(fixture, 'sensors', 'run', '--fast');
|
|
93
|
+
expect([0, 1]).toContain(run.status);
|
|
94
|
+
expect(json(run)).toHaveProperty('overall');
|
|
95
|
+
const coverage = command(fixture, 'sensors', 'coverage', '--json', '--min', '2');
|
|
96
|
+
if (process.platform === 'win32') {
|
|
97
|
+
expect(coverage.status).toBe(1);
|
|
98
|
+
expect(`${coverage.stdout}${coverage.stderr}`).toContain('platform cannot guarantee no symlink dereference');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
expect(coverage.status).toBe(0);
|
|
102
|
+
expect(json(coverage)).toHaveProperty('schemaVersion', 2);
|
|
103
|
+
}
|
|
104
|
+
expect(hashTree(fixture.project)).toBe(beforeReadOnly);
|
|
105
|
+
command(fixture, 'ledger', 'add', '--branch', 'published-acceptance', '--polarity', 'finding', '--class', 'seguridad', '--signature', 'published-r3-finding', '--severity', 'important', '--desc', 'sanitized acceptance finding', '--defect-class', 'hardcoded-secrets');
|
|
106
|
+
const beforeArchive = command(fixture, 'sensors', 'coverage', '--json', '--min', '2');
|
|
107
|
+
expect(beforeArchive.status).toBe(process.platform === 'win32' ? 1 : 0);
|
|
108
|
+
const archived = command(fixture, 'ledger', 'archive', '--branch', 'published-acceptance');
|
|
109
|
+
expect(archived.status).toBe(0);
|
|
110
|
+
expect(json(archived)).toMatchObject({ archived: true, branch: 'published-acceptance' });
|
|
111
|
+
expect(fs_1.default.existsSync(path_1.default.join(fixture.project, '.awm', 'ledger', 'published-acceptance.jsonl'))).toBe(false);
|
|
112
|
+
expect(fs_1.default.readdirSync(path_1.default.join(fixture.project, '.awm', 'ledger', 'archive'))).toHaveLength(1);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
// Guard estructural — una capacidad del release que CI no puede invocar no existe en la
|
|
7
|
+
// práctica, y su ausencia no se nota hasta el día que hace falta.
|
|
8
|
+
//
|
|
9
|
+
// Pasó con `--force`. El flag estaba implementado y cubierto por tests
|
|
10
|
+
// (tests/release/orchestrator.test.ts: "--force patch publica aunque no haya commits
|
|
11
|
+
// releasables"), pero `release.yml` solo pasaba `--dry-run`. Consecuencia real: el PR #88
|
|
12
|
+
// corrigió `"license": "ISC"` -> `"Apache-2.0"` en el paquete publicado, se mergeó como
|
|
13
|
+
// `chore(legal):`, `determineBump` devolvió null — correctamente, un chore no es un
|
|
14
|
+
// release —, el workflow corrió EN VERDE y no publicó nada. `main` quedó bien y npm
|
|
15
|
+
// siguió entregando la licencia equivocada. No había forma de publicarlo desde CI.
|
|
16
|
+
//
|
|
17
|
+
// La regla no enumera los flags a mano: los deriva del propio `parseArgs`, así que un
|
|
18
|
+
// flag agregado mañana obliga a decidir explícitamente si CI puede alcanzarlo, en vez de
|
|
19
|
+
// heredar el silencio.
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
23
|
+
const CLI_ROOT = path_1.default.resolve(__dirname, '..', '..');
|
|
24
|
+
const REPO_ROOT = path_1.default.resolve(CLI_ROOT, '..');
|
|
25
|
+
const RELEASE_ENTRY = path_1.default.join(CLI_ROOT, 'src', 'release', 'index.ts');
|
|
26
|
+
const RELEASE_WORKFLOW = path_1.default.join(REPO_ROOT, '.github', 'workflows', 'release.yml');
|
|
27
|
+
// Flags deliberadamente NO expuestos a CI, con la razón por la que no lo están. Sacar uno
|
|
28
|
+
// de acá sin exponerlo en el workflow rompe el test a propósito.
|
|
29
|
+
const NOT_FOR_CI = {
|
|
30
|
+
'--no-push': 'uso local: un release desde CI siempre empuja',
|
|
31
|
+
'--branch': 'CI siempre libera desde main; el gate de rama vive en el orquestador',
|
|
32
|
+
};
|
|
33
|
+
/** Los flags que `parseArgs` realmente acepta, leídos del código, no de una lista. */
|
|
34
|
+
function acceptedFlags() {
|
|
35
|
+
const source = fs_1.default.readFileSync(RELEASE_ENTRY, 'utf8');
|
|
36
|
+
const found = [...source.matchAll(/a === '(--[a-z-]+)'/g)].map((m) => m[1]);
|
|
37
|
+
return [...new Set(found)];
|
|
38
|
+
}
|
|
39
|
+
/** El workflow sin sus comentarios. La alcanzabilidad se mide sobre lo que EJECUTA: un
|
|
40
|
+
* comentario que menciona `--force` no lo hace invocable, y la primera versión de este
|
|
41
|
+
* test se dejaba engañar por exactamente eso. */
|
|
42
|
+
function withoutComments(source) {
|
|
43
|
+
return source
|
|
44
|
+
.split('\n')
|
|
45
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
46
|
+
.join('\n');
|
|
47
|
+
}
|
|
48
|
+
describe('flags del release: implementados <=> alcanzables desde CI', () => {
|
|
49
|
+
const workflowSource = fs_1.default.readFileSync(RELEASE_WORKFLOW, 'utf8');
|
|
50
|
+
const executable = withoutComments(workflowSource);
|
|
51
|
+
const workflow = js_yaml_1.default.load(workflowSource);
|
|
52
|
+
it('parseArgs declara al menos un flag', () => {
|
|
53
|
+
expect(acceptedFlags().length).toBeGreaterThan(0);
|
|
54
|
+
});
|
|
55
|
+
it('cada flag del release está expuesto en release.yml o excluido con motivo', () => {
|
|
56
|
+
const unreachable = acceptedFlags().filter((flag) => !executable.includes(flag) && !(flag in NOT_FOR_CI));
|
|
57
|
+
expect(unreachable).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('workflow_dispatch expone el bump forzado', () => {
|
|
60
|
+
const inputs = workflow.on?.workflow_dispatch?.inputs ?? {};
|
|
61
|
+
expect(Object.keys(inputs)).toContain('bump');
|
|
62
|
+
});
|
|
63
|
+
it('el paso de Release pasa --force solo cuando el bump no es auto', () => {
|
|
64
|
+
expect(workflowSource).toMatch(/inputs\.bump != 'auto'/);
|
|
65
|
+
expect(workflowSource).toMatch(/--force \{0\}/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -55,10 +55,10 @@ describe('docs/support-matrix.md refleja el codigo', () => {
|
|
|
55
55
|
it('CI regenerates the published matrix from the immutable registry tag', () => {
|
|
56
56
|
const workflow = fs_1.default.readFileSync(CI_WORKFLOW_PATH, 'utf8');
|
|
57
57
|
expect(workflow).toContain('repository: Kodria/awm-baseline-registry');
|
|
58
|
-
expect(workflow).toContain('ref: v2.0.
|
|
58
|
+
expect(workflow).toContain('ref: v2.0.1');
|
|
59
59
|
expect(workflow).toContain('path: awm-baseline-registry');
|
|
60
60
|
expect(workflow).toContain('Verify published sensor support matrix');
|
|
61
|
-
expect(workflow).toContain('--registry-root ../awm-baseline-registry --registry-tag v2.0.
|
|
61
|
+
expect(workflow).toContain('--registry-root ../awm-baseline-registry --registry-tag v2.0.1 --registry-commit 6f40632006fc65300ac633c5a54f2635cf0eb8e9');
|
|
62
62
|
expect(workflow).toContain('git diff --exit-code -- docs/support-matrix.md');
|
|
63
63
|
});
|
|
64
64
|
it('retains the published registry certification rows verbatim instead of inventing fixture evidence', () => {
|
|
@@ -87,7 +87,7 @@ describe('docs/support-matrix.md refleja el codigo', () => {
|
|
|
87
87
|
expect((0, sensor_support_matrix_1.extractPublishedSupportMetadata)(publishedSupport)).toContain('compatible-unverified');
|
|
88
88
|
expect((0, sensor_support_matrix_1.extractPublishedSupportMetadata)(publishedSupport)).not.toContain('Fixture-declared ranges only');
|
|
89
89
|
});
|
|
90
|
-
it('rejects a mutable checkout even when it contains the published v2.0.
|
|
90
|
+
it('rejects a mutable checkout even when it contains the published v2.0.1 tag', () => {
|
|
91
91
|
const registryRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-published-registry-'));
|
|
92
92
|
const git = (...args) => (0, child_process_1.execFileSync)('git', ['-C', registryRoot, ...args], { stdio: 'pipe' });
|
|
93
93
|
try {
|
|
@@ -97,11 +97,11 @@ describe('docs/support-matrix.md refleja el codigo', () => {
|
|
|
97
97
|
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'published.txt'), 'published\n');
|
|
98
98
|
git('add', '.');
|
|
99
99
|
git('commit', '-m', 'published registry');
|
|
100
|
-
git('tag', 'v2.0.
|
|
100
|
+
git('tag', 'v2.0.1');
|
|
101
101
|
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'mutable.txt'), 'newer checkout\n');
|
|
102
102
|
git('add', '.');
|
|
103
103
|
git('commit', '-m', 'mutable checkout');
|
|
104
|
-
expect(() => (0, sensor_support_matrix_1.verifyPublishedRegistryIdentity)(registryRoot, 'v2.0.
|
|
104
|
+
expect(() => (0, sensor_support_matrix_1.verifyPublishedRegistryIdentity)(registryRoot, 'v2.0.1', '6f40632006fc65300ac633c5a54f2635cf0eb8e9')).toThrow(/HEAD .*expected commit/i);
|
|
105
105
|
}
|
|
106
106
|
finally {
|
|
107
107
|
fs_1.default.rmSync(registryRoot, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.3",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"ai"
|
|
39
39
|
],
|
|
40
40
|
"author": "Kodria",
|
|
41
|
-
"license": "
|
|
41
|
+
"license": "Apache-2.0",
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=22"
|
|
44
44
|
},
|