agentic-workflow-manager 3.13.0 → 3.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/doctor.js +8 -0
- package/dist/src/commands/hooks/shared.js +14 -1
- package/dist/src/commands/init.js +5 -1
- package/dist/src/commands/registry/add.js +10 -1
- package/dist/src/commands/sync.js +4 -0
- package/dist/src/commands/update.js +4 -0
- package/dist/src/core/atomic-file.js +24 -2
- package/dist/src/core/executor.js +40 -1
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/process.js +241 -9
- package/dist/src/core/paths.js +28 -10
- package/dist/src/index.js +0 -3
- package/dist/tests/commands/doctor-platform.test.js +48 -4
- package/dist/tests/commands/doctor.test.js +19 -0
- package/dist/tests/commands/hooks/install-symlink-fallback.test.js +25 -0
- package/dist/tests/commands/hooks/status.test.js +23 -3
- package/dist/tests/commands/init.test.js +39 -0
- package/dist/tests/commands/job/exec-wrapper.test.js +15 -1
- package/dist/tests/commands/job/gate-reconcile.test.js +31 -5
- package/dist/tests/commands/multi-agent-targeting.test.js +65 -0
- package/dist/tests/commands/preflight/preflight.test.js +4 -1
- package/dist/tests/commands/registry/add.test.js +27 -0
- package/dist/tests/commands/sensors/changed.test.js +13 -0
- package/dist/tests/commands/sensors/exec-windows.test.js +13 -0
- package/dist/tests/commands/sensors/exec.test.js +28 -6
- package/dist/tests/commands/sensors/formatters/ruff.test.js +22 -6
- package/dist/tests/commands/sensors/run-changed.test.js +33 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +54 -18
- package/dist/tests/commands/watch/runner.test.js +32 -2
- package/dist/tests/commands/watch/supervisor-loop.test.js +24 -3
- package/dist/tests/core/artifact-state.test.js +11 -1
- package/dist/tests/core/atomic-file-durable.test.js +48 -1
- package/dist/tests/core/atomic-file.test.js +14 -3
- package/dist/tests/core/executor.test.js +58 -0
- package/dist/tests/core/install-transaction.test.js +59 -2
- package/dist/tests/core/journal/adapter.test.js +54 -0
- package/dist/tests/core/journal/fingerprint.test.js +10 -2
- package/dist/tests/core/journal/process.test.js +208 -9
- package/dist/tests/core/journal/store.test.js +8 -2
- package/dist/tests/core/no-color.test.js +23 -0
- package/dist/tests/core/paths.test.js +32 -5
- package/dist/tests/core/registries-sync.test.js +32 -3
- package/package.json +1 -1
package/dist/src/core/paths.js
CHANGED
|
@@ -3,13 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.WINDOWS_KNOWN_GAP = void 0;
|
|
7
7
|
exports.homeDir = homeDir;
|
|
8
8
|
exports.awmHome = awmHome;
|
|
9
9
|
exports.platform = platform;
|
|
10
10
|
exports.isWindowsNative = isWindowsNative;
|
|
11
11
|
exports.platformLabel = platformLabel;
|
|
12
|
-
exports.
|
|
12
|
+
exports.noteWindowsCaveat = noteWindowsCaveat;
|
|
13
13
|
exports.resolveOnPath = resolveOnPath;
|
|
14
14
|
// cli/src/core/paths.ts
|
|
15
15
|
//
|
|
@@ -35,11 +35,14 @@ function platform() {
|
|
|
35
35
|
function isWindowsNative() {
|
|
36
36
|
return platform() === 'win32';
|
|
37
37
|
}
|
|
38
|
-
/** Human-friendly platform label for diagnostics.
|
|
38
|
+
/** Human-friendly platform label for diagnostics. Windows is a first-class,
|
|
39
|
+
* CI-verified platform since R6 (`.github/workflows/ci.yml` runs the full
|
|
40
|
+
* suite on `ubuntu-latest` + `windows-latest` on every PR) — this no longer
|
|
41
|
+
* hedges toward WSL. */
|
|
39
42
|
function platformLabel() {
|
|
40
43
|
switch (platform()) {
|
|
41
44
|
case 'win32':
|
|
42
|
-
return 'Windows (native
|
|
45
|
+
return 'Windows (native, CI-verified)';
|
|
43
46
|
case 'darwin':
|
|
44
47
|
return 'macOS';
|
|
45
48
|
case 'linux':
|
|
@@ -48,13 +51,28 @@ function platformLabel() {
|
|
|
48
51
|
return platform();
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The one honest, narrow gap left on native Windows: `awm watch`'s supervisor
|
|
56
|
+
* crash-recovery E2E tests (spawn -> identity capture -> adoption after the
|
|
57
|
+
* supervisor is killed) never converged on real `windows-latest` CI despite 4
|
|
58
|
+
* evidence-based fix attempts in R6 (WMI-based `refIsAlive`, `activitySnapshot`
|
|
59
|
+
* degraded off `ps`/`pgrep`, `spawnStructured`'s `detached` flag tried both
|
|
60
|
+
* ways) — scoped POSIX-only (`itPosix`) in `cli/tests/commands/watch/
|
|
61
|
+
* supervisor-loop.test.ts` (2 tests) and `cli/tests/commands/watch/
|
|
62
|
+
* e2e-crash.test.ts` (2 tests); see the `refIsAlive` comment in
|
|
63
|
+
* `cli/src/core/journal/process.ts` for the full investigation. This is
|
|
64
|
+
* deliberately narrow, not a blanket "some things may not work" hedge:
|
|
65
|
+
* `awm init`/`update`/`sync`/`sensors`/`preflight`/`doctor`/hooks are all
|
|
66
|
+
* exercised green by the same CI matrix and are unaffected by this gap.
|
|
67
|
+
*/
|
|
68
|
+
exports.WINDOWS_KNOWN_GAP = 'AWM on native Windows: supported and continuously verified in CI (ubuntu-latest + windows-latest, every PR).\n' +
|
|
69
|
+
' One known, narrow gap: `awm watch`\'s supervisor crash-recovery (spawn -> identity capture -> adoption\n' +
|
|
70
|
+
' after the supervisor is killed) has not yet converged on real windows-latest CI — see\n' +
|
|
71
|
+
' cli/src/core/journal/process.ts (refIsAlive) for detail. Everything else is CI-verified on Windows.';
|
|
72
|
+
/** Emit `WINDOWS_KNOWN_GAP` via the provided logger, only on native Windows. */
|
|
73
|
+
function noteWindowsCaveat(log) {
|
|
56
74
|
if (isWindowsNative())
|
|
57
|
-
log(exports.
|
|
75
|
+
log(exports.WINDOWS_KNOWN_GAP);
|
|
58
76
|
}
|
|
59
77
|
/** Resolve a binary on PATH portably: `where` on win32, POSIX `command -v` elsewhere. */
|
|
60
78
|
function resolveOnPath(bin) {
|
package/dist/src/index.js
CHANGED
|
@@ -42,7 +42,6 @@ const sync_1 = require("./commands/sync");
|
|
|
42
42
|
const update_1 = require("./commands/update");
|
|
43
43
|
const agent_targets_1 = require("./core/agent-targets");
|
|
44
44
|
const update_check_1 = require("./core/update-check");
|
|
45
|
-
const paths_1 = require("./core/paths");
|
|
46
45
|
const cli_version_1 = require("./core/cli-version");
|
|
47
46
|
const program = new commander_1.Command();
|
|
48
47
|
program.name('awm').description('Agentic Workflow Manager').version((0, cli_version_1.cliVersion)());
|
|
@@ -390,7 +389,6 @@ program.command('update')
|
|
|
390
389
|
.option('-a, --agent <agent>', `Target agent(s), comma-separated: ${providers_1.AGENT_TARGETS.join(', ')} (defaults to every enabled agent)`)
|
|
391
390
|
.action(async (options) => {
|
|
392
391
|
(0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Update Registries ')));
|
|
393
|
-
(0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
|
|
394
392
|
const result = await (0, update_1.runUpdateCore)(options);
|
|
395
393
|
(0, prompts_1.outro)(result.code === 0 ? '✅ Registries, skills and hooks updated.' : picocolors_1.default.red('Update failed — see errors above.'));
|
|
396
394
|
process.exitCode = result.code;
|
|
@@ -401,7 +399,6 @@ program.command('sync')
|
|
|
401
399
|
.option('-m, --method <method>', 'Install method: symlink or copy', 'symlink')
|
|
402
400
|
.action(async (options) => {
|
|
403
401
|
(0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Sync Project Profile ')));
|
|
404
|
-
(0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
|
|
405
402
|
const { code } = await (0, sync_1.runSyncCore)(options);
|
|
406
403
|
(0, prompts_1.outro)(code === 0 ? 'Done.' : picocolors_1.default.red('Sync failed — see errors above.'));
|
|
407
404
|
process.exitCode = code;
|
|
@@ -6,17 +6,61 @@ describe('doctor renderReport — platform line', () => {
|
|
|
6
6
|
afterEach(() => {
|
|
7
7
|
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
8
8
|
});
|
|
9
|
+
function setPlatform(p) {
|
|
10
|
+
Object.defineProperty(process, 'platform', { value: p, configurable: true });
|
|
11
|
+
}
|
|
9
12
|
function emptyReport() {
|
|
10
13
|
return { overall: 'healthy', hasProject: false, projectName: undefined, results: [] };
|
|
11
14
|
}
|
|
12
15
|
it('renders the platform label under the Machine header', () => {
|
|
13
|
-
|
|
16
|
+
setPlatform('linux');
|
|
14
17
|
const out = (0, doctor_1.renderReport)(emptyReport());
|
|
15
18
|
expect(out).toContain('platform: Linux');
|
|
16
19
|
});
|
|
17
|
-
it('
|
|
18
|
-
|
|
20
|
+
it('labels native Windows as supported and CI-verified, not deferred to WSL', () => {
|
|
21
|
+
setPlatform('win32');
|
|
22
|
+
const out = (0, doctor_1.renderReport)(emptyReport());
|
|
23
|
+
expect(out).toContain('platform: Windows (native, CI-verified)');
|
|
24
|
+
expect(out).not.toContain('WSL');
|
|
25
|
+
});
|
|
26
|
+
// `renderReport` (init's before/after dashboard) deliberately does NOT
|
|
27
|
+
// embed the Windows caveat — it used to (R7), which made a single `awm
|
|
28
|
+
// init` run print the same caveat text 3 times (once via `noteWindowsCaveat`
|
|
29
|
+
// at the top of `runInit`, once for "Initial state", once for "Final
|
|
30
|
+
// state"). The caveat now lives solely in `runInit`'s own single emission
|
|
31
|
+
// and in `renderProviderReport` below (the real `awm doctor` output).
|
|
32
|
+
it('never embeds the awm-watch supervisor caveat in the init before/after dashboard', () => {
|
|
33
|
+
setPlatform('win32');
|
|
19
34
|
const out = (0, doctor_1.renderReport)(emptyReport());
|
|
20
|
-
expect(out).
|
|
35
|
+
expect(out).not.toMatch(/awm watch/i);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
// Regression coverage for the confirmed bug: the `WINDOWS_KNOWN_GAP` caveat
|
|
39
|
+
// used to live only inside `renderReport`, whose only callers are `init.ts`'s
|
|
40
|
+
// before/after dashboard — `awm doctor`'s REAL command path
|
|
41
|
+
// (`runDoctor` → `renderProviderReport`) never touched it, so a user running
|
|
42
|
+
// `awm doctor` on native Windows never saw the caveat its own code comment
|
|
43
|
+
// claimed doctor was the place for. This exercises `renderProviderReport`
|
|
44
|
+
// directly, the function `runDoctor` actually calls for its text output.
|
|
45
|
+
describe('doctor renderProviderReport — platform caveat (the real `awm doctor` path)', () => {
|
|
46
|
+
const realPlatform = process.platform;
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
49
|
+
});
|
|
50
|
+
function setPlatform(p) {
|
|
51
|
+
Object.defineProperty(process, 'platform', { value: p, configurable: true });
|
|
52
|
+
}
|
|
53
|
+
function emptyProviderReport() {
|
|
54
|
+
return { overall: 'healthy', providers: [] };
|
|
55
|
+
}
|
|
56
|
+
it('surfaces the narrow awm-watch supervisor caveat on native Windows only, not on macOS or Linux', () => {
|
|
57
|
+
setPlatform('win32');
|
|
58
|
+
const winOut = (0, doctor_1.renderProviderReport)(emptyProviderReport());
|
|
59
|
+
expect(winOut).toMatch(/awm watch/i);
|
|
60
|
+
for (const p of ['linux', 'darwin']) {
|
|
61
|
+
setPlatform(p);
|
|
62
|
+
const out = (0, doctor_1.renderProviderReport)(emptyProviderReport());
|
|
63
|
+
expect(out).not.toMatch(/awm watch/i);
|
|
64
|
+
}
|
|
21
65
|
});
|
|
22
66
|
});
|
|
@@ -121,6 +121,25 @@ describe('runDoctor', () => {
|
|
|
121
121
|
const code = (0, doctor_1.runDoctor)({ cwd: tmpHome });
|
|
122
122
|
expect(code).toBe(1);
|
|
123
123
|
});
|
|
124
|
+
// Finding #1 (R7 QA): the real `awm doctor` command (this `runDoctor`,
|
|
125
|
+
// text mode — not `renderReport`, which only `init.ts` calls) must
|
|
126
|
+
// actually surface the Windows caveat on native Windows.
|
|
127
|
+
it('surfaces the Windows caveat in real text output on native Windows only', () => {
|
|
128
|
+
const realPlatform = process.platform;
|
|
129
|
+
try {
|
|
130
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
131
|
+
writeSpy.mockClear();
|
|
132
|
+
(0, doctor_1.runDoctor)({ cwd: tmpHome });
|
|
133
|
+
expect(stdout()).toMatch(/awm watch/i);
|
|
134
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
135
|
+
writeSpy.mockClear();
|
|
136
|
+
(0, doctor_1.runDoctor)({ cwd: tmpHome });
|
|
137
|
+
expect(stdout()).not.toMatch(/awm watch/i);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
124
143
|
it('--json emits a parseable provider report and keeps the same exit code', () => {
|
|
125
144
|
const code = (0, doctor_1.runDoctor)({ cwd: tmpHome, json: true });
|
|
126
145
|
const parsed = JSON.parse(stdout());
|
|
@@ -56,4 +56,29 @@ describe('hooks/install — skill symlink fallback to copy', () => {
|
|
|
56
56
|
expect(fs_1.default.lstatSync(skillDest).isSymbolicLink()).toBe(false); // it was copied, not linked
|
|
57
57
|
expect(fs_1.default.readFileSync(skillDest, 'utf-8')).toContain('using-awm');
|
|
58
58
|
});
|
|
59
|
+
// Regression: syncExecutable (shared.ts) — used for the hook SCRIPT files
|
|
60
|
+
// (session-start, run-hook.cmd), not just the bootstrap skill above — called
|
|
61
|
+
// fs.symlinkSync unconditionally when installMethod is 'symlink', with no
|
|
62
|
+
// EPERM fallback at all. On native Windows without SeCreateSymbolicLinkPrivilege
|
|
63
|
+
// (the default for GitHub Actions' windows-latest runner), that would throw
|
|
64
|
+
// out of installHook uninstall, failing the whole `awm init` step. Proves the
|
|
65
|
+
// same fallback-to-copy this file already established for the skill file also
|
|
66
|
+
// now covers the script files when the caller actually requests 'symlink'.
|
|
67
|
+
it('copies the hook scripts when symlink throws (EPERM), instead of throwing out of installHook', () => {
|
|
68
|
+
const registryRoot = path_1.default.join(tmpHome, 'registry');
|
|
69
|
+
seedRegistry(registryRoot);
|
|
70
|
+
symlinkSpy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => {
|
|
71
|
+
const err = new Error('EPERM: operation not permitted, symlink');
|
|
72
|
+
err.code = 'EPERM';
|
|
73
|
+
throw err;
|
|
74
|
+
});
|
|
75
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
76
|
+
const result = installHook({ agent: 'claude-code', registryRoot, installMethod: 'symlink' });
|
|
77
|
+
const scriptDest = path_1.default.join(result.scriptsDir, 'session-start');
|
|
78
|
+
const wrapperDest = path_1.default.join(result.scriptsDir, 'run-hook.cmd');
|
|
79
|
+
expect(fs_1.default.existsSync(scriptDest)).toBe(true);
|
|
80
|
+
expect(fs_1.default.lstatSync(scriptDest).isSymbolicLink()).toBe(false); // copied, not linked
|
|
81
|
+
expect(fs_1.default.existsSync(wrapperDest)).toBe(true);
|
|
82
|
+
expect(fs_1.default.lstatSync(wrapperDest).isSymbolicLink()).toBe(false);
|
|
83
|
+
});
|
|
59
84
|
});
|
|
@@ -78,8 +78,23 @@ describe('computeHookStatus', () => {
|
|
|
78
78
|
fs_1.default.chmodSync(path_1.default.join(tmpHome, '.awm/hooks/session-start'), 0o644);
|
|
79
79
|
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
80
80
|
const result = computeHookStatus('claude-code');
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
// Windows has no POSIX executable-bit concept at all, and this isn't a
|
|
83
|
+
// gap in computeHookStatus's checkExecutable() — it's Node's own
|
|
84
|
+
// documented behavior: fs.accessSync(file, X_OK) "has no effect on
|
|
85
|
+
// Windows (will behave like fs.constants.F_OK)" (Node fs docs). So
|
|
86
|
+
// chmod(0o644) here only clears write bits, which Windows collapses
|
|
87
|
+
// into "still not read-only" either way — there was never a distinct
|
|
88
|
+
// exec permission to remove, and the script remains just as runnable
|
|
89
|
+
// (via its interpreter/file association) as before the chmod. HEALTHY
|
|
90
|
+
// is the factually correct report here, not a gap to paper over.
|
|
91
|
+
expect(result.overall).toBe('HEALTHY');
|
|
92
|
+
expect(result.checks.sessionStartScript.ok).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
expect(result.overall).toBe('DEGRADED');
|
|
96
|
+
expect(result.checks.sessionStartScript.ok).toBe(false);
|
|
97
|
+
}
|
|
83
98
|
});
|
|
84
99
|
it('throws when agent target has no hooks config', () => {
|
|
85
100
|
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
@@ -91,7 +106,12 @@ describe('computeHookStatus', () => {
|
|
|
91
106
|
// never touching Claude's settings.json path.
|
|
92
107
|
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
93
108
|
const result = computeHookStatus('codex');
|
|
94
|
-
|
|
109
|
+
// Separator-agnostic: the detail embeds a real OS path (`path.join`
|
|
110
|
+
// under the hood), so it's `\` on windows-latest and `/` elsewhere —
|
|
111
|
+
// assert the two path segments independently rather than one
|
|
112
|
+
// POSIX-shaped fragment.
|
|
113
|
+
expect(result.checks.settingsEntry.detail).toContain('.codex');
|
|
114
|
+
expect(result.checks.settingsEntry.detail).toContain('hooks.json');
|
|
95
115
|
expect(result.checks.bootstrapSkill).toBeUndefined();
|
|
96
116
|
expect(result.checks.runHookWrapper).toBeUndefined();
|
|
97
117
|
});
|
|
@@ -70,6 +70,45 @@ describe('runInit', () => {
|
|
|
70
70
|
else
|
|
71
71
|
process.env.AWM_HOME = originalAwmHome;
|
|
72
72
|
});
|
|
73
|
+
// Finding #2/#3 (R7 QA): the Windows caveat used to fire via a direct
|
|
74
|
+
// call in registerInitCommand's Commander `.action()` closure — a call
|
|
75
|
+
// site no test exercised, since every other `runInit` test here bypasses
|
|
76
|
+
// the Commander wrapper entirely. It now lives at the top of `runInit`
|
|
77
|
+
// itself (the unit these tests already drive directly), firing at most
|
|
78
|
+
// once per run, native Windows only.
|
|
79
|
+
describe('Windows caveat (noteWindowsCaveat)', () => {
|
|
80
|
+
const realPlatform = process.platform;
|
|
81
|
+
let logSpy;
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
84
|
+
});
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
logSpy.mockRestore();
|
|
87
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
88
|
+
});
|
|
89
|
+
it('logs the caveat exactly once on native Windows', async () => {
|
|
90
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
91
|
+
const { runInit } = require('../../src/commands/init');
|
|
92
|
+
await runInit({
|
|
93
|
+
cwd: tmpHome,
|
|
94
|
+
yes: true,
|
|
95
|
+
actions: { syncCache: async () => { }, installHook: () => ({ status: 'installed' }) },
|
|
96
|
+
});
|
|
97
|
+
const caveatCalls = logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
|
|
98
|
+
expect(caveatCalls).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
it('never logs the caveat on non-Windows platforms', async () => {
|
|
101
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
102
|
+
const { runInit } = require('../../src/commands/init');
|
|
103
|
+
await runInit({
|
|
104
|
+
cwd: tmpHome,
|
|
105
|
+
yes: true,
|
|
106
|
+
actions: { syncCache: async () => { }, installHook: () => ({ status: 'installed' }) },
|
|
107
|
+
});
|
|
108
|
+
const caveatCalls = logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
|
|
109
|
+
expect(caveatCalls).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
73
112
|
it('returns exit 1 on a bare HOME and never prompts with --yes (cache stubbed)', async () => {
|
|
74
113
|
const { runInit } = require('../../src/commands/init');
|
|
75
114
|
const code = await runInit({
|
|
@@ -8,6 +8,11 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
|
|
10
10
|
const process_1 = require("../../../src/core/journal/process");
|
|
11
|
+
// Real subprocess spawn + termination + fsync per test; the jest default of
|
|
12
|
+
// 5000ms proved too tight on a loaded windows-latest CI runner (regression:
|
|
13
|
+
// real CI, "Exceeded timeout of 5000 ms"). Same class of issue already fixed
|
|
14
|
+
// in registries-sync.test.ts this round.
|
|
15
|
+
jest.setTimeout(20000);
|
|
11
16
|
describe('exec-wrapper', () => {
|
|
12
17
|
let dir;
|
|
13
18
|
beforeEach(() => { dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrap-')); });
|
|
@@ -19,7 +24,16 @@ describe('exec-wrapper', () => {
|
|
|
19
24
|
const identity = JSON.parse(fs_1.default.readFileSync((0, exec_wrapper_1.identityPath)(dir, 'job1', 'nonceA'), 'utf8'));
|
|
20
25
|
expect(identity.wrapper.pid).toBe(process.pid); // ProcessRef REAL del wrapper
|
|
21
26
|
expect(identity.command.pid).toBeGreaterThan(0); // ProcessRef REAL del comando
|
|
22
|
-
|
|
27
|
+
// hex real cuando la plataforma pudo observar el proceso (ps en
|
|
28
|
+
// POSIX, WMI/powershell en win32 — ver captureRefFor en
|
|
29
|
+
// src/core/journal/process.ts); sentinel 'unknown' documentado
|
|
30
|
+
// cuando esa observacion no estuvo disponible (ps ausente, o en
|
|
31
|
+
// win32 powershell/WMI restringido/deshabilitado). Mismo criterio
|
|
32
|
+
// ya establecido en tests/core/journal/process.test.ts — el formato
|
|
33
|
+
// exacto de este campo nunca es la fuente de verdad de vida/muerte
|
|
34
|
+
// (esa es refIsAlive), asi que este test no puede exigir mas
|
|
35
|
+
// certeza de la que la plataforma real puede dar.
|
|
36
|
+
expect(identity.command.psArgsDigest).toMatch(/^([0-9a-f]{16}|unknown)$/);
|
|
23
37
|
expect(identity.command.processGroup).not.toBe(identity.wrapper.processGroup); // el wrapper puede limpiar el grupo sin matarse
|
|
24
38
|
const result = JSON.parse(fs_1.default.readFileSync((0, exec_wrapper_1.resultPath)(dir, 'job1', 'nonceA'), 'utf8'));
|
|
25
39
|
expect(result.exitCode).toBe(0);
|
|
@@ -272,14 +272,40 @@ describe('reap — limpieza explicita con identidad validada (R2.2)', () => {
|
|
|
272
272
|
const deadRef = { pid: 999999, startTime: 'gone', spawnNonce: 'n1', argvDigest: 'd', processGroup: 999999, psArgsDigest: 'x' };
|
|
273
273
|
s.jobs['sinRef'] = job({ id: 'sinRef', executionState: 'running' });
|
|
274
274
|
s.jobs['muerto'] = job({ id: 'muerto', executionState: 'running', processRef: deadRef });
|
|
275
|
-
|
|
275
|
+
// deadRef.pid (999999) debe comportarse como un pid REALMENTE
|
|
276
|
+
// ausente ante un sondeo de existencia (signal 0) — igual que en
|
|
277
|
+
// windows-latest real, donde `isWindowsNative()` es cierto de
|
|
278
|
+
// verdad y refIsAlive/groupIsGone dependen de pidExistsNative
|
|
279
|
+
// (process.kill(pid, 0)) como UNICA fuente de veredicto (ronda 3,
|
|
280
|
+
// ver process.ts). Mockear esto como "siempre exito" incondicional
|
|
281
|
+
// rompia esa unica fuente de verdad en CI real: pidExistsNative
|
|
282
|
+
// reportaba "vivo" para un pid que nunca existio, y la escalera de
|
|
283
|
+
// terminacion quedaba reintentando hasta el timeout del test — no
|
|
284
|
+
// por una señal real enviada, sino porque nunca podia CONFIRMAR
|
|
285
|
+
// ausencia. En POSIX este spy ni siquiera se ejercita (refIsAlive
|
|
286
|
+
// ahi usa ps/pgrep, no process.kill), asi que el fix solo cambia
|
|
287
|
+
// comportamiento win32.
|
|
288
|
+
const killSpy = jest.spyOn(process, 'kill').mockImplementation((pid) => {
|
|
289
|
+
if (pid === deadRef.pid) {
|
|
290
|
+
const err = new Error('no such process');
|
|
291
|
+
err.code = 'ESRCH';
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
return true;
|
|
295
|
+
});
|
|
276
296
|
try {
|
|
277
297
|
const killed = await (0, reap_1.executeReap)(s, ['sinRef', 'muerto', 'no-existe']);
|
|
278
298
|
expect(killed).toEqual([]);
|
|
279
|
-
// no solo el resultado: nunca se
|
|
280
|
-
// la ausencia de identidad viva confirmada
|
|
281
|
-
// a terminateGroupConfirmed
|
|
282
|
-
|
|
299
|
+
// no solo el resultado: nunca se envio una señal REAL de
|
|
300
|
+
// terminacion (R2.1) — la ausencia de identidad viva confirmada
|
|
301
|
+
// corta antes de llegar a terminateGroupConfirmed. pidExistsNative
|
|
302
|
+
// SI llama a process.kill(pid, 0) como sondeo de existencia
|
|
303
|
+
// (signal 0, no una señal real) — eso aparece legitimamente en
|
|
304
|
+
// este spy y no es evidencia de una señal enviada; se filtra
|
|
305
|
+
// explicitamente (mismo patron ya aplicado en adapter.test.ts
|
|
306
|
+
// para execFileSync, narrowing en vez de "cero llamadas").
|
|
307
|
+
const realSignals = killSpy.mock.calls.filter((call) => call[1] !== 0);
|
|
308
|
+
expect(realSignals).toEqual([]);
|
|
283
309
|
}
|
|
284
310
|
finally {
|
|
285
311
|
killSpy.mockRestore();
|
|
@@ -208,6 +208,71 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
208
208
|
});
|
|
209
209
|
expect(outcome.code).toBe(1); // no silent catch{} — verifies Step 6's "no aborta" removal
|
|
210
210
|
});
|
|
211
|
+
// Finding #3 (R7 QA): `noteWindowsCaveat` used to be called only from the
|
|
212
|
+
// raw Commander `.action()` closures in `index.ts` (update.ts:441,
|
|
213
|
+
// sync.ts:453 pre-fix) — call sites no test exercised, since `update`/
|
|
214
|
+
// `sync` are always driven here via `runUpdateCore`/`runSyncCore`
|
|
215
|
+
// directly. It now lives at the top of those core functions themselves,
|
|
216
|
+
// firing at most once per run, native Windows only.
|
|
217
|
+
describe('Windows caveat (noteWindowsCaveat) — update & sync', () => {
|
|
218
|
+
const realPlatform = process.platform;
|
|
219
|
+
let logSpy;
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
222
|
+
});
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
logSpy.mockRestore();
|
|
225
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
226
|
+
});
|
|
227
|
+
function caveatCalls() {
|
|
228
|
+
return logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
|
|
229
|
+
}
|
|
230
|
+
it('update logs the caveat exactly once on native Windows, never on Linux', async () => {
|
|
231
|
+
writePrefs(['claude-code']);
|
|
232
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
233
|
+
const deps = {
|
|
234
|
+
syncRegistries: async () => [],
|
|
235
|
+
verifyMinCliVersions: () => [],
|
|
236
|
+
regenerateGlobalContext: () => [],
|
|
237
|
+
planReconciliation: () => ({ operations: [], records: [], reports: [] }),
|
|
238
|
+
applyInstallPlan: () => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }),
|
|
239
|
+
resyncInstalledHooks: () => [],
|
|
240
|
+
offerSelfUpdate: async () => { },
|
|
241
|
+
};
|
|
242
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
243
|
+
await runUpdateCore({}, deps);
|
|
244
|
+
expect(caveatCalls()).toHaveLength(1);
|
|
245
|
+
logSpy.mockClear();
|
|
246
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
247
|
+
await runUpdateCore({}, deps);
|
|
248
|
+
expect(caveatCalls()).toHaveLength(0);
|
|
249
|
+
});
|
|
250
|
+
it('sync logs the caveat exactly once on native Windows, never on Linux', async () => {
|
|
251
|
+
writePrefs(['claude-code']);
|
|
252
|
+
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sync-project-'));
|
|
253
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
254
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: [] }));
|
|
255
|
+
const { runSyncCore } = require('../../src/commands/sync');
|
|
256
|
+
const deps = {
|
|
257
|
+
syncRegistries: async () => [],
|
|
258
|
+
verifyMinCliVersions: () => [],
|
|
259
|
+
verifyProjectPins: async () => [],
|
|
260
|
+
syncProfile: () => ({ installed: [], skipped: [], extensions: [], transactionIds: [], modifiedFiles: [] }),
|
|
261
|
+
};
|
|
262
|
+
try {
|
|
263
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
264
|
+
await runSyncCore({ cwd: projectRoot }, deps);
|
|
265
|
+
expect(caveatCalls()).toHaveLength(1);
|
|
266
|
+
logSpy.mockClear();
|
|
267
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
268
|
+
await runSyncCore({ cwd: projectRoot }, deps);
|
|
269
|
+
expect(caveatCalls()).toHaveLength(0);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
211
276
|
it('sync reports a syncProfile failure and returns a non-zero exit code instead of crashing uncaught', async () => {
|
|
212
277
|
writePrefs(['claude-code'], 'claude-code');
|
|
213
278
|
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sync-project-'));
|
|
@@ -167,8 +167,11 @@ describe('preflight', () => {
|
|
|
167
167
|
it('reports github + gh available, and does not affect status', () => {
|
|
168
168
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
169
169
|
gitRepo(dir, 'git@github.com:kodria/agentic-workflow.git');
|
|
170
|
+
// `resolveOnPath('gh')` runs `command -v gh` on POSIX but `where gh` on
|
|
171
|
+
// win32 (see paths.ts) — match both invocation forms so this test's
|
|
172
|
+
// "gh is available" fixture holds on windows-latest CI too.
|
|
170
173
|
mockExecSync.mockImplementation(((cmd) => {
|
|
171
|
-
if (cmd === 'command -v gh')
|
|
174
|
+
if (cmd === 'command -v gh' || cmd === 'where gh')
|
|
172
175
|
return Buffer.from('/usr/bin/gh');
|
|
173
176
|
throw new Error(`not found: ${cmd}`);
|
|
174
177
|
}));
|
|
@@ -24,6 +24,33 @@ function makeSourceRepo(base, opts) {
|
|
|
24
24
|
GIT(dir, 'commit -qm init');
|
|
25
25
|
return dir;
|
|
26
26
|
}
|
|
27
|
+
describe('deriveRegistryName', () => {
|
|
28
|
+
// Regression: on native Windows, a local clone source is a backslash-separated
|
|
29
|
+
// path (e.g. `C:\Users\runner\AppData\Local\Temp\awm-regadd-work-xyz\src-alpha`).
|
|
30
|
+
// The old `split(/[/:]/)` only split on '/' and ':', so the whole backslash-joined
|
|
31
|
+
// tail after the drive letter's ':' survived as one segment — `deriveRegistryName`
|
|
32
|
+
// returned a name containing backslashes, which addRegistry's own
|
|
33
|
+
// `/[/\\]/.test(name)` guard then rejected as invalid, making every
|
|
34
|
+
// `addRegistry(localWindowsPath)` call fail with "Invalid registry name" on
|
|
35
|
+
// windows-latest CI (got `result.ok === false` instead of `true`).
|
|
36
|
+
it('derives the basename from a Windows-style backslash path', () => {
|
|
37
|
+
const { deriveRegistryName } = require('../../../src/commands/registry/add');
|
|
38
|
+
const winPath = 'C:\\Users\\runner\\AppData\\Local\\Temp\\awm-regadd-work-xyz\\src-alpha';
|
|
39
|
+
expect(deriveRegistryName(winPath)).toBe('src-alpha');
|
|
40
|
+
});
|
|
41
|
+
it('derives the basename from a POSIX path (unchanged behavior)', () => {
|
|
42
|
+
const { deriveRegistryName } = require('../../../src/commands/registry/add');
|
|
43
|
+
expect(deriveRegistryName('/tmp/awm-regadd-work-xyz/src-alpha')).toBe('src-alpha');
|
|
44
|
+
});
|
|
45
|
+
it('still strips a trailing .git and derives from an https remote URL', () => {
|
|
46
|
+
const { deriveRegistryName } = require('../../../src/commands/registry/add');
|
|
47
|
+
expect(deriveRegistryName('https://github.com/Kodria/awm-baseline-registry.git')).toBe('awm-baseline-registry');
|
|
48
|
+
});
|
|
49
|
+
it('still derives from an SSH-style remote (host:org/repo.git)', () => {
|
|
50
|
+
const { deriveRegistryName } = require('../../../src/commands/registry/add');
|
|
51
|
+
expect(deriveRegistryName('git@github.com:Kodria/awm-baseline-registry.git')).toBe('awm-baseline-registry');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
27
54
|
describe('addRegistry', () => {
|
|
28
55
|
let tmpHome;
|
|
29
56
|
let tmpWork;
|
|
@@ -80,6 +80,19 @@ describe('changedFiles', () => {
|
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
describe('applyChangedCmd', () => {
|
|
83
|
+
// shellQuote (changed.ts) branches on isWindowsNative(), which reads real
|
|
84
|
+
// process.platform. These assertions exercise the POSIX single-quote branch —
|
|
85
|
+
// pin the platform so the expectations are deterministic on windows-latest CI too,
|
|
86
|
+
// instead of silently asserting whatever the CI runner's real OS happens to be.
|
|
87
|
+
// The win32 double-quote branch is covered separately in changed-windows.test.ts.
|
|
88
|
+
// Pattern: AGENTS.md "stub-process-platform".
|
|
89
|
+
const originalPlatform = process.platform;
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
95
|
+
});
|
|
83
96
|
it('substitutes the file list into the template', () => {
|
|
84
97
|
expect((0, changed_1.applyChangedCmd)('eslint --format json {files}', ['a.ts', 'b.ts']))
|
|
85
98
|
.toBe(`eslint --format json 'a.ts' 'b.ts'`);
|
|
@@ -38,6 +38,19 @@ describe('runCommand — win32', () => {
|
|
|
38
38
|
const r = await pending;
|
|
39
39
|
expect(r.code).toBe(0);
|
|
40
40
|
});
|
|
41
|
+
it('propagates cmd.exe\'s own exit code for a command that does not exist (1, not the POSIX 127)', async () => {
|
|
42
|
+
// Mirrors exec.test.ts's POSIX "reports 127" case. cmd.exe has no
|
|
43
|
+
// equivalent 127 convention — it reports 1 (sometimes 9009) for
|
|
44
|
+
// "not recognized as an internal or external command" — and
|
|
45
|
+
// runCommand does not remap it: whatever the shell's `close` event
|
|
46
|
+
// carries is exactly what comes out on `r.code`.
|
|
47
|
+
const child = fakeChild();
|
|
48
|
+
mockSpawn.mockReturnValue(child);
|
|
49
|
+
const pending = (0, exec_1.runCommand)('awm-definitely-not-a-real-binary-xyz', { timeout: 5000, cwd: process.cwd() });
|
|
50
|
+
child.emit('close', 1, null);
|
|
51
|
+
const r = await pending;
|
|
52
|
+
expect(r.code).toBe(1);
|
|
53
|
+
});
|
|
41
54
|
it('kills via `taskkill /pid <pid> /T /F` on timeout, never the POSIX process.kill(-pid) path', async () => {
|
|
42
55
|
jest.useFakeTimers();
|
|
43
56
|
const child = fakeChild(4242);
|
|
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const exec_1 = require("../../../src/commands/sensors/exec");
|
|
10
10
|
const onPosix = process.platform !== 'win32' ? describe : describe.skip;
|
|
11
|
+
const itPosix = process.platform !== 'win32' ? it : it.skip;
|
|
11
12
|
/** Poll until `fn()` is true or the budget runs out. Avoids fixed sleeps. */
|
|
12
13
|
async function until(fn, budgetMs = 4000) {
|
|
13
14
|
const deadline = Date.now() + budgetMs;
|
|
@@ -27,22 +28,43 @@ describe('runCommand — exit codes and output', () => {
|
|
|
27
28
|
expect(r.overflowed).toBe(false);
|
|
28
29
|
});
|
|
29
30
|
it('captures stderr and a non-zero exit code without throwing', async () => {
|
|
30
|
-
|
|
31
|
+
// Portable by construction: `node -e "..."` is invoked identically by
|
|
32
|
+
// `spawn(cmd, {shell:true})` on both `/bin/sh -c` (POSIX) and
|
|
33
|
+
// `cmd.exe /d /s /c` (win32) — the shell only tokenizes the outer
|
|
34
|
+
// double-quoted argument, and node's own -e parsing is platform-
|
|
35
|
+
// independent from there. A previous version of this test used
|
|
36
|
+
// POSIX-only shell syntax (`;` as a separator, `1>&2` redirect
|
|
37
|
+
// ordering) that cmd.exe does not support: `;` isn't a command
|
|
38
|
+
// separator there, so the whole string became literal arguments to
|
|
39
|
+
// `echo` and `exit 3` never ran as its own command — the run
|
|
40
|
+
// "succeeded" with code 0 instead of 3 on windows-latest CI.
|
|
41
|
+
const r = await (0, exec_1.runCommand)(`node -e "process.stderr.write('oops'); process.exit(3)"`, { timeout: 5000, cwd: process.cwd() });
|
|
31
42
|
expect(r.code).toBe(3);
|
|
32
43
|
expect(r.stderr).toMatch(/oops/);
|
|
33
44
|
expect(r.timedOut).toBe(false);
|
|
34
45
|
});
|
|
35
|
-
|
|
46
|
+
itPosix('reports 127 for a command that does not exist', async () => {
|
|
47
|
+
// 127 is the POSIX shell's own "command not found" convention (`/bin/sh
|
|
48
|
+
// -c`), not something this codebase computes — runCommand just relays
|
|
49
|
+
// whatever the shell's `close` event reports. cmd.exe has no such
|
|
50
|
+
// convention (it reports 1 for "not recognized..."), so this is
|
|
51
|
+
// POSIX-only; see exec-windows.test.ts for the win32 equivalent.
|
|
36
52
|
const r = await (0, exec_1.runCommand)('awm-definitely-not-a-real-binary-xyz', { timeout: 5000, cwd: process.cwd() });
|
|
37
53
|
expect(r.code).toBe(127);
|
|
38
54
|
});
|
|
39
55
|
});
|
|
40
56
|
describe('runCommand — output cap', () => {
|
|
41
57
|
it('stops at maxBuffer, flags overflow, and keeps what it read', async () => {
|
|
42
|
-
// 200 lines of ~50 bytes each, capped at 1KB.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
// 200 lines of ~50 bytes each, capped at 1KB. A `for i in $(seq ...); do
|
|
59
|
+
// ... done` POSIX shell loop silently no-ops under cmd.exe (win32's
|
|
60
|
+
// spawn(cmd, {shell:true}) target) instead of erroring — cmd.exe has
|
|
61
|
+
// no `$(...)`/`do...done` syntax, so the whole string is passed through
|
|
62
|
+
// largely inert and stdout never reaches the cap (regression: this test
|
|
63
|
+
// isn't POSIX-scoped, so it ran for-real on windows-latest CI and
|
|
64
|
+
// r.overflowed came back false). A `node -e` one-liner is invoked
|
|
65
|
+
// identically by both shells (same portability reasoning as the
|
|
66
|
+
// exit-code test above).
|
|
67
|
+
const r = await (0, exec_1.runCommand)(`node -e "for(let i=1;i<=200;i++){console.log('line-'+i+'-padding-padding-padding-padding')}"`, { timeout: 10_000, cwd: process.cwd(), maxBuffer: 1024 });
|
|
46
68
|
expect(r.overflowed).toBe(true);
|
|
47
69
|
expect(r.stdout.length).toBeLessThanOrEqual(1024);
|
|
48
70
|
// The point of the cap change: what was read is still usable, not discarded.
|