agentic-workflow-manager 9.16.1 → 9.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/env/index.js +192 -0
- package/dist/src/commands/preflight/checks.js +63 -13
- package/dist/src/commands/registry/doctor.js +428 -0
- package/dist/src/commands/registry/index.js +52 -0
- package/dist/src/commands/registry/scaffold.js +140 -0
- package/dist/src/commands/sensors/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- package/dist/src/commands/watch/index.js +3 -0
- package/dist/src/core/environments/resolve.js +122 -0
- package/dist/src/core/environments/store.js +323 -0
- package/dist/src/core/facts/catalog.js +138 -0
- package/dist/src/core/facts/needs.js +57 -0
- package/dist/src/core/facts/types.js +21 -0
- package/dist/src/core/init/steps.js +5 -1
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/preflight/preflight.test.js +121 -0
- package/dist/tests/commands/registry/doctor.test.js +283 -0
- package/dist/tests/commands/registry/scaffold.test.js +186 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -0
- package/dist/tests/commands/watch/reopened-196.test.js +9 -2
- package/dist/tests/core/environments/resolve.test.js +347 -0
- package/dist/tests/core/environments/store.test.js +248 -0
- package/dist/tests/core/facts/catalog.test.js +179 -0
- package/dist/tests/core/facts/needs.test.js +88 -0
- package/dist/tests/structural/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -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
|
@@ -641,3 +641,334 @@ describe('runSensors — honest floor (not_certified over real stack)', () => {
|
|
|
641
641
|
}
|
|
642
642
|
});
|
|
643
643
|
});
|
|
644
|
+
describe('runSensors project-declared parity (S2)', () => {
|
|
645
|
+
let project;
|
|
646
|
+
let home;
|
|
647
|
+
const version = '10.0.0';
|
|
648
|
+
const packBoundLint = () => ({
|
|
649
|
+
enabled: true,
|
|
650
|
+
fast: true,
|
|
651
|
+
variantId: 'eslint-10',
|
|
652
|
+
command: { executable: 'stale-eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
653
|
+
assets: ['eslint.config.awm.mjs'],
|
|
654
|
+
initializedCompatibility: {
|
|
655
|
+
state: 'certified',
|
|
656
|
+
reason: 'range-and-probe',
|
|
657
|
+
variantId: 'eslint-10',
|
|
658
|
+
toolVersion: version,
|
|
659
|
+
runtimeVersion: process.versions.node,
|
|
660
|
+
certifiedRange: '>=10 <11',
|
|
661
|
+
evidence: [],
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
const projectIac = (exitScript = 'process.exit(0)') => ({
|
|
665
|
+
source: 'project',
|
|
666
|
+
enabled: true,
|
|
667
|
+
fast: true,
|
|
668
|
+
command: { executable: 'node', resolution: 'path', args: ['-e', exitScript] },
|
|
669
|
+
formatter: 'exit-code',
|
|
670
|
+
});
|
|
671
|
+
beforeEach(() => {
|
|
672
|
+
jest.resetModules();
|
|
673
|
+
project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-v3-project-run-'));
|
|
674
|
+
home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-v3-project-home-'));
|
|
675
|
+
process.env.AWM_HOME = home;
|
|
676
|
+
const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
677
|
+
fs_1.default.mkdirSync(registry, { recursive: true });
|
|
678
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
|
|
679
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
680
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
|
|
681
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
|
|
682
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
|
|
683
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
|
|
684
|
+
schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
|
|
685
|
+
coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } } } },
|
|
686
|
+
sensors: {
|
|
687
|
+
lint: {
|
|
688
|
+
fast: true,
|
|
689
|
+
applicability: { allFiles: ['package.json'] },
|
|
690
|
+
variants: [{
|
|
691
|
+
id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
|
|
692
|
+
requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
|
|
693
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'generic',
|
|
694
|
+
probe: { kind: 'package-script-present', script: 'lint' },
|
|
695
|
+
command: { executable: 'live-eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
696
|
+
}],
|
|
697
|
+
},
|
|
698
|
+
typecheck: {
|
|
699
|
+
fast: true,
|
|
700
|
+
applicability: { allFiles: ['package.json'] },
|
|
701
|
+
variants: [{
|
|
702
|
+
id: 'tsc-5', priority: 1, certifiedRange: '>=5 <6',
|
|
703
|
+
requirements: { tool: 'tsc', toolRange: '>=5 <6', runtime: 'node', runtimeRange: '>=0' },
|
|
704
|
+
assets: [], formatter: 'tsc',
|
|
705
|
+
probe: { kind: 'version' },
|
|
706
|
+
command: { executable: 'tsc', resolution: 'node-modules-bin', args: ['--noEmit'] },
|
|
707
|
+
}],
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
}));
|
|
711
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'));
|
|
712
|
+
mockRunStructuredCommand.mockReset();
|
|
713
|
+
mockRunCommand.mockReset();
|
|
714
|
+
});
|
|
715
|
+
afterEach(() => {
|
|
716
|
+
delete process.env.AWM_HOME;
|
|
717
|
+
fs_1.default.rmSync(project, { recursive: true, force: true });
|
|
718
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
719
|
+
});
|
|
720
|
+
function writeManifest(sensors, pack = 'js-ts') {
|
|
721
|
+
const body = {
|
|
722
|
+
schemaVersion: 3,
|
|
723
|
+
mode: 'project-sensors',
|
|
724
|
+
sensors,
|
|
725
|
+
};
|
|
726
|
+
if (pack !== undefined) {
|
|
727
|
+
body.pack = pack;
|
|
728
|
+
if (typeof pack === 'string' && pack.length > 0)
|
|
729
|
+
body.source = { registry: 'baseline' };
|
|
730
|
+
}
|
|
731
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(body));
|
|
732
|
+
}
|
|
733
|
+
it('runs pack lint and project iac-format together with project-declared provenance and overall pass (RF-2.1/2.2/2.3)', async () => {
|
|
734
|
+
writeManifest({ lint: packBoundLint(), 'iac-format': projectIac('process.exit(0)') });
|
|
735
|
+
mockRunStructuredCommand.mockImplementation(async (command) => {
|
|
736
|
+
if (command.executable === 'live-eslint')
|
|
737
|
+
return ok();
|
|
738
|
+
if (command.executable === 'node')
|
|
739
|
+
return ok();
|
|
740
|
+
return ok();
|
|
741
|
+
});
|
|
742
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
743
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
744
|
+
expect(result.sensors.map((s) => s.name).sort()).toEqual(['iac-format', 'lint']);
|
|
745
|
+
const projectRow = result.sensors.find((s) => s.name === 'iac-format');
|
|
746
|
+
expect(projectRow).toMatchObject({ status: 'pass', certification: 'project-declared' });
|
|
747
|
+
expect(projectRow.certification).not.toBe('certified');
|
|
748
|
+
expect(result.overall).toBe('pass');
|
|
749
|
+
expect(result.invalidEntries ?? []).toEqual([]);
|
|
750
|
+
});
|
|
751
|
+
it('fails overall when the project sensor exits non-zero (RF-2.5)', async () => {
|
|
752
|
+
writeManifest({ lint: packBoundLint(), 'iac-format': projectIac('process.exit(1)') });
|
|
753
|
+
mockRunStructuredCommand.mockImplementation(async (command) => {
|
|
754
|
+
if (command.executable === 'live-eslint')
|
|
755
|
+
return ok();
|
|
756
|
+
if (command.executable === 'node')
|
|
757
|
+
return exited(1, 'fmt drift\n');
|
|
758
|
+
return ok();
|
|
759
|
+
});
|
|
760
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
761
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
762
|
+
expect(result.sensors.find((s) => s.name === 'iac-format')).toMatchObject({ status: 'fail' });
|
|
763
|
+
expect(result.overall).toBe('fail');
|
|
764
|
+
});
|
|
765
|
+
it('skips a schema-invalid project sibling, still runs pack, and lists the invalid entry (RF-2.6)', async () => {
|
|
766
|
+
writeManifest({
|
|
767
|
+
lint: packBoundLint(),
|
|
768
|
+
broken: {
|
|
769
|
+
source: 'project',
|
|
770
|
+
enabled: true,
|
|
771
|
+
fast: true,
|
|
772
|
+
command: { executable: 'node', resolution: 'path', args: ['.'] },
|
|
773
|
+
formatter: 'not-a-formatter',
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
777
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
778
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
779
|
+
expect(result.sensors.some((s) => s.name === 'lint')).toBe(true);
|
|
780
|
+
expect(result.sensors.some((s) => s.name === 'broken')).toBe(false);
|
|
781
|
+
expect(result.invalidEntries).toEqual([
|
|
782
|
+
expect.objectContaining({
|
|
783
|
+
name: 'broken',
|
|
784
|
+
reason: expect.stringMatching(/sensors\.broken\.formatter|not a registered formatter/i),
|
|
785
|
+
}),
|
|
786
|
+
]);
|
|
787
|
+
expect(result.reason).not.toBe('manifest-malformed');
|
|
788
|
+
expect(result.sensors.length).toBeGreaterThan(0);
|
|
789
|
+
});
|
|
790
|
+
it('suppresses a baselined project exit-code failure as sensor-level debt (RF-2.7)', async () => {
|
|
791
|
+
writeManifest({ 'iac-format': projectIac('process.exit(1)') }, null);
|
|
792
|
+
mockRunStructuredCommand.mockResolvedValue(exited(1, 'fmt drift\n'));
|
|
793
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
794
|
+
const { buildBaseline, writeBaseline } = require('../../../src/commands/sensors/baseline');
|
|
795
|
+
const first = await runSensors({ cwd: project, all: true });
|
|
796
|
+
expect(first.overall).toBe('fail');
|
|
797
|
+
const row = first.sensors.find((s) => s.name === 'iac-format');
|
|
798
|
+
expect(row.status).toBe('fail');
|
|
799
|
+
writeBaseline(project, buildBaseline(first.sensors.map((s) => ({ name: s.name, errors: s.errors }))));
|
|
800
|
+
mockRunStructuredCommand.mockResolvedValue(exited(1, 'fmt drift\n'));
|
|
801
|
+
const second = await runSensors({ cwd: project, all: true });
|
|
802
|
+
const suppressed = second.sensors.find((s) => s.name === 'iac-format');
|
|
803
|
+
expect(suppressed).toMatchObject({ status: 'pass', baselineCount: 1 });
|
|
804
|
+
expect(second.overall).toBe('pass');
|
|
805
|
+
});
|
|
806
|
+
it('yields overall pass for a passing project-only run (RF-2.3 provenance never gates)', async () => {
|
|
807
|
+
writeManifest({ 'iac-format': projectIac('process.exit(0)') }, null);
|
|
808
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
809
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
810
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
811
|
+
expect(result.sensors).toEqual([
|
|
812
|
+
expect.objectContaining({ name: 'iac-format', status: 'pass', certification: 'project-declared' }),
|
|
813
|
+
]);
|
|
814
|
+
expect(result.overall).toBe('pass');
|
|
815
|
+
});
|
|
816
|
+
it('marks a colliding project entry invalid and still prepares/runs the pack sensor for that id (RF-1.7)', async () => {
|
|
817
|
+
// Project entry reuses pack id `lint` — invalid, but pack lint must remain executable.
|
|
818
|
+
writeManifest({ lint: projectIac('process.exit(0)') });
|
|
819
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
820
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
821
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
822
|
+
expect(result.invalidEntries).toEqual([
|
|
823
|
+
expect.objectContaining({
|
|
824
|
+
name: 'lint',
|
|
825
|
+
reason: expect.stringMatching(/project-sensor-name-collision:\s*lint/),
|
|
826
|
+
}),
|
|
827
|
+
]);
|
|
828
|
+
const lint = result.sensors.find((s) => s.name === 'lint');
|
|
829
|
+
expect(lint).toBeDefined();
|
|
830
|
+
expect(lint.certification).not.toBe('project-declared');
|
|
831
|
+
expect(lint.status).toBe('pass');
|
|
832
|
+
expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'live-eslint' }), expect.any(Object));
|
|
833
|
+
});
|
|
834
|
+
it('on collision with null liveState.variantId still prepares pack (not project) for that id (RF-1.7)', async () => {
|
|
835
|
+
writeManifest({ lint: projectIac('process.exit(0)') });
|
|
836
|
+
// Remove the tool so live resolution yields missing-tool with variantId null,
|
|
837
|
+
// while the pack still defines the lint sensor.
|
|
838
|
+
fs_1.default.rmSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true, force: true });
|
|
839
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' } }));
|
|
840
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
841
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
842
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
843
|
+
expect(result.invalidEntries).toEqual([
|
|
844
|
+
expect.objectContaining({
|
|
845
|
+
name: 'lint',
|
|
846
|
+
reason: expect.stringMatching(/project-sensor-name-collision:\s*lint/),
|
|
847
|
+
}),
|
|
848
|
+
]);
|
|
849
|
+
const lint = result.sensors.find((s) => s.name === 'lint');
|
|
850
|
+
expect(lint).toBeDefined();
|
|
851
|
+
expect(lint.certification).not.toBe('project-declared');
|
|
852
|
+
// Pack prepare/synthetic outcome — never the project node command.
|
|
853
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalledWith(expect.objectContaining({ executable: 'node' }), expect.any(Object));
|
|
854
|
+
});
|
|
855
|
+
it('carries project-declared provenance on a disabled/synthetic project sensor (RF-2.2)', async () => {
|
|
856
|
+
writeManifest({
|
|
857
|
+
'iac-format': { ...projectIac('process.exit(0)'), enabled: false },
|
|
858
|
+
}, null);
|
|
859
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
860
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
861
|
+
expect(result.sensors).toEqual([
|
|
862
|
+
expect.objectContaining({
|
|
863
|
+
name: 'iac-format',
|
|
864
|
+
status: 'skipped',
|
|
865
|
+
skipReason: 'disabled',
|
|
866
|
+
certification: 'project-declared',
|
|
867
|
+
}),
|
|
868
|
+
]);
|
|
869
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
870
|
+
});
|
|
871
|
+
it('runs a project sensor when its allFiles marker exists', async () => {
|
|
872
|
+
writeManifest({
|
|
873
|
+
'iac-format': {
|
|
874
|
+
...projectIac('process.exit(0)'),
|
|
875
|
+
applicability: { allFiles: ['package.json'] },
|
|
876
|
+
},
|
|
877
|
+
}, null);
|
|
878
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
879
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
880
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
881
|
+
expect(result.sensors).toEqual([
|
|
882
|
+
expect.objectContaining({ name: 'iac-format', status: 'pass', certification: 'project-declared' }),
|
|
883
|
+
]);
|
|
884
|
+
expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'node' }), expect.any(Object));
|
|
885
|
+
});
|
|
886
|
+
it('keeps a project sensor synthetic not-applicable when its allFiles marker is missing', async () => {
|
|
887
|
+
writeManifest({
|
|
888
|
+
'iac-format': {
|
|
889
|
+
...projectIac('process.exit(0)'),
|
|
890
|
+
applicability: { allFiles: ['missing-stack.marker'] },
|
|
891
|
+
},
|
|
892
|
+
}, null);
|
|
893
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
894
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
895
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
896
|
+
expect(result.sensors).toEqual([
|
|
897
|
+
expect.objectContaining({
|
|
898
|
+
name: 'iac-format',
|
|
899
|
+
status: 'inconclusive',
|
|
900
|
+
skipReason: 'not-applicable: applicability-not-met',
|
|
901
|
+
certification: 'project-declared',
|
|
902
|
+
}),
|
|
903
|
+
]);
|
|
904
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
905
|
+
});
|
|
906
|
+
it('does not soft-recover when packageRoot is a symlink escaping the project root', async () => {
|
|
907
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-soft-escape-out-'));
|
|
908
|
+
try {
|
|
909
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(project, 'pkg'));
|
|
910
|
+
const body = {
|
|
911
|
+
schemaVersion: 3,
|
|
912
|
+
mode: 'project-sensors',
|
|
913
|
+
packageRoot: 'pkg',
|
|
914
|
+
sensors: {
|
|
915
|
+
broken: {
|
|
916
|
+
source: 'project',
|
|
917
|
+
enabled: true,
|
|
918
|
+
fast: true,
|
|
919
|
+
command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
|
|
920
|
+
formatter: 'not-a-formatter',
|
|
921
|
+
},
|
|
922
|
+
good: projectIac('process.exit(0)'),
|
|
923
|
+
},
|
|
924
|
+
};
|
|
925
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(body));
|
|
926
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
927
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
928
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
929
|
+
expect(result.overall).toBe('not_certified');
|
|
930
|
+
expect(result.mode).toBe('invalid');
|
|
931
|
+
expect(result.sensors).toEqual([]);
|
|
932
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
it('does not soft-recover a symlinked sensors.json (non-regular-file authority)', async () => {
|
|
939
|
+
const realManifest = path_1.default.join(project, 'elsewhere-sensors.json');
|
|
940
|
+
fs_1.default.writeFileSync(realManifest, JSON.stringify({
|
|
941
|
+
schemaVersion: 3,
|
|
942
|
+
mode: 'project-sensors',
|
|
943
|
+
sensors: { good: projectIac('process.exit(0)') },
|
|
944
|
+
}));
|
|
945
|
+
const manifestPath = path_1.default.join(project, '.awm', 'sensors.json');
|
|
946
|
+
if (fs_1.default.existsSync(manifestPath))
|
|
947
|
+
fs_1.default.unlinkSync(manifestPath);
|
|
948
|
+
fs_1.default.symlinkSync(realManifest, manifestPath);
|
|
949
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
950
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
951
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
952
|
+
expect(result.overall).toBe('not_certified');
|
|
953
|
+
expect(result.mode).toBe('invalid');
|
|
954
|
+
expect(result.sensors).toEqual([]);
|
|
955
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
956
|
+
});
|
|
957
|
+
it('fail-closes a project id collision when the live pack cannot be resolved (live null)', async () => {
|
|
958
|
+
writeManifest({ lint: projectIac('process.exit(0)') });
|
|
959
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'not-valid-json{');
|
|
960
|
+
mockRunStructuredCommand.mockResolvedValue(ok());
|
|
961
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
962
|
+
const result = await runSensors({ cwd: project, all: true });
|
|
963
|
+
expect(result.invalidEntries).toEqual([
|
|
964
|
+
expect.objectContaining({
|
|
965
|
+
name: 'lint',
|
|
966
|
+
reason: expect.stringMatching(/project-sensor-name-collision:\s*lint/),
|
|
967
|
+
}),
|
|
968
|
+
]);
|
|
969
|
+
const lint = result.sensors.find((s) => s.name === 'lint');
|
|
970
|
+
if (lint)
|
|
971
|
+
expect(lint.certification).not.toBe('project-declared');
|
|
972
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalledWith(expect.objectContaining({ executable: 'node' }), expect.any(Object));
|
|
973
|
+
});
|
|
974
|
+
});
|
|
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const status_1 = require("../../../src/commands/sensors/status");
|
|
10
|
+
const coverage_1 = require("../../../src/commands/sensors/coverage");
|
|
10
11
|
jest.mock('../../../src/commands/sensors/exec', () => ({
|
|
11
12
|
runCommand: jest.fn(),
|
|
12
13
|
runStructuredCommand: jest.fn(),
|
|
@@ -409,3 +410,206 @@ describe('computeSensorStatus', () => {
|
|
|
409
410
|
}
|
|
410
411
|
});
|
|
411
412
|
});
|
|
413
|
+
describe('project-declared status and coverage (S3)', () => {
|
|
414
|
+
let tmpDir;
|
|
415
|
+
let pathDir;
|
|
416
|
+
let originalPath;
|
|
417
|
+
beforeEach(() => {
|
|
418
|
+
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-s3-'));
|
|
419
|
+
pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-s3-path-'));
|
|
420
|
+
originalPath = process.env.PATH;
|
|
421
|
+
process.env.PATH = pathDir;
|
|
422
|
+
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'terraform'), '#!/bin/sh\nexit 0\n');
|
|
423
|
+
fs_1.default.chmodSync(path_1.default.join(pathDir, 'terraform'), 0o755);
|
|
424
|
+
});
|
|
425
|
+
afterEach(() => {
|
|
426
|
+
if (originalPath === undefined)
|
|
427
|
+
delete process.env.PATH;
|
|
428
|
+
else
|
|
429
|
+
process.env.PATH = originalPath;
|
|
430
|
+
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
431
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
432
|
+
});
|
|
433
|
+
function writeOnlyProject(sensors) {
|
|
434
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
435
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
436
|
+
schemaVersion: 3,
|
|
437
|
+
mode: 'project-sensors',
|
|
438
|
+
sensors,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
it('lists each only-project sensor with project-declared provenance (RF-2.8)', async () => {
|
|
442
|
+
writeOnlyProject({
|
|
443
|
+
'iac-format': {
|
|
444
|
+
source: 'project',
|
|
445
|
+
enabled: true,
|
|
446
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
447
|
+
formatter: 'exit-code',
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
451
|
+
expect(result.checks['iac-format']).toMatchObject({
|
|
452
|
+
ok: true,
|
|
453
|
+
certification: 'project-declared',
|
|
454
|
+
});
|
|
455
|
+
expect(result.checks['iac-format'].detail).toMatch(/project-declared/i);
|
|
456
|
+
expect(result.overall).not.toBe('NOT_CONFIGURED');
|
|
457
|
+
// Must not be empty Pack:none / DEGRADED with zero detail solely because pack is absent.
|
|
458
|
+
expect(result.pack === null || result.pack === undefined).toBe(true);
|
|
459
|
+
expect(Object.keys(result.checks)).toEqual(expect.arrayContaining(['iac-format']));
|
|
460
|
+
});
|
|
461
|
+
it('reports coverage as inconclusive / no pack-reference for only-project (RF-2.9)', async () => {
|
|
462
|
+
writeOnlyProject({
|
|
463
|
+
'iac-format': {
|
|
464
|
+
source: 'project',
|
|
465
|
+
enabled: true,
|
|
466
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
467
|
+
formatter: 'exit-code',
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
const coverage = await (0, coverage_1.runCoverage)(tmpDir);
|
|
471
|
+
expect(coverage.overall).toBe('inconclusive');
|
|
472
|
+
expect(coverage.static.status).toBe('inconclusive');
|
|
473
|
+
expect(coverage.static.reason).toBe('no_reference');
|
|
474
|
+
expect(coverage.static.classes).toEqual([]);
|
|
475
|
+
expect(coverage.overall).not.toBe('covered');
|
|
476
|
+
expect(JSON.stringify(coverage)).not.toMatch(/"certification"\s*:\s*"certified"/);
|
|
477
|
+
});
|
|
478
|
+
it('soft-isolates valid siblings when one project entry is schema-invalid (status vs run)', async () => {
|
|
479
|
+
writeOnlyProject({
|
|
480
|
+
good: {
|
|
481
|
+
source: 'project',
|
|
482
|
+
enabled: true,
|
|
483
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
484
|
+
formatter: 'exit-code',
|
|
485
|
+
},
|
|
486
|
+
bad: {
|
|
487
|
+
source: 'project',
|
|
488
|
+
enabled: true,
|
|
489
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
490
|
+
formatter: 'not-a-formatter',
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
494
|
+
expect(Object.keys(result.checks).length).toBeGreaterThan(0);
|
|
495
|
+
expect(result.checks['good']).toMatchObject({
|
|
496
|
+
ok: true,
|
|
497
|
+
certification: 'project-declared',
|
|
498
|
+
});
|
|
499
|
+
expect(result.checks['bad']).toMatchObject({ ok: false });
|
|
500
|
+
expect(result.checks['bad'].detail).toMatch(/formatter|invalid/i);
|
|
501
|
+
expect(result.overall).toBe('DEGRADED');
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
describe('project-declared status pack collision (S3 RF-1.7)', () => {
|
|
505
|
+
let project;
|
|
506
|
+
let home;
|
|
507
|
+
let pathDir;
|
|
508
|
+
let originalPath;
|
|
509
|
+
let originalHome;
|
|
510
|
+
const version = '10.0.0';
|
|
511
|
+
beforeEach(() => {
|
|
512
|
+
project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-collision-'));
|
|
513
|
+
home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-collision-home-'));
|
|
514
|
+
pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-collision-path-'));
|
|
515
|
+
originalPath = process.env.PATH;
|
|
516
|
+
originalHome = process.env.AWM_HOME;
|
|
517
|
+
process.env.PATH = pathDir;
|
|
518
|
+
process.env.AWM_HOME = home;
|
|
519
|
+
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'node'), '#!/bin/sh\nexit 0\n');
|
|
520
|
+
fs_1.default.chmodSync(path_1.default.join(pathDir, 'node'), 0o755);
|
|
521
|
+
const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
522
|
+
fs_1.default.mkdirSync(registry, { recursive: true });
|
|
523
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
|
|
524
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
525
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
|
|
526
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
|
|
527
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
|
|
528
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', '.bin'), { recursive: true });
|
|
529
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', '.bin', 'eslint'), '');
|
|
530
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
|
|
531
|
+
schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
|
|
532
|
+
coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } } } },
|
|
533
|
+
sensors: {
|
|
534
|
+
lint: {
|
|
535
|
+
fast: true,
|
|
536
|
+
applicability: { allFiles: ['package.json'] },
|
|
537
|
+
variants: [{
|
|
538
|
+
id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
|
|
539
|
+
requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
|
|
540
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'generic',
|
|
541
|
+
probe: { kind: 'package-script-present', script: 'lint' },
|
|
542
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
543
|
+
}],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
}));
|
|
547
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'));
|
|
548
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'eslint.config.awm.mjs'), 'export default []');
|
|
549
|
+
});
|
|
550
|
+
afterEach(() => {
|
|
551
|
+
if (originalPath === undefined)
|
|
552
|
+
delete process.env.PATH;
|
|
553
|
+
else
|
|
554
|
+
process.env.PATH = originalPath;
|
|
555
|
+
if (originalHome === undefined)
|
|
556
|
+
delete process.env.AWM_HOME;
|
|
557
|
+
else
|
|
558
|
+
process.env.AWM_HOME = originalHome;
|
|
559
|
+
fs_1.default.rmSync(project, { recursive: true, force: true });
|
|
560
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
561
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
562
|
+
});
|
|
563
|
+
it('does not label a colliding project id as project-declared READY (RF-1.7 status)', async () => {
|
|
564
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
|
|
565
|
+
schemaVersion: 3,
|
|
566
|
+
mode: 'project-sensors',
|
|
567
|
+
pack: 'js-ts',
|
|
568
|
+
source: { registry: 'baseline' },
|
|
569
|
+
sensors: {
|
|
570
|
+
lint: {
|
|
571
|
+
source: 'project',
|
|
572
|
+
enabled: true,
|
|
573
|
+
command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
|
|
574
|
+
formatter: 'exit-code',
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
}));
|
|
578
|
+
const result = await (0, status_1.computeSensorStatus)(project);
|
|
579
|
+
expect(result.checks['lint']).toBeDefined();
|
|
580
|
+
expect(result.checks['lint'].certification).not.toBe('project-declared');
|
|
581
|
+
expect(result.checks['lint'].detail).toMatch(/project-sensor-name-collision|pack (sensor )?kept/i);
|
|
582
|
+
expect(result.overall).not.toBe('READY');
|
|
583
|
+
});
|
|
584
|
+
it('soft-isolate + pack + colliding project id does not certify project-declared READY (RF-1.7)', async () => {
|
|
585
|
+
// Invalid sibling forces softIsolatedChecks; lint collides with live pack id.
|
|
586
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
|
|
587
|
+
schemaVersion: 3,
|
|
588
|
+
mode: 'project-sensors',
|
|
589
|
+
pack: 'js-ts',
|
|
590
|
+
source: { registry: 'baseline' },
|
|
591
|
+
sensors: {
|
|
592
|
+
lint: {
|
|
593
|
+
source: 'project',
|
|
594
|
+
enabled: true,
|
|
595
|
+
command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
|
|
596
|
+
formatter: 'exit-code',
|
|
597
|
+
},
|
|
598
|
+
bad: {
|
|
599
|
+
source: 'project',
|
|
600
|
+
enabled: true,
|
|
601
|
+
command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
|
|
602
|
+
formatter: 'not-a-formatter',
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
}));
|
|
606
|
+
const result = await (0, status_1.computeSensorStatus)(project);
|
|
607
|
+
expect(result.mode).toBe('invalid');
|
|
608
|
+
expect(result.checks['bad']).toMatchObject({ ok: false });
|
|
609
|
+
expect(result.checks['lint']).toBeDefined();
|
|
610
|
+
expect(result.checks['lint'].ok).toBe(false);
|
|
611
|
+
expect(result.checks['lint'].certification).not.toBe('project-declared');
|
|
612
|
+
expect(result.checks['lint'].detail).toMatch(/project-sensor-name-collision|pack (sensor )?kept/i);
|
|
613
|
+
expect(result.overall).not.toBe('READY');
|
|
614
|
+
});
|
|
615
|
+
});
|
|
@@ -921,9 +921,16 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
921
921
|
}
|
|
922
922
|
expect(stdout).toContain('supervisor activo');
|
|
923
923
|
child.kill('SIGINT');
|
|
924
|
-
const exit = await new Promise(resolve =>
|
|
925
|
-
|
|
924
|
+
const exit = await new Promise(resolve => {
|
|
925
|
+
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
926
|
+
});
|
|
927
|
+
// Contract: truthful interrupt report. Exit status is secondary —
|
|
928
|
+
// on Darwin, child_process often reports code null + SIGINT when the
|
|
929
|
+
// signal disposition races process.exitCode=130 (never use process.exit
|
|
930
|
+
// in watch/index: it kills in-process Jest callers). Either 130 or
|
|
931
|
+
// null+SIGINT is an interrupted stop; require the detenido line either way.
|
|
926
932
|
expect(stdout).toMatch(/detenido; ciclo IN_PROGRESS/);
|
|
933
|
+
expect(exit.code === 130 || (exit.code === null && exit.signal === 'SIGINT')).toBe(true);
|
|
927
934
|
expect(stdout).not.toContain('COMPLETE');
|
|
928
935
|
expect(stderr).not.toContain('Error');
|
|
929
936
|
}
|