agentic-workflow-manager 6.5.2 → 8.1.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/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +382 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +92 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +244 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +95 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
- package/dist/tests/commands/sensors/exec.test.js +115 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +204 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -14,6 +14,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
14
14
|
const os_1 = __importDefault(require("os"));
|
|
15
15
|
const path_1 = __importDefault(require("path"));
|
|
16
16
|
const paths_1 = require("../../src/core/paths");
|
|
17
|
+
const itPosix = process.platform !== 'win32' ? it : it.skip;
|
|
17
18
|
describe('sameExistingPath', () => {
|
|
18
19
|
let root;
|
|
19
20
|
beforeEach(() => { root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-samepath-'))); });
|
|
@@ -34,12 +35,46 @@ describe('sameExistingPath', () => {
|
|
|
34
35
|
fs_1.default.mkdirSync(b);
|
|
35
36
|
expect((0, paths_1.sameExistingPath)(a, b)).toBe(false);
|
|
36
37
|
});
|
|
38
|
+
it('does not treat colliding inode metadata as identity when native realpaths differ', () => {
|
|
39
|
+
const a = path_1.default.join(root, 'inode-collision-a');
|
|
40
|
+
const b = path_1.default.join(root, 'inode-collision-b');
|
|
41
|
+
fs_1.default.mkdirSync(a);
|
|
42
|
+
fs_1.default.mkdirSync(b);
|
|
43
|
+
const stat = jest.spyOn(fs_1.default, 'statSync').mockImplementation((() => ({ dev: 1, ino: 1 })));
|
|
44
|
+
const platform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
45
|
+
Object.defineProperty(process, 'platform', { ...platform, value: 'win32' });
|
|
46
|
+
try {
|
|
47
|
+
expect((0, paths_1.sameExistingPath)(a, b)).toBe(false);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
Object.defineProperty(process, 'platform', platform);
|
|
51
|
+
stat.mockRestore();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
itPosix('conserva la identidad de un archivo con hard links POSIX', () => {
|
|
55
|
+
const original = path_1.default.join(root, 'original');
|
|
56
|
+
const alias = path_1.default.join(root, 'alias');
|
|
57
|
+
fs_1.default.writeFileSync(original, 'contenido');
|
|
58
|
+
fs_1.default.linkSync(original, alias);
|
|
59
|
+
expect((0, paths_1.sameExistingPath)(alias, original)).toBe(true);
|
|
60
|
+
});
|
|
37
61
|
it('una ruta inexistente nunca "coincide" — la identidad no se afirma sin prueba', () => {
|
|
38
62
|
const a = path_1.default.join(root, 'a');
|
|
39
63
|
fs_1.default.mkdirSync(a);
|
|
40
64
|
expect((0, paths_1.sameExistingPath)(a, path_1.default.join(root, 'fantasma'))).toBe(false);
|
|
41
65
|
expect((0, paths_1.sameExistingPath)(path_1.default.join(root, 'fantasma'), path_1.default.join(root, 'fantasma'))).toBe(false);
|
|
42
66
|
});
|
|
67
|
+
it('falla cerrado si la canonicalización nativa no está disponible', () => {
|
|
68
|
+
const a = path_1.default.join(root, 'a');
|
|
69
|
+
fs_1.default.mkdirSync(a);
|
|
70
|
+
const realpath = jest.spyOn(fs_1.default.realpathSync, 'native').mockImplementation(() => { throw new Error('canonicalización no disponible'); });
|
|
71
|
+
try {
|
|
72
|
+
expect((0, paths_1.sameExistingPath)(a, a)).toBe(false);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
realpath.mockRestore();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
43
78
|
it('sobrevive a separadores y segmentos redundantes', () => {
|
|
44
79
|
const a = path_1.default.join(root, 'a');
|
|
45
80
|
fs_1.default.mkdirSync(a);
|
|
@@ -41,7 +41,8 @@ async function rmSyncRetryingBusy(target, attempts = 10, delayMs = 100) {
|
|
|
41
41
|
}
|
|
42
42
|
function makeRegistryWithManifest(base, name, manifest) {
|
|
43
43
|
const dir = path_1.default.join(base, name);
|
|
44
|
-
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
44
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'skills'), { recursive: true });
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'skills', '.keep'), 'fixture');
|
|
45
46
|
GIT(dir, 'init -q -b main');
|
|
46
47
|
fs_1.default.writeFileSync(path_1.default.join(dir, 'awm-registry.json'), JSON.stringify(manifest));
|
|
47
48
|
GIT(dir, 'add -A');
|
|
@@ -94,6 +94,24 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
|
|
|
94
94
|
function readPrefs() {
|
|
95
95
|
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/preferences.json'), 'utf8'));
|
|
96
96
|
}
|
|
97
|
+
it('machine-only enables Copilot without writing project artifacts (R7, R15)', async () => {
|
|
98
|
+
seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
|
|
99
|
+
fs_1.default.rmSync(path_1.default.join(tmpWork, 'package.json'));
|
|
100
|
+
const before = fs_1.default.readdirSync(tmpWork).sort();
|
|
101
|
+
const { runInit } = require('../../src/commands/init');
|
|
102
|
+
const code = await runInit({
|
|
103
|
+
cwd: tmpWork,
|
|
104
|
+
yes: true,
|
|
105
|
+
agent: 'copilot',
|
|
106
|
+
machineOnly: true,
|
|
107
|
+
});
|
|
108
|
+
expect(code).toBe(0);
|
|
109
|
+
expect(readPrefs().enabledAgents).toEqual(['copilot']);
|
|
110
|
+
expect(fs_1.default.readdirSync(tmpWork).sort()).toEqual(before);
|
|
111
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpWork, 'AGENTS.md'))).toBe(false);
|
|
112
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpWork, '.github'))).toBe(false);
|
|
113
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpWork, '.awm'))).toBe(false);
|
|
114
|
+
});
|
|
97
115
|
it('completes without crashing/rolling back (real bug repro) and delivers content via AGENTS.md, not a doomed global-scope install', async () => {
|
|
98
116
|
seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
|
|
99
117
|
const { runInit } = require('../../src/commands/init');
|
|
@@ -0,0 +1,141 @@
|
|
|
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 contract_1 = require("../../src/commands/sensors/compatibility/contract");
|
|
12
|
+
const discovery_1 = require("../../src/commands/sensors/compatibility/discovery");
|
|
13
|
+
const probe_1 = require("../../src/commands/sensors/compatibility/probe");
|
|
14
|
+
const resolve_1 = require("../../src/commands/sensors/compatibility/resolve");
|
|
15
|
+
const cliDir = path_1.default.resolve(__dirname, '../..');
|
|
16
|
+
const bin = path_1.default.join(cliDir, 'dist/src/index.js');
|
|
17
|
+
const fixtureRoot = path_1.default.join(cliDir, 'tests/fixtures/sensor-compatibility');
|
|
18
|
+
const projectFixture = path_1.default.join(fixtureRoot, 'project');
|
|
19
|
+
const registryFixture = path_1.default.join(fixtureRoot, 'registry');
|
|
20
|
+
const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
|
|
21
|
+
&& typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
|
|
22
|
+
// Other native platforms exercise coverage through testWithNoFollow; this
|
|
23
|
+
// branch preserves the deliberate Windows fail-closed behavior instead.
|
|
24
|
+
const testWithNoFollow = noFollowUnavailableOnNativeWindows ? test.skip : test;
|
|
25
|
+
const testWithoutNoFollowOnNativeWindows = noFollowUnavailableOnNativeWindows ? test : test.skip;
|
|
26
|
+
function hashTree(root) {
|
|
27
|
+
const hash = crypto_1.default.createHash('sha256');
|
|
28
|
+
const walk = (directory) => {
|
|
29
|
+
for (const entry of fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
30
|
+
const file = path_1.default.join(directory, entry.name);
|
|
31
|
+
hash.update(path_1.default.relative(root, file));
|
|
32
|
+
if (entry.isDirectory())
|
|
33
|
+
walk(file);
|
|
34
|
+
else if (entry.isFile())
|
|
35
|
+
hash.update(fs_1.default.readFileSync(file));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
walk(root);
|
|
39
|
+
return hash.digest('hex');
|
|
40
|
+
}
|
|
41
|
+
function createFixture() {
|
|
42
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-r3-compatibility-'));
|
|
43
|
+
const project = path_1.default.join(root, 'project');
|
|
44
|
+
const awmHome = path_1.default.join(root, 'awm-home');
|
|
45
|
+
const registryRoot = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
46
|
+
fs_1.default.cpSync(projectFixture, project, { recursive: true });
|
|
47
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
|
|
48
|
+
fs_1.default.copyFileSync(path_1.default.join(project, 'eslint-tool-package.json'), path_1.default.join(project, 'node_modules', 'eslint', 'package.json'));
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.dirname(registryRoot), { recursive: true });
|
|
50
|
+
fs_1.default.cpSync(registryFixture, registryRoot, { recursive: true });
|
|
51
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'fixture' }]));
|
|
52
|
+
return { root, project, awmHome, registryRoot };
|
|
53
|
+
}
|
|
54
|
+
function runCli(fixture, ...args) {
|
|
55
|
+
return (0, child_process_1.spawnSync)(process.execPath, [bin, 'sensors', ...args], {
|
|
56
|
+
cwd: fixture.project,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function json(result) {
|
|
62
|
+
expect(result.status).toBe(0);
|
|
63
|
+
if (!(result.stdout ?? '').trim())
|
|
64
|
+
throw new Error(`coverage emitted no JSON: stderr=${result.stderr ?? ''}`);
|
|
65
|
+
return JSON.parse(result.stdout ?? '');
|
|
66
|
+
}
|
|
67
|
+
beforeAll(() => {
|
|
68
|
+
if (!fs_1.default.existsSync(bin))
|
|
69
|
+
throw new Error(`Compatibility E2E requires the compiled CLI at ${bin}; run npm run build before this test.`);
|
|
70
|
+
});
|
|
71
|
+
// This is a portable resolver *semantic* matrix: `platform` is injected into
|
|
72
|
+
// the resolver, so it proves the three path/selection branches but does not
|
|
73
|
+
// claim that this Linux process is a native macOS or Windows execution. Native
|
|
74
|
+
// binary coverage is exercised separately below and by the CI OS matrix.
|
|
75
|
+
test.each(['linux', 'darwin', 'win32'])('keeps injected resolver semantics consistent on %s (R9.1)', async (platform) => {
|
|
76
|
+
const fixture = createFixture();
|
|
77
|
+
try {
|
|
78
|
+
const source = JSON.parse(fs_1.default.readFileSync(path_1.default.join(fixture.registryRoot, 'sensor-packs', 'js-ts', 'pack.json'), 'utf8'));
|
|
79
|
+
const parsed = (0, contract_1.parseSensorPack)(source, 'fixture/pack.json');
|
|
80
|
+
if (parsed.kind !== 'v2')
|
|
81
|
+
throw new Error('fixture must be a v2 pack');
|
|
82
|
+
const discovered = (0, discovery_1.discoverProjectEvidence)(fixture.project, parsed.pack, { platform: () => platform });
|
|
83
|
+
const probe = await (0, probe_1.runCompatibilityProbe)({ kind: 'version' }, { cwd: fixture.project, toolExecutable: 'eslint' }, async () => ({
|
|
84
|
+
code: 0, signal: null, timedOut: false, overflowed: false, stdout: 'eslint v10.4.1', stderr: '',
|
|
85
|
+
}));
|
|
86
|
+
const result = (0, resolve_1.resolveSensorCompatibility)(parsed.pack.sensors.lint, { ...discovered, probe }, { pack: 'js-ts', sensor: 'lint' });
|
|
87
|
+
expect(result).toMatchObject({ state: 'certified', variantId: 'eslint-10', toolVersion: '10.4.1' });
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
testWithNoFollow('compiled binary dispatches coverage and emits parseable JSON on the native CI platform', () => {
|
|
94
|
+
const fixture = createFixture();
|
|
95
|
+
try {
|
|
96
|
+
const report = json(runCli(fixture, 'coverage', '--json'));
|
|
97
|
+
expect(report).toMatchObject({ schemaVersion: 2, static: expect.any(Object), empirical: expect.any(Object) });
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
testWithNoFollow('legacy coverage stays unverified, init migrates explicitly, and version drift is visible (R7.2, R7.8)', () => {
|
|
104
|
+
const fixture = createFixture();
|
|
105
|
+
try {
|
|
106
|
+
const before = [hashTree(fixture.project), hashTree(fixture.awmHome)];
|
|
107
|
+
const legacy = json(runCli(fixture, 'coverage', '--json'));
|
|
108
|
+
expect(legacy).toMatchObject({ schemaVersion: 2, static: { status: 'inconclusive' } });
|
|
109
|
+
expect(legacy.static.classes).toEqual(expect.arrayContaining([expect.objectContaining({
|
|
110
|
+
id: 'lint-errors', status: 'unverifiable', detectors: expect.arrayContaining([expect.objectContaining({ compatibility: expect.objectContaining({ state: 'compatible-unverified' }) })]),
|
|
111
|
+
})]));
|
|
112
|
+
expect([hashTree(fixture.project), hashTree(fixture.awmHome)]).toEqual(before);
|
|
113
|
+
const migrated = runCli(fixture, 'init', '--registry-root', fixture.registryRoot, '--pack', 'js-ts', '--no-configure');
|
|
114
|
+
expect(migrated.status).toBe(0);
|
|
115
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(fixture.project, '.awm', 'sensors.json'), 'utf8'));
|
|
116
|
+
expect(manifest).toMatchObject({ schemaVersion: 2, pack: 'js-ts', sensors: { lint: { variantId: 'eslint-10' } } });
|
|
117
|
+
const certified = json(runCli(fixture, 'coverage', '--json'));
|
|
118
|
+
expect(certified.static.classes).toEqual(expect.arrayContaining([expect.objectContaining({
|
|
119
|
+
id: 'lint-errors', status: 'covered', detectors: expect.arrayContaining([expect.objectContaining({ compatibility: expect.objectContaining({ state: 'certified' }) })]),
|
|
120
|
+
})]));
|
|
121
|
+
fs_1.default.writeFileSync(path_1.default.join(fixture.project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ name: 'eslint', version: '11.0.0' }));
|
|
122
|
+
const drift = json(runCli(fixture, 'coverage', '--json'));
|
|
123
|
+
expect(drift).toMatchObject({ overall: 'inconclusive', static: { classes: [expect.objectContaining({
|
|
124
|
+
id: 'lint-errors', status: 'unverifiable', detectors: expect.arrayContaining([expect.objectContaining({ compatibility: expect.objectContaining({ state: 'compatible-unverified' }) })]),
|
|
125
|
+
})] } });
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
testWithoutNoFollowOnNativeWindows('coverage fails closed when native Windows cannot prevent symlink dereference', () => {
|
|
132
|
+
const fixture = createFixture();
|
|
133
|
+
try {
|
|
134
|
+
const result = runCli(fixture, 'coverage', '--json');
|
|
135
|
+
expect(result.status).toBe(1);
|
|
136
|
+
expect(`${result.stdout ?? ''}${result.stderr ?? ''}`).toContain('platform cannot guarantee no symlink dereference');
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -14,6 +14,7 @@ const fixture = path_1.default.join(cliDir, 'tests/fixtures/sensor-coverage/js-t
|
|
|
14
14
|
const registryFixture = path_1.default.join(cliDir, 'tests/fixtures/sensor-coverage/registry');
|
|
15
15
|
const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
|
|
16
16
|
&& typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
|
|
17
|
+
const testWithNoFollow = noFollowUnavailableOnNativeWindows ? test.skip : test;
|
|
17
18
|
const hashTree = (root) => {
|
|
18
19
|
const hash = crypto_1.default.createHash('sha256');
|
|
19
20
|
const walk = (directory) => {
|
|
@@ -63,7 +64,7 @@ function runWithFixture(mutatePack) {
|
|
|
63
64
|
}
|
|
64
65
|
return { tmp, project, awmHome };
|
|
65
66
|
}
|
|
66
|
-
test('compiled CLI reports
|
|
67
|
+
test('compiled CLI reports static and read-only empirical coverage without leaking ledger text (R5.6, R5.10)', () => {
|
|
67
68
|
const { tmp, project, awmHome } = runWithFixture();
|
|
68
69
|
try {
|
|
69
70
|
const before = [hashTree(project), hashTree(awmHome)];
|
|
@@ -78,9 +79,16 @@ test('compiled CLI reports gaps or fails closed when no-follow safety is unavail
|
|
|
78
79
|
expect(result.status).toBe(0);
|
|
79
80
|
const report = JSON.parse(result.stdout ?? '');
|
|
80
81
|
expect(report.static.classes).toEqual(expect.arrayContaining([
|
|
81
|
-
expect.objectContaining({ id: 'formatting', status: '
|
|
82
|
-
expect.objectContaining({ id: 'project-style-conventions', status: '
|
|
82
|
+
expect.objectContaining({ id: 'formatting', status: 'unverifiable' }),
|
|
83
|
+
expect.objectContaining({ id: 'project-style-conventions', status: 'unverifiable' }),
|
|
83
84
|
]));
|
|
85
|
+
expect(report).toMatchObject({ schemaVersion: 2, empirical: {
|
|
86
|
+
status: 'partial', classes: [expect.objectContaining({ defectClass: 'lint-errors', occurrences: 2, recurrent: true })],
|
|
87
|
+
unclassified: { occurrences: 1 },
|
|
88
|
+
} });
|
|
89
|
+
expect(result.stdout).not.toContain('fixture-secret-description');
|
|
90
|
+
expect(result.stdout).not.toContain('private-lint-signature');
|
|
91
|
+
expect(result.stdout).not.toContain('windows secret');
|
|
84
92
|
}
|
|
85
93
|
expect([hashTree(project), hashTree(awmHome)]).toEqual(before);
|
|
86
94
|
}
|
|
@@ -88,7 +96,25 @@ test('compiled CLI reports gaps or fails closed when no-follow safety is unavail
|
|
|
88
96
|
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
89
97
|
}
|
|
90
98
|
});
|
|
91
|
-
|
|
99
|
+
testWithNoFollow('compiled coverage keeps static verdict stable while --min changes only recurrence emphasis', () => {
|
|
100
|
+
const { tmp, project, awmHome } = runWithFixture();
|
|
101
|
+
try {
|
|
102
|
+
const before = [hashTree(project), hashTree(awmHome)];
|
|
103
|
+
const run = (min) => (0, child_process_1.spawnSync)(process.execPath, [bin, 'sensors', 'coverage', '--json', '--min', min], {
|
|
104
|
+
cwd: project, encoding: 'utf8', env: { ...process.env, AWM_HOME: awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
105
|
+
});
|
|
106
|
+
const defaultReport = JSON.parse(run('2').stdout ?? '');
|
|
107
|
+
const minThree = JSON.parse(run('3').stdout ?? '');
|
|
108
|
+
expect(defaultReport.static).toEqual(minThree.static);
|
|
109
|
+
expect(defaultReport.overall).toBe(minThree.overall);
|
|
110
|
+
expect(defaultReport.empirical.classes[0]).toMatchObject({ occurrences: 2, recurrent: true });
|
|
111
|
+
expect(minThree.empirical.classes[0]).toMatchObject({ occurrences: 2, recurrent: false });
|
|
112
|
+
expect([hashTree(project), hashTree(awmHome)]).toEqual(before);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
92
118
|
testWithNoFollow('informative states exit zero; malformed contract exits non-zero (R2.7, R2.9)', () => {
|
|
93
119
|
const run = (mutatePack) => {
|
|
94
120
|
const { tmp, project, awmHome } = runWithFixture(mutatePack);
|
|
@@ -105,9 +131,12 @@ testWithNoFollow('informative states exit zero; malformed contract exits non-zer
|
|
|
105
131
|
const noReference = run((pack) => { delete pack.coverage; });
|
|
106
132
|
const malformed = run((pack) => { pack.coverage.schemaVersion = 2; });
|
|
107
133
|
expect(gaps.status).toBe(0);
|
|
108
|
-
expect(JSON.parse(gaps.stdout ?? '')).toMatchObject({ overall: '
|
|
134
|
+
expect(JSON.parse(gaps.stdout ?? '')).toMatchObject({ overall: 'inconclusive' });
|
|
109
135
|
expect(noReference.status).toBe(0);
|
|
110
|
-
expect(JSON.parse(noReference.stdout ?? '')).toMatchObject({
|
|
136
|
+
expect(JSON.parse(noReference.stdout ?? '')).toMatchObject({
|
|
137
|
+
static: { reason: 'no_reference' },
|
|
138
|
+
empirical: { classes: [expect.objectContaining({ outcome: 'coverage-unverifiable' })] },
|
|
139
|
+
});
|
|
111
140
|
expect(malformed.status).toBe(1);
|
|
112
141
|
expect(`${malformed.stdout ?? ''}${malformed.stderr ?? ''}`).toContain('schemaVersion must be 1');
|
|
113
142
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
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 child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const cliDir = path_1.default.resolve(__dirname, '../..');
|
|
11
|
+
const bin = path_1.default.join(cliDir, 'dist/src/index.js');
|
|
12
|
+
function writeJson(file, value) {
|
|
13
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
14
|
+
fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* This is deliberately an argument which changes meaning under a shell on both
|
|
18
|
+
* POSIX and cmd.exe. The controlled Node tool records argv before the possible
|
|
19
|
+
* injected command can run, while the injected command would create the marker.
|
|
20
|
+
* A shell:true regression must therefore either alter argv or create the marker.
|
|
21
|
+
*/
|
|
22
|
+
const shellSensitiveLiteral = '--literal=preserve-this&node -e "require(\'fs\').writeFileSync(\'structured-shell-injection\', \'unexpected\')"';
|
|
23
|
+
function createFixture() {
|
|
24
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-structured-run-'));
|
|
25
|
+
const project = path_1.default.join(root, 'project');
|
|
26
|
+
const awmHome = path_1.default.join(root, 'awm-home');
|
|
27
|
+
const registryRoot = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
28
|
+
const literal = shellSensitiveLiteral;
|
|
29
|
+
writeJson(path_1.default.join(project, 'package.json'), { name: 'structured-run-fixture', private: true });
|
|
30
|
+
// Local metadata is the compatibility evidence. It is not a tool found in a
|
|
31
|
+
// global installation, so init and run must keep resolving this fixture's pack.
|
|
32
|
+
writeJson(path_1.default.join(project, 'node_modules', 'fixture-sensor', 'package.json'), {
|
|
33
|
+
name: 'fixture-sensor', version: '1.0.0',
|
|
34
|
+
});
|
|
35
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.config.mjs'), 'export default {};\n');
|
|
36
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.mjs'), [
|
|
37
|
+
"import fs from 'fs';",
|
|
38
|
+
"fs.writeFileSync('structured-argv.json', JSON.stringify(process.argv.slice(2)));",
|
|
39
|
+
].join('\n'));
|
|
40
|
+
writeJson(path_1.default.join(registryRoot, 'sensor-packs', 'fixture', 'pack.json'), {
|
|
41
|
+
schemaVersion: 2,
|
|
42
|
+
name: 'fixture',
|
|
43
|
+
description: 'compiled structured-run fixture',
|
|
44
|
+
detects: ['package.json'],
|
|
45
|
+
sensors: {
|
|
46
|
+
structured: {
|
|
47
|
+
applicability: { allFiles: ['package.json'] },
|
|
48
|
+
fast: true,
|
|
49
|
+
variants: [{
|
|
50
|
+
id: 'fixture-v1', priority: 100,
|
|
51
|
+
requirements: {
|
|
52
|
+
tool: 'fixture-sensor', toolRange: '>=1 <2',
|
|
53
|
+
runtime: 'node', runtimeRange: '>=20', configFiles: ['fixture-sensor.config.mjs'],
|
|
54
|
+
},
|
|
55
|
+
certifiedRange: '>=1 <2',
|
|
56
|
+
// `node` is resolved from the process environment, but the actual
|
|
57
|
+
// executable payload is the fixture-local script. Keeping the
|
|
58
|
+
// literal as one argv element proves no shell parses it.
|
|
59
|
+
command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs', literal] },
|
|
60
|
+
assets: ['fixture-sensor.config.mjs'], formatter: 'generic', probe: { kind: 'config-present' },
|
|
61
|
+
}],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
coverage: {
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
classes: {
|
|
67
|
+
'fixture-output': {
|
|
68
|
+
description: 'fixture structured output', detectors: [{ sensor: 'structured' }],
|
|
69
|
+
remedy: { summary: 'run the fixture sensor', command: 'awm sensors init --pack fixture' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
writeJson(path_1.default.join(awmHome, 'registries.json'), [{ name: 'baseline', remote: 'fixture' }]);
|
|
75
|
+
return { root, project, awmHome, registryRoot, literal };
|
|
76
|
+
}
|
|
77
|
+
function runCli(fixture, ...args) {
|
|
78
|
+
return (0, child_process_1.spawnSync)(process.execPath, [bin, 'sensors', ...args], {
|
|
79
|
+
cwd: fixture.project,
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function assertSuccessfulCli(result, phase, requireStdout = false) {
|
|
85
|
+
const details = [
|
|
86
|
+
`status=${String(result.status)}`,
|
|
87
|
+
`signal=${String(result.signal)}`,
|
|
88
|
+
`error=${result.error ? `${result.error.name}: ${result.error.message}` : 'none'}`,
|
|
89
|
+
`stdout=${JSON.stringify(result.stdout ?? '')}`,
|
|
90
|
+
`stderr=${JSON.stringify(result.stderr ?? '')}`,
|
|
91
|
+
].join('\n');
|
|
92
|
+
if (result.error || result.status !== 0 || result.signal !== null) {
|
|
93
|
+
throw new Error(`${phase} child process failed:\n${details}`);
|
|
94
|
+
}
|
|
95
|
+
if (requireStdout && !(result.stdout ?? '').trim()) {
|
|
96
|
+
throw new Error(`${phase} child process emitted no JSON:\n${details}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
beforeAll(() => {
|
|
100
|
+
if (!fs_1.default.existsSync(bin))
|
|
101
|
+
throw new Error(`Structured sensor E2E requires the compiled CLI at ${bin}; run npm run build before this test.`);
|
|
102
|
+
});
|
|
103
|
+
test('compiled sensors run materializes a v2 registry command and passes its literal argv without a shell', () => {
|
|
104
|
+
const fixture = createFixture();
|
|
105
|
+
try {
|
|
106
|
+
const initialized = runCli(fixture, 'init', '--registry-root', fixture.registryRoot, '--pack', 'fixture', '--no-configure');
|
|
107
|
+
assertSuccessfulCli(initialized, 'init');
|
|
108
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(fixture.project, '.awm', 'sensors.json'), 'utf8'));
|
|
109
|
+
expect(manifest).toMatchObject({
|
|
110
|
+
schemaVersion: 2,
|
|
111
|
+
pack: 'fixture',
|
|
112
|
+
sensors: { structured: { variantId: 'fixture-v1', command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs', fixture.literal] } } },
|
|
113
|
+
});
|
|
114
|
+
const result = runCli(fixture, 'run', '--fast');
|
|
115
|
+
assertSuccessfulCli(result, 'run --fast', true);
|
|
116
|
+
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
117
|
+
overall: 'pass', sensors: [expect.objectContaining({ name: 'structured', status: 'pass' })],
|
|
118
|
+
});
|
|
119
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(fixture.project, 'structured-argv.json'), 'utf8'))).toEqual([fixture.literal]);
|
|
120
|
+
// A shell:true mutation executes the trailing `& node -e ...` (or fails to
|
|
121
|
+
// preserve argv); this sentinel is the side-effect half of the regression.
|
|
122
|
+
expect(fs_1.default.existsSync(path_1.default.join(fixture.project, 'structured-shell-injection'))).toBe(false);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
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 providers_1 = require("../../src/providers");
|
|
9
|
+
const ROOT = path_1.default.resolve(__dirname, '../../..');
|
|
10
|
+
const ACTIVE = [
|
|
11
|
+
'README.md',
|
|
12
|
+
'docs/README.md',
|
|
13
|
+
'docs/framework.md',
|
|
14
|
+
'docs/installation.md',
|
|
15
|
+
'docs/configuration.md',
|
|
16
|
+
'docs/project-setup.md',
|
|
17
|
+
'docs/agents-setup.md',
|
|
18
|
+
'docs/runbook.md',
|
|
19
|
+
'docs/cli-reference.md',
|
|
20
|
+
'docs/support-matrix.md',
|
|
21
|
+
'docs/architecture.md',
|
|
22
|
+
'docs/decisions.md',
|
|
23
|
+
'docs/guides/product-process.md',
|
|
24
|
+
'docs/guides/development-process.md',
|
|
25
|
+
'docs/guides/parallel-tracks.md',
|
|
26
|
+
];
|
|
27
|
+
const read = (file) => fs_1.default.readFileSync(path_1.default.join(ROOT, file), 'utf8');
|
|
28
|
+
function slug(text) {
|
|
29
|
+
return text
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[`*_~]/g, '')
|
|
33
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
34
|
+
.replace(/\s+/g, '-')
|
|
35
|
+
.replace(/-+/g, '-');
|
|
36
|
+
}
|
|
37
|
+
function anchors(markdown) {
|
|
38
|
+
const seen = new Map();
|
|
39
|
+
const out = new Set();
|
|
40
|
+
for (const match of markdown.matchAll(/^#{1,6}\s+(.+)$/gm)) {
|
|
41
|
+
const base = slug(match[1]);
|
|
42
|
+
const count = seen.get(base) ?? 0;
|
|
43
|
+
seen.set(base, count + 1);
|
|
44
|
+
out.add(count === 0 ? base : `${base}-${count}`);
|
|
45
|
+
}
|
|
46
|
+
for (const match of markdown.matchAll(/<a\s+(?:name|id)="([^"]+)"/g))
|
|
47
|
+
out.add(match[1]);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function localLinks(markdown) {
|
|
51
|
+
return Array.from(markdown.matchAll(/(?<!!)\[[^\]]+\]\(([^)]+)\)/g), (m) => m[1])
|
|
52
|
+
.filter((target) => !/^(?:https?:|mailto:)/.test(target));
|
|
53
|
+
}
|
|
54
|
+
describe('active documentation contract', () => {
|
|
55
|
+
it('has one reachable active editorial set (R1, R11, R14.4)', () => {
|
|
56
|
+
const navigation = read('README.md') + read('docs/README.md');
|
|
57
|
+
for (const file of ACTIVE) {
|
|
58
|
+
expect(fs_1.default.existsSync(path_1.default.join(ROOT, file))).toBe(true);
|
|
59
|
+
if (file !== 'README.md' && file !== 'docs/README.md') {
|
|
60
|
+
expect(navigation).toContain(file.replace(/^docs\//, ''));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
it('keeps every relative link and anchor valid (R14.1)', () => {
|
|
65
|
+
for (const source of ACTIVE) {
|
|
66
|
+
for (const raw of localLinks(read(source))) {
|
|
67
|
+
const [filePart, fragment] = raw.split('#');
|
|
68
|
+
const target = filePart
|
|
69
|
+
? path_1.default.resolve(path_1.default.dirname(path_1.default.join(ROOT, source)), decodeURI(filePart))
|
|
70
|
+
: path_1.default.join(ROOT, source);
|
|
71
|
+
expect(fs_1.default.existsSync(target)).toBe(true);
|
|
72
|
+
if (fragment && target.endsWith('.md')) {
|
|
73
|
+
expect(anchors(fs_1.default.readFileSync(target, 'utf8'))).toContain(decodeURI(fragment));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
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
|
+
it('documents every init provider and option (R5, R14.2)', () => {
|
|
85
|
+
const config = read('docs/configuration.md');
|
|
86
|
+
for (const agent of providers_1.AGENT_TARGETS) {
|
|
87
|
+
expect(config).toContain(`awm init --agent ${agent} --machine-only`);
|
|
88
|
+
}
|
|
89
|
+
for (const flag of ['--agent', '--machine-only', '--yes', '--json']) {
|
|
90
|
+
expect(config).toContain(`\`${flag}\``);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
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 entrypoint = path_1.default.resolve(__dirname, '../../src/index.ts');
|
|
9
|
+
describe('CLI async entrypoint contract', () => {
|
|
10
|
+
it('awaits Commander actions instead of starting them with parse()', () => {
|
|
11
|
+
const source = fs_1.default.readFileSync(entrypoint, 'utf8');
|
|
12
|
+
expect(source).toMatch(/program\.parseAsync\(\)\.catch\(/);
|
|
13
|
+
expect(source).not.toMatch(/\bprogram\.parse\(\);/);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const os_1 = __importDefault(require("os"));
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
7
9
|
describe('Jest environment isolation', () => {
|
|
8
10
|
it('does not inherit the operator Codex home', () => {
|
|
9
11
|
expect(process.env.CODEX_HOME).toBeUndefined();
|
|
@@ -11,4 +13,46 @@ describe('Jest environment isolation', () => {
|
|
|
11
13
|
it('uses a suite-owned temporary directory', () => {
|
|
12
14
|
expect(os_1.default.tmpdir()).toContain('awm-jest-');
|
|
13
15
|
});
|
|
16
|
+
it('falls back to the system temp directory when the home cache is read-only', async () => {
|
|
17
|
+
const originalEnv = { ...process.env };
|
|
18
|
+
const fallback = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-jest-fallback-'));
|
|
19
|
+
try {
|
|
20
|
+
jest.resetModules();
|
|
21
|
+
jest.doMock('os', () => ({
|
|
22
|
+
...jest.requireActual('os'),
|
|
23
|
+
homedir: () => '/readonly-home',
|
|
24
|
+
tmpdir: () => fallback,
|
|
25
|
+
}));
|
|
26
|
+
const actualFs = jest.requireActual('fs');
|
|
27
|
+
jest.doMock('fs', () => ({
|
|
28
|
+
...actualFs,
|
|
29
|
+
mkdirSync: jest.fn((target, ...args) => {
|
|
30
|
+
if (String(target).includes('readonly-home')) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return actualFs.mkdirSync(target, ...args);
|
|
34
|
+
}),
|
|
35
|
+
mkdtempSync: jest.fn((prefix, ...args) => {
|
|
36
|
+
if (prefix.includes('readonly-home')) {
|
|
37
|
+
const error = new Error('read-only');
|
|
38
|
+
error.code = 'EROFS';
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return actualFs.mkdtempSync(prefix, ...args);
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
const setup = require('../../jest.global-setup.js');
|
|
45
|
+
await expect(setup()).resolves.toBeUndefined();
|
|
46
|
+
expect(process.env.AWM_JEST_TMPDIR?.startsWith(path_1.default.join(fallback, 'awm-cache', 'awm-jest-'))).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
jest.dontMock('fs');
|
|
50
|
+
jest.dontMock('os');
|
|
51
|
+
jest.resetModules();
|
|
52
|
+
for (const key of Object.keys(process.env))
|
|
53
|
+
delete process.env[key];
|
|
54
|
+
Object.assign(process.env, originalEnv);
|
|
55
|
+
fs_1.default.rmSync(fallback, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
14
58
|
});
|