agentic-workflow-manager 3.13.1 → 3.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/doctor.js +8 -0
- package/dist/src/commands/init.js +5 -1
- package/dist/src/commands/sync.js +4 -0
- package/dist/src/commands/update.js +4 -0
- package/dist/src/core/journal/process.js +43 -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/init.test.js +39 -0
- package/dist/tests/commands/multi-agent-targeting.test.js +65 -0
- package/dist/tests/core/journal/process.test.js +16 -0
- package/dist/tests/core/paths.test.js +32 -5
- package/package.json +1 -1
|
@@ -99,6 +99,14 @@ function providerCheckLine(check) {
|
|
|
99
99
|
function renderProviderReport(report) {
|
|
100
100
|
const lines = [];
|
|
101
101
|
lines.push(picocolors_1.default.bold('AWM · harness status'));
|
|
102
|
+
lines.push(picocolors_1.default.dim(` platform: ${(0, paths_1.platformLabel)()}`));
|
|
103
|
+
// Advisory only, native Windows only. This is the text rendered by the
|
|
104
|
+
// real `awm doctor` command (`runDoctor` below) — the diagnostics surface
|
|
105
|
+
// an operator actually goes looking at for platform-specific detail.
|
|
106
|
+
// `init`/`update`/`sync` also note it once each, at the top of their own
|
|
107
|
+
// run (`runInit`/`runUpdateCore`/`runSyncCore`), independently of this.
|
|
108
|
+
if ((0, paths_1.isWindowsNative)())
|
|
109
|
+
lines.push(picocolors_1.default.dim(` ${paths_1.WINDOWS_KNOWN_GAP.replace(/\n\s*/g, ' ')}`));
|
|
102
110
|
lines.push('');
|
|
103
111
|
for (const provider of report.providers) {
|
|
104
112
|
lines.push(`Provider: ${provider.label} ${picocolors_1.default.dim(`(${provider.tier})`)}`);
|
|
@@ -125,6 +125,11 @@ function reportInitFailure(o) {
|
|
|
125
125
|
return 2;
|
|
126
126
|
}
|
|
127
127
|
async function runInit(opts = {}) {
|
|
128
|
+
// Fires at most once per `awm init` run (native Windows only) — this is
|
|
129
|
+
// the single emission point; `renderReport`/`renderInitOutcome` below do
|
|
130
|
+
// NOT also embed it (that used to triple-fire the same text: once here,
|
|
131
|
+
// once in the "Initial state" render, once in "Final state").
|
|
132
|
+
(0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
|
|
128
133
|
const cwd = opts.cwd ?? process.cwd();
|
|
129
134
|
const agent = opts.agent === undefined ? 'claude-code' : (0, providers_1.requireAgentTarget)(opts.agent);
|
|
130
135
|
// R2: gate BEFORE anything is read or written — an unsupported provider
|
|
@@ -313,7 +318,6 @@ function registerInitCommand(program) {
|
|
|
313
318
|
.option('--machine-only', 'Only run machine-level steps (skip project steps)')
|
|
314
319
|
.option('--json', 'Emit the InitOutcome as JSON — on success and on failure (failed steps + rollback)')
|
|
315
320
|
.action(async (options) => {
|
|
316
|
-
(0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
|
|
317
321
|
const code = await runInit({
|
|
318
322
|
yes: options.yes,
|
|
319
323
|
agent: options.agent,
|
|
@@ -16,6 +16,7 @@ exports.runSyncCore = runSyncCore;
|
|
|
16
16
|
// intro/outro chrome lives in `index.ts`'s Commander registration instead
|
|
17
17
|
// (which already imports clack at the top and is never `require()`d by tests).
|
|
18
18
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
19
|
+
const paths_1 = require("../core/paths");
|
|
19
20
|
const profile_1 = require("../core/profile");
|
|
20
21
|
const registries_1 = require("../core/registries");
|
|
21
22
|
const bundles_1 = require("../core/bundles");
|
|
@@ -31,6 +32,9 @@ const defaultDeps = {
|
|
|
31
32
|
};
|
|
32
33
|
/** Core, UI-free `awm sync` logic — see `runSync` for the Commander-facing wrapper. */
|
|
33
34
|
async function runSyncCore(options, deps = {}) {
|
|
35
|
+
// Fires at most once per `awm sync` run, native Windows only — the single
|
|
36
|
+
// emission point for this command (mirrors `runInit`/`runUpdateCore`).
|
|
37
|
+
(0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
|
|
34
38
|
const d = { ...defaultDeps, ...deps };
|
|
35
39
|
const cwd = options.cwd ?? process.cwd();
|
|
36
40
|
const projectRoot = (0, profile_1.findProjectRoot)(cwd);
|
|
@@ -20,6 +20,7 @@ exports.runUpdateCore = runUpdateCore;
|
|
|
20
20
|
// instead (which already imports clack at the top and is never `require()`d
|
|
21
21
|
// by tests).
|
|
22
22
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
23
|
+
const paths_1 = require("../core/paths");
|
|
23
24
|
const config_1 = require("../utils/config");
|
|
24
25
|
const agent_targets_1 = require("../core/agent-targets");
|
|
25
26
|
const registries_1 = require("../core/registries");
|
|
@@ -49,6 +50,9 @@ const defaultDeps = {
|
|
|
49
50
|
* `runInit`'s `actions` seam).
|
|
50
51
|
*/
|
|
51
52
|
async function runUpdateCore(options = {}, deps = {}) {
|
|
53
|
+
// Fires at most once per `awm update` run, native Windows only — the
|
|
54
|
+
// single emission point for this command (mirrors `runInit`/`runSyncCore`).
|
|
55
|
+
(0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
|
|
52
56
|
const d = { ...defaultDeps, ...deps };
|
|
53
57
|
const prefs = (0, config_1.getPreferences)();
|
|
54
58
|
const resolved = (0, agent_targets_1.resolveAgentTargetsOrError)({ prefs, explicit: options.agent });
|
|
@@ -275,17 +275,51 @@ function spawnStructured(argv, cwd, nonce, extraEnv = {}) {
|
|
|
275
275
|
* rompia el invariante "JAMAS safe sin evidencia" (safeToReplace
|
|
276
276
|
* devolvia 'safe' para un proceso vivo). process.kill(pid,0) evita la
|
|
277
277
|
* capa de emulacion por completo. */
|
|
278
|
+
/** Sleep sincronico REAL, multiplataforma, sin depender de un binario externo
|
|
279
|
+
* (`sleep` no existe nativamente en win32 — ver el comentario de sleepSync
|
|
280
|
+
* mas arriba). `Atomics.wait` sobre un buffer compartido bloquea el hilo
|
|
281
|
+
* actual durante `ms` sin abrir ningun subproceso; funciona identico en
|
|
282
|
+
* cualquier plataforma que soporte Node. Usado SOLO por pidExistsNative
|
|
283
|
+
* para el reintento acotado de abajo — nunca en un hot-path de alta
|
|
284
|
+
* frecuencia. */
|
|
285
|
+
function sleepMsSync(ms) {
|
|
286
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
287
|
+
}
|
|
288
|
+
/** Reintento acotado (R6, post-mortem de CI real): dos corridas reales de
|
|
289
|
+
* windows-latest sobre el MISMO commit (uno vía `release.yml`, uno vía
|
|
290
|
+
* `ci.yml`, ambos re-corriendo el suite completo desde cero) dieron
|
|
291
|
+
* resultados DISTINTOS para el mismo test — `pidExistsNative` reportando
|
|
292
|
+
* ESRCH para un proceso recien spawneado por el propio test, genuinamente
|
|
293
|
+
* vivo — evidencia directa de una condicion de carrera transitoria
|
|
294
|
+
* especifica de esta plataforma/runner, no un bug determinista (el codigo
|
|
295
|
+
* no cambio entre ambas corridas). Un solo intento en el hot-path exacto
|
|
296
|
+
* "acabo de spawnear esto, ¿esta vivo?" no le da tiempo al SO a que el pid
|
|
297
|
+
* sea consultable via OpenProcess bajo carga pesada del runner. Reintentar
|
|
298
|
+
* con una pausa breve ANTES de declarar ESRCH definitivo no debilita el
|
|
299
|
+
* invariante "jamas muerte sin evidencia" — un proceso genuinamente muerto
|
|
300
|
+
* sigue reportando ESRCH en el reintento; esto solo absorbe el falso
|
|
301
|
+
* negativo transitorio. Costo maximo: ~150ms, y SOLO en la rama que ya iba
|
|
302
|
+
* a declarar "no existe" — el camino feliz (proceso vivo, exito inmediato)
|
|
303
|
+
* no paga nada. */
|
|
304
|
+
const PID_EXISTS_RETRY_ATTEMPTS = 3;
|
|
305
|
+
const PID_EXISTS_RETRY_DELAY_MS = 50;
|
|
278
306
|
function pidExistsNative(pid) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
307
|
+
for (let attempt = 0; attempt < PID_EXISTS_RETRY_ATTEMPTS; attempt++) {
|
|
308
|
+
try {
|
|
309
|
+
process.kill(pid, 0);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
// ESRCH = el SO confirmo que el pid no existe. Cualquier otro
|
|
314
|
+
// codigo (ej. EPERM: existe pero sin permiso de senializarlo) NO
|
|
315
|
+
// es evidencia de muerte — falla a favor de "vivo" (R2.1).
|
|
316
|
+
if (error.code !== 'ESRCH')
|
|
317
|
+
return true;
|
|
318
|
+
if (attempt < PID_EXISTS_RETRY_ATTEMPTS - 1)
|
|
319
|
+
sleepMsSync(PID_EXISTS_RETRY_DELAY_MS);
|
|
320
|
+
}
|
|
288
321
|
}
|
|
322
|
+
return false;
|
|
289
323
|
}
|
|
290
324
|
/** Vivo Y con la MISMA identidad — tupla completa, nunca PID solo (R2.1,
|
|
291
325
|
* bloqueador 6): startTime + pgid + digest de ps args.
|
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());
|
|
@@ -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({
|
|
@@ -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-'));
|
|
@@ -237,6 +237,22 @@ describe('process identity (win32, mockeado — sin windows real disponible en e
|
|
|
237
237
|
expect((0, process_1.refIsAlive)(fakeRef)).toBe(false);
|
|
238
238
|
expect(killSpy).toHaveBeenCalledWith(999999, 0);
|
|
239
239
|
});
|
|
240
|
+
test('pidExistsNative reintenta un ESRCH transitorio antes de declarar muerte — no confia en un unico intento (regresion: CI real windows-latest, dos corridas del mismo commit dieron resultados distintos para un proceso recien spawneado)', () => {
|
|
241
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
242
|
+
let calls = 0;
|
|
243
|
+
const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
244
|
+
calls++;
|
|
245
|
+
if (calls < 3) {
|
|
246
|
+
const err = new Error('transient');
|
|
247
|
+
err.code = 'ESRCH';
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
return true; // el pid "aparece" recien al tercer intento — simula la carrera real observada
|
|
251
|
+
});
|
|
252
|
+
const fakeRef = { pid: 424242, startTime: 'x', spawnNonce: 'n', argvDigest: 'd', processGroup: 424242, psArgsDigest: 'x' };
|
|
253
|
+
expect((0, process_1.refIsAlive)(fakeRef)).toBe(true); // NUNCA declara muerte por el ESRCH transitorio de los primeros 2 intentos
|
|
254
|
+
expect(killSpy).toHaveBeenCalledTimes(3);
|
|
255
|
+
});
|
|
240
256
|
test('refIsAlive en win32 NUNCA declara muerte por un error que no sea ESRCH (ej. EPERM: el pid existe pero sin permiso de senializarlo) (R2.1)', () => {
|
|
241
257
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
242
258
|
jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
@@ -67,17 +67,44 @@ describe('core/paths', () => {
|
|
|
67
67
|
setPlatform('darwin');
|
|
68
68
|
expect((0, paths_1.platformLabel)()).toBe('macOS');
|
|
69
69
|
setPlatform('win32');
|
|
70
|
-
|
|
70
|
+
// Windows is a first-class, CI-verified platform since R6 — the label no
|
|
71
|
+
// longer hedges toward WSL, and must not silently regress back to it.
|
|
72
|
+
expect((0, paths_1.platformLabel)()).toBe('Windows (native, CI-verified)');
|
|
73
|
+
expect((0, paths_1.platformLabel)()).not.toContain('WSL');
|
|
71
74
|
});
|
|
72
|
-
it('
|
|
75
|
+
it('noteWindowsCaveat calls the logger only on win32, with the narrow watch-supervisor gap', () => {
|
|
73
76
|
const calls = [];
|
|
74
77
|
const log = (m) => calls.push(m);
|
|
75
78
|
setPlatform('linux');
|
|
76
|
-
(0, paths_1.
|
|
79
|
+
(0, paths_1.noteWindowsCaveat)(log);
|
|
77
80
|
expect(calls).toHaveLength(0);
|
|
78
81
|
setPlatform('win32');
|
|
79
|
-
(0, paths_1.
|
|
80
|
-
expect(calls).toEqual([paths_1.
|
|
82
|
+
(0, paths_1.noteWindowsCaveat)(log);
|
|
83
|
+
expect(calls).toEqual([paths_1.WINDOWS_KNOWN_GAP]);
|
|
84
|
+
// The message must assert Windows support, not disclaim it, and must name
|
|
85
|
+
// the one specific gap rather than a blanket "some things may not work"
|
|
86
|
+
// hedge — pinning both halves so neither regresses independently.
|
|
87
|
+
expect(paths_1.WINDOWS_KNOWN_GAP).toMatch(/supported and continuously verified/i);
|
|
88
|
+
expect(paths_1.WINDOWS_KNOWN_GAP).toMatch(/awm watch/i);
|
|
89
|
+
expect(paths_1.WINDOWS_KNOWN_GAP).not.toMatch(/WSL/i);
|
|
90
|
+
expect(paths_1.WINDOWS_KNOWN_GAP).not.toMatch(/not supported/i);
|
|
91
|
+
});
|
|
92
|
+
it('noteWindowsCaveat propagates a throwing logger instead of swallowing it', () => {
|
|
93
|
+
// `noteWindowsCaveat` has no try/catch around the logger call — callers
|
|
94
|
+
// (init.ts/update.ts/sync.ts/doctor.ts) all pass simple `console.log`
|
|
95
|
+
// wrappers that are not expected to throw, and every caller controls its
|
|
96
|
+
// own logger, so there is no shared reason for this helper to be
|
|
97
|
+
// defensive on their behalf. Pin that behavior explicitly: a throwing
|
|
98
|
+
// logger's error propagates out of `noteWindowsCaveat`, it is not
|
|
99
|
+
// swallowed.
|
|
100
|
+
setPlatform('win32');
|
|
101
|
+
const boom = new Error('logger exploded');
|
|
102
|
+
const throwingLog = () => { throw boom; };
|
|
103
|
+
expect(() => (0, paths_1.noteWindowsCaveat)(throwingLog)).toThrow(boom);
|
|
104
|
+
// And on non-Windows the logger is never even invoked, so a throwing
|
|
105
|
+
// logger is harmless there.
|
|
106
|
+
setPlatform('linux');
|
|
107
|
+
expect(() => (0, paths_1.noteWindowsCaveat)(throwingLog)).not.toThrow();
|
|
81
108
|
});
|
|
82
109
|
describe('resolveOnPath', () => {
|
|
83
110
|
it('uses `command -v` on POSIX and returns true when the binary resolves', () => {
|