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
|
@@ -16,6 +16,60 @@ describe('ControllerAdapter', () => {
|
|
|
16
16
|
expect(a.safeToReplace(ref)).toBe('indeterminate'); // vivo: codex no observa llamadas en vuelo => custodia
|
|
17
17
|
child.kill('SIGKILL');
|
|
18
18
|
});
|
|
19
|
+
/** Regresion (CI windows-latest): reproduce el bug real via mock de
|
|
20
|
+
* process.platform, ya que no hay windows real disponible en este
|
|
21
|
+
* entorno. Antes del fix, refIsAlive en win32 caia en la rama POSIX
|
|
22
|
+
* (ps/pgrep), y ps/pgrep alli — cuando resuelven en el PATH — son el
|
|
23
|
+
* ps/pgrep EMULADO de MSYS/Cygwin, ciego a pids nativos: para el hijo
|
|
24
|
+
* real y vivo que spawnStructured produce, devolvian exit 1 ("no such
|
|
25
|
+
* process" segun su vista emulada), que el codigo interpretaba como
|
|
26
|
+
* "el SO confirmo la muerte" -> refIsAlive(ref) daba `false` -> este
|
|
27
|
+
* mismo test daba `safeToReplace(ref) === 'safe'` para un proceso
|
|
28
|
+
* VIVO, violando "JAMAS safe sin evidencia" (R4.2b). Tras el fix,
|
|
29
|
+
* refIsAlive en win32 usa process.kill(pid,0) (respaldado por el
|
|
30
|
+
* kernel via libuv) y nunca ps/pgrep. */
|
|
31
|
+
test('safeToReplace en win32 (mockeado): muerto probado (ESRCH real) => safe; vivo => indeterminate, JAMAS safe por ceguera de ps/pgrep emulado (R4.2b)', async () => {
|
|
32
|
+
const originalPlatform = process.platform;
|
|
33
|
+
try {
|
|
34
|
+
const a = (0, adapter_1.adapterFor)('codex');
|
|
35
|
+
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 3000)'], process.cwd(), 'nWin32');
|
|
36
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
37
|
+
// El pid vive de verdad: nunca 'safe' sin evidencia real de muerte.
|
|
38
|
+
expect(a.safeToReplace(ref)).toBe('indeterminate');
|
|
39
|
+
// Un ps/pgrep emulado y ciego (exit 1 para un pid nativo real) ya
|
|
40
|
+
// NO puede empujar el veredicto hacia 'safe': refIsAlive en win32
|
|
41
|
+
// nunca invoca ps/pgrep — SI invoca powershell.exe (win32ProcessInfo,
|
|
42
|
+
// R6 ronda 2) para la verificacion de identidad completa cuando el
|
|
43
|
+
// ref no esta degradado, que es un mecanismo distinto y legitimo
|
|
44
|
+
// (WMI, no la capa emulada MSYS/Cygwin) — no se afirma "cero
|
|
45
|
+
// llamadas", se afirma "ps/pgrep especificamente, nunca".
|
|
46
|
+
const cp = require('child_process');
|
|
47
|
+
const execSpy = jest.spyOn(cp, 'execFileSync').mockImplementation(() => {
|
|
48
|
+
const err = new Error('no matches found');
|
|
49
|
+
err.status = 1;
|
|
50
|
+
throw err;
|
|
51
|
+
});
|
|
52
|
+
expect(a.safeToReplace(ref)).toBe('indeterminate');
|
|
53
|
+
const calledBinaries = execSpy.mock.calls.map((call) => call[0]);
|
|
54
|
+
expect(calledBinaries).not.toContain('ps');
|
|
55
|
+
expect(calledBinaries).not.toContain('pgrep');
|
|
56
|
+
execSpy.mockRestore();
|
|
57
|
+
child.kill('SIGKILL');
|
|
58
|
+
// Muerte PROBADA por el SO (ESRCH real de process.kill(pid,0)):
|
|
59
|
+
// recien ahi es legitimo declarar 'safe'.
|
|
60
|
+
const deadRef = { ...ref };
|
|
61
|
+
const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
62
|
+
const err = new Error('no such process');
|
|
63
|
+
err.code = 'ESRCH';
|
|
64
|
+
throw err;
|
|
65
|
+
});
|
|
66
|
+
expect(a.safeToReplace(deadRef)).toBe('safe');
|
|
67
|
+
killSpy.mockRestore();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
19
73
|
test('launchArgv construye el comando de reanudacion journal-first (R4.8)', () => {
|
|
20
74
|
const argv = (0, adapter_1.adapterFor)('codex').launchArgv('retoma desde next_action');
|
|
21
75
|
expect(argv[0]).toBe('codex');
|
|
@@ -111,7 +111,15 @@ describe('computeFingerprint', () => {
|
|
|
111
111
|
* de un git real) — sin el fix, esto tumba al hijo; con el fix, sobrevive. */
|
|
112
112
|
describe('fingerprint.ts git(): stdio explicito evita inheritStderr hacia un pipe roto', () => {
|
|
113
113
|
const DIST_ENTRY = path_1.default.resolve(__dirname, '..', '..', '..', 'dist', 'src', 'core', 'journal', 'fingerprint.js');
|
|
114
|
-
|
|
114
|
+
// `which` is POSIX-only and not reliably on PATH in a pwsh-shell windows-latest
|
|
115
|
+
// runner even though Git for Windows is installed (Windows uses `where`). `where`
|
|
116
|
+
// can print one match per line when git is reachable via more than one PATH
|
|
117
|
+
// entry, so only the FIRST line is a valid single path to spawn directly — the
|
|
118
|
+
// rest would make execFileSync try to exec a multi-line string as one path.
|
|
119
|
+
const REAL_GIT = (0, child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'which', ['git'], { encoding: 'utf8' })
|
|
120
|
+
.split(/\r?\n/)
|
|
121
|
+
.map((line) => line.trim())
|
|
122
|
+
.find((line) => line.length > 0);
|
|
115
123
|
beforeAll(() => {
|
|
116
124
|
if (!fs_1.default.existsSync(DIST_ENTRY)) {
|
|
117
125
|
throw new Error('dist ausente: corre `cd cli && npm run build` antes de este test (verifica el dist compilado real, no el source transpilado por ts-jest)');
|
|
@@ -143,7 +151,7 @@ describe('fingerprint.ts git(): stdio explicito evita inheritStderr hacia un pip
|
|
|
143
151
|
`);
|
|
144
152
|
const child = (0, child_process_1.spawn)(process.execPath, [childScript], {
|
|
145
153
|
cwd: workDir,
|
|
146
|
-
env: { ...process.env, PATH: `${workDir}
|
|
154
|
+
env: { ...process.env, PATH: `${workDir}${path_1.default.delimiter}${process.env.PATH}` },
|
|
147
155
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
148
156
|
detached: true,
|
|
149
157
|
});
|
|
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const process_1 = require("../../../src/core/journal/process");
|
|
11
|
+
const paths_1 = require("../../../src/core/paths");
|
|
11
12
|
describe('process identity', () => {
|
|
12
13
|
test('spawnStructured produce ProcessRef con tupla completa (R2.1, R4.7)', async () => {
|
|
13
14
|
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'nonce-abc');
|
|
@@ -15,24 +16,52 @@ describe('process identity', () => {
|
|
|
15
16
|
expect(ref.spawnNonce).toBe('nonce-abc');
|
|
16
17
|
expect(typeof ref.startTime).toBe('string');
|
|
17
18
|
expect(ref.processGroup).toBeGreaterThan(0);
|
|
18
|
-
|
|
19
|
+
// hex real cuando `ps` pudo observar el proceso (caso normal en
|
|
20
|
+
// POSIX); sentinel 'unknown' documentado (ver captureRefFor) cuando
|
|
21
|
+
// no pudo — en win32 esto es la ruta NORMAL, no un error: `ps`/`pgrep`
|
|
22
|
+
// ahi (si resuelven en el PATH) son el ps/pgrep emulado de
|
|
23
|
+
// MSYS/Cygwin, ciego a procesos nativos (ver pidExistsNative en
|
|
24
|
+
// src/core/journal/process.ts). El formato exacto de este campo
|
|
25
|
+
// nunca es la fuente de verdad de vida/muerte — solo lo es refIsAlive
|
|
26
|
+
// (ver los tests de la seccion 'process identity (win32, mockeado)').
|
|
27
|
+
expect(ref.psArgsDigest).toMatch(/^([0-9a-f]{16}|unknown)$/);
|
|
19
28
|
expect((0, process_1.refIsAlive)(ref)).toBe(true);
|
|
20
29
|
const dead = await (0, process_1.terminateGroupConfirmed)(ref, { termGraceMs: 300, killGraceMs: 300 });
|
|
21
30
|
expect(dead).toBe(true);
|
|
22
31
|
expect((0, process_1.refIsAlive)(ref)).toBe(false);
|
|
23
32
|
});
|
|
24
|
-
test('refIsAlive rechaza cualquier campo distinto de la tupla completa (R2.1)', () => {
|
|
33
|
+
test('refIsAlive rechaza cualquier campo distinto de la tupla completa (R2.1) — en win32 nativo, solo identidad reducida (ronda 3, ver src/core/journal/process.ts)', () => {
|
|
25
34
|
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 3000)'], process.cwd(), 'n2');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if ((0, paths_1.isWindowsNative)()) {
|
|
36
|
+
// win32 real (ronda 3): refIsAlive solo valida pid existente +
|
|
37
|
+
// processGroup === pid; el resto de la tupla es informativo
|
|
38
|
+
// (via WMI, captureRefFor) pero NO gatea el veredicto — gap
|
|
39
|
+
// aceptado y documentado (ver refIsAlive en process.ts).
|
|
40
|
+
expect((0, process_1.refIsAlive)({ ...ref, startTime: 'otro-momento' })).toBe(true);
|
|
41
|
+
expect((0, process_1.refIsAlive)({ ...ref, spawnNonce: 'otro-nonce' })).toBe(true);
|
|
42
|
+
expect((0, process_1.refIsAlive)({ ...ref, argvDigest: 'ffffffffffffffff' })).toBe(true);
|
|
43
|
+
expect((0, process_1.refIsAlive)({ ...ref, psArgsDigest: 'ffffffffffffffff' })).toBe(true);
|
|
44
|
+
expect((0, process_1.refIsAlive)({ ...ref, processGroup: ref.processGroup + 1 })).toBe(false);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
expect((0, process_1.refIsAlive)({ ...ref, startTime: 'otro-momento' })).toBe(false);
|
|
48
|
+
expect((0, process_1.refIsAlive)({ ...ref, spawnNonce: 'otro-nonce' })).toBe(false);
|
|
49
|
+
expect((0, process_1.refIsAlive)({ ...ref, argvDigest: 'ffffffffffffffff' })).toBe(false);
|
|
50
|
+
expect((0, process_1.refIsAlive)({ ...ref, psArgsDigest: 'ffffffffffffffff' })).toBe(false);
|
|
51
|
+
expect((0, process_1.refIsAlive)({ ...ref, processGroup: ref.processGroup + 1 })).toBe(false);
|
|
52
|
+
}
|
|
31
53
|
child.kill('SIGKILL');
|
|
32
54
|
});
|
|
33
55
|
test('terminateGroupConfirmed no senializa un PGID si la identidad del lider no coincide', async () => {
|
|
34
56
|
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'n-no-kill');
|
|
35
|
-
|
|
57
|
+
// win32 (ronda 3): un mismatch de SOLO startTime ya no lo detecta
|
|
58
|
+
// refIsAlive ahi (gap de reciclado de PID aceptado, ver process.ts) —
|
|
59
|
+
// se usa un mismatch de processGroup, que SI se valida en ambas
|
|
60
|
+
// plataformas, para que este test siga siendo significativo en
|
|
61
|
+
// cualquier host.
|
|
62
|
+
const mismatched = (0, paths_1.isWindowsNative)()
|
|
63
|
+
? { ...ref, processGroup: ref.processGroup + 1 }
|
|
64
|
+
: { ...ref, startTime: 'identidad-de-otro-proceso' };
|
|
36
65
|
const confirmed = await (0, process_1.terminateGroupConfirmed)(mismatched, { termGraceMs: 20, killGraceMs: 20 });
|
|
37
66
|
expect(confirmed).toBe(false);
|
|
38
67
|
expect((0, process_1.refIsAlive)(ref)).toBe(true);
|
|
@@ -148,6 +177,176 @@ describe('process identity', () => {
|
|
|
148
177
|
}
|
|
149
178
|
});
|
|
150
179
|
});
|
|
180
|
+
/** Regresion (CI windows-latest, primera corrida real de la matriz): ps/pgrep,
|
|
181
|
+
* cuando resuelven en el PATH en Windows, son el ps/pgrep EMULADO de
|
|
182
|
+
* MSYS/Cygwin (Git for Windows) — una capa con su propia tabla de pids,
|
|
183
|
+
* ciega a procesos nativos spawneados via CreateProcess (exactamente lo que
|
|
184
|
+
* produce spawnStructured). El codigo viejo interpretaba el exit 1 de ese
|
|
185
|
+
* ps/pgrep "ciego" como "el SO confirmo que el pid no existe" — falso, y
|
|
186
|
+
* rompia el invariante "JAMAS safe sin evidencia": `safeToReplace` devolvia
|
|
187
|
+
* 'safe' para un proceso genuinamente vivo (ver adapter.test.ts). No hay
|
|
188
|
+
* windows-latest real disponible en este entorno; estos tests mockean
|
|
189
|
+
* `process.platform` (mismo patron que
|
|
190
|
+
* tests/commands/sensors/exec-windows.test.ts, que ya cubre exactamente
|
|
191
|
+
* este problema para sensors/exec.ts::killTree) para ejercitar la rama
|
|
192
|
+
* win32 REAL del codigo de produccion contra un pid real y vivo. */
|
|
193
|
+
describe('process identity (win32, mockeado — sin windows real disponible en este entorno)', () => {
|
|
194
|
+
const originalPlatform = process.platform;
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
197
|
+
jest.restoreAllMocks();
|
|
198
|
+
});
|
|
199
|
+
test('refIsAlive en win32 usa process.kill(pid,0), NUNCA ps/pgrep — reproduce el bug: un ps/pgrep "ciego" que devuelve exit 1 para un pid real y vivo ya no lo declara muerto (R2.1, R4.2b)', () => {
|
|
200
|
+
// Ronda 3 (ver refIsAlive en process.ts): el veredicto win32 ya NO
|
|
201
|
+
// depende de si la identidad esta degradada o no ('unknown' vs
|
|
202
|
+
// datos reales de WMI) — refIsAlive ahi SOLO llama a
|
|
203
|
+
// pidExistsNative (process.kill) + convencion de processGroup,
|
|
204
|
+
// incondicionalmente. La precondicion original de este test
|
|
205
|
+
// ("identidad degradada porque no hay powershell.exe real en este
|
|
206
|
+
// entorno") ya no es ni necesaria ni confiable: en windows-latest
|
|
207
|
+
// CI real, powershell.exe SI esta disponible y captureRefFor
|
|
208
|
+
// devuelve un startTime real via WMI — lo cual esta bien, porque
|
|
209
|
+
// esta rama de refIsAlive nunca lo consulta de todos modos.
|
|
210
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
211
|
+
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 3000)'], process.cwd(), 'n-win32-a');
|
|
212
|
+
const cp = require('child_process');
|
|
213
|
+
// Simula EXACTAMENTE el bug real de CI: ps/pgrep "corren" pero
|
|
214
|
+
// devuelven exit 1 (ceguera de MSYS a pids nativos) para un pid que
|
|
215
|
+
// esta genuinamente vivo — el codigo viejo confiaba en esto.
|
|
216
|
+
const execSpy = jest.spyOn(cp, 'execFileSync').mockImplementation(() => {
|
|
217
|
+
const err = new Error('no matches found');
|
|
218
|
+
err.status = 1;
|
|
219
|
+
throw err;
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
expect((0, process_1.refIsAlive)(ref)).toBe(true); // vivo de verdad: nunca declarado muerto
|
|
223
|
+
expect(execSpy).not.toHaveBeenCalled(); // refIsAlive en win32 ni siquiera intenta ps/pgrep/WMI
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
child.kill('SIGKILL');
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
test('refIsAlive en win32 declara muerte SOLO con ESRCH real de process.kill(pid,0) (R2.1)', () => {
|
|
230
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
231
|
+
const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
232
|
+
const err = new Error('no such process');
|
|
233
|
+
err.code = 'ESRCH';
|
|
234
|
+
throw err;
|
|
235
|
+
});
|
|
236
|
+
const fakeRef = { pid: 999999, startTime: 'x', spawnNonce: 'n', argvDigest: 'd', processGroup: 999999, psArgsDigest: 'x' };
|
|
237
|
+
expect((0, process_1.refIsAlive)(fakeRef)).toBe(false);
|
|
238
|
+
expect(killSpy).toHaveBeenCalledWith(999999, 0);
|
|
239
|
+
});
|
|
240
|
+
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
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
242
|
+
jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
243
|
+
const err = new Error('operation not permitted');
|
|
244
|
+
err.code = 'EPERM';
|
|
245
|
+
throw err;
|
|
246
|
+
});
|
|
247
|
+
// Ronda 3: refIsAlive en win32 ya no consulta WMI/powershell — solo
|
|
248
|
+
// pidExistsNative + convencion de processGroup. EPERM (el pid existe
|
|
249
|
+
// pero sin permiso de senializarlo) no es ESRCH => pidExistsNative
|
|
250
|
+
// dice "vivo"; execFileSync no deberia ni invocarse.
|
|
251
|
+
const cp = require('child_process');
|
|
252
|
+
const execSpy = jest.spyOn(cp, 'execFileSync').mockImplementation((...args) => {
|
|
253
|
+
throw new Error('llamada inesperada a execFileSync en este test: ' + args[0]);
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
const fakeRef = { pid: 4242, startTime: 'x', spawnNonce: 'n', argvDigest: 'd', processGroup: 4242, psArgsDigest: 'x' };
|
|
257
|
+
expect((0, process_1.refIsAlive)(fakeRef)).toBe(true);
|
|
258
|
+
expect(execSpy).not.toHaveBeenCalled();
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
execSpy.mockRestore();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
test('terminateGroupConfirmed en win32 usa `taskkill /pid <pid> /T /F`, NUNCA process.kill(-pgid) (mismo patron probado en sensors/exec.ts::killTree, ver tests/commands/sensors/exec-windows.test.ts)', async () => {
|
|
265
|
+
// Spawnea en modo POSIX real (platform sin mockear todavia) para que
|
|
266
|
+
// ref.processGroup sea un pgid real de ps (detached:true en esta
|
|
267
|
+
// plataforma) — evita que el pgid observado sea el del test runner.
|
|
268
|
+
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'n-win32-taskkill');
|
|
269
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
270
|
+
const cp = require('child_process');
|
|
271
|
+
// Ronda 3: refIsAlive en win32 ya no consulta WMI/powershell — solo
|
|
272
|
+
// pidExistsNative (process.kill) + convencion de processGroup, asi
|
|
273
|
+
// que el unico execFileSync que este camino dispara es taskkill.
|
|
274
|
+
const execSpy = jest.spyOn(cp, 'execFileSync').mockImplementation((...args) => {
|
|
275
|
+
const [cmd] = args;
|
|
276
|
+
if (cmd === 'taskkill') {
|
|
277
|
+
child.kill('SIGKILL'); // simula taskkill matando de verdad al pid real
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
throw new Error('llamada inesperada a execFileSync en este test: ' + cmd);
|
|
281
|
+
});
|
|
282
|
+
const posixKillSpy = jest.spyOn(process, 'kill');
|
|
283
|
+
const dead = await (0, process_1.terminateGroupConfirmed)(ref, { termGraceMs: 2000, killGraceMs: 500 });
|
|
284
|
+
expect(execSpy).toHaveBeenCalledWith('taskkill', ['/pid', String(ref.pid), '/T', '/F'], expect.anything());
|
|
285
|
+
expect(dead).toBe(true);
|
|
286
|
+
// Nunca la convencion POSIX de pid negativo (grupo) en esta plataforma.
|
|
287
|
+
for (const call of posixKillSpy.mock.calls) {
|
|
288
|
+
expect(call[0]).toBeGreaterThanOrEqual(0);
|
|
289
|
+
}
|
|
290
|
+
}, 15000);
|
|
291
|
+
});
|
|
292
|
+
/** Ronda 2 del fix win32 (R2.1/R6) agrego captura de identidad completa
|
|
293
|
+
* (startTime/psArgsDigest reales) via WMI (`Get-CimInstance Win32_Process`,
|
|
294
|
+
* ver win32ProcessInfo) para `captureRefFor`. Ronda 3 (ver refIsAlive en
|
|
295
|
+
* process.ts) revirtio el USO de esa captura como gate de liveness —
|
|
296
|
+
* `refIsAlive` en win32 volvio a pid-existence + convencion de
|
|
297
|
+
* processGroup solamente, tras un falso negativo real en CI (WMI
|
|
298
|
+
* demostradamente no confiable en su primera corrida real) — pero la
|
|
299
|
+
* CAPTURA en si (`captureRefFor`) sigue poblando esos campos como
|
|
300
|
+
* informacion persistida en el ProcessRef, asi que estos dos tests de
|
|
301
|
+
* captura siguen vigentes. Los tests que ejercitaban `refIsAlive` via el
|
|
302
|
+
* camino WMI (ronda 2) fueron removidos: ese camino ya no existe en
|
|
303
|
+
* produccion — ver la suite generica de arriba
|
|
304
|
+
* ('refIsAlive rechaza cualquier campo...') para la cobertura actual de
|
|
305
|
+
* refIsAlive en win32. */
|
|
306
|
+
describe('process identity (win32, mockeado) — captura de identidad via WMI (R2.1/R6, ronda 2)', () => {
|
|
307
|
+
const originalPlatform = process.platform;
|
|
308
|
+
afterEach(() => {
|
|
309
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
310
|
+
jest.restoreAllMocks();
|
|
311
|
+
});
|
|
312
|
+
function mockPowershell(response) {
|
|
313
|
+
const cp = require('child_process');
|
|
314
|
+
jest.spyOn(cp, 'execFileSync').mockImplementation((...args) => {
|
|
315
|
+
const [cmd] = args;
|
|
316
|
+
if (cmd === 'powershell.exe')
|
|
317
|
+
return typeof response === 'function' ? response() : response;
|
|
318
|
+
throw new Error('llamada inesperada a execFileSync en este test: ' + cmd);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
test('captureRefFor en win32 usa WMI para obtener startTime/psArgsDigest REALES cuando powershell responde (R2.1)', () => {
|
|
322
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
323
|
+
const fakeCreationDate = '2026-08-08T00:00:00.0000000-00:00';
|
|
324
|
+
mockPowershell(JSON.stringify({ CreationDate: fakeCreationDate, CommandLine: 'C:\\node.exe fake-argv' }));
|
|
325
|
+
const ref = (0, process_1.captureRefFor)(process.pid, 'nonce-win32-wmi-ok', ['node', 'fake-argv']);
|
|
326
|
+
expect(ref.startTime).toBe(fakeCreationDate); // ya NO 'unknown': WMI respondio
|
|
327
|
+
expect(ref.psArgsDigest).toMatch(/^[0-9a-f]{16}$/); // digest real, no sentinel
|
|
328
|
+
expect(ref.processGroup).toBe(process.pid); // convencion fija win32: sin pgid real jamas
|
|
329
|
+
});
|
|
330
|
+
test('captureRefFor en win32 degrada a unknown si WMI/powershell no responde, nunca crashea (R2.1) — mismo contrato que la rama POSIX', () => {
|
|
331
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
332
|
+
const cp = require('child_process');
|
|
333
|
+
const execSpy = jest.spyOn(cp, 'execFileSync').mockImplementation(() => {
|
|
334
|
+
const err = new Error('powershell no encontrado');
|
|
335
|
+
err.code = 'ENOENT';
|
|
336
|
+
throw err;
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
expect(() => (0, process_1.captureRefFor)(process.pid, 'nonce-win32-wmi-fail', ['node'])).not.toThrow();
|
|
340
|
+
const ref = (0, process_1.captureRefFor)(process.pid, 'nonce-win32-wmi-fail', ['node']);
|
|
341
|
+
expect(ref.startTime).toBe('unknown');
|
|
342
|
+
expect(ref.psArgsDigest).toBe('unknown');
|
|
343
|
+
expect(ref.processGroup).toBe(process.pid);
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
execSpy.mockRestore();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
151
350
|
/** Defense-in-depth: los execFileSync internos de este archivo (psField,
|
|
152
351
|
* sleepSync, groupIsGone, activitySnapshot) deben capturar el stderr del
|
|
153
352
|
* subproceso INTERNAMENTE, nunca relayearlo al stderr del proceso llamante
|
|
@@ -192,7 +391,7 @@ describe('process.ts execFileSync: stdio explicito evita inheritStderr hacia un
|
|
|
192
391
|
`);
|
|
193
392
|
const child = (0, child_process_1.spawn)(process.execPath, [childScript], {
|
|
194
393
|
cwd: workDir,
|
|
195
|
-
env: { ...process.env, PATH: `${workDir}
|
|
394
|
+
env: { ...process.env, PATH: `${workDir}${path_1.default.delimiter}${process.env.PATH}` },
|
|
196
395
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
197
396
|
detached: true,
|
|
198
397
|
});
|
|
@@ -15,8 +15,14 @@ describe('journal store', () => {
|
|
|
15
15
|
test('initJournal crea 0700/0600 y estado inicial valido (R1.2)', () => {
|
|
16
16
|
(0, store_1.initJournal)(repo, 'rama');
|
|
17
17
|
const dir = (0, paths_1.journalDir)(repo, 'rama');
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Windows fs.chmod can only toggle the read-only attribute, not set granular
|
|
19
|
+
// POSIX bits -- see tests/core/atomic-file.test.ts (files, confirmed against
|
|
20
|
+
// real windows-latest CI) and tests/core/install-transaction.test.ts
|
|
21
|
+
// (directories, same reasoning) for the 0o666/0o777 shapes.
|
|
22
|
+
// Confirmed against real windows-latest CI (2026-08-08): directories get the same
|
|
23
|
+
// 0o666 shape as files there, not 0o777 as first reasoned.
|
|
24
|
+
expect(fs_1.default.statSync(dir).mode & 0o777).toBe(process.platform === 'win32' ? 0o666 : 0o700);
|
|
25
|
+
expect(fs_1.default.statSync((0, paths_1.statePath)(repo, 'rama')).mode & 0o777).toBe(process.platform === 'win32' ? 0o666 : 0o600);
|
|
20
26
|
const r = (0, store_1.readJournal)(repo, 'rama');
|
|
21
27
|
expect(r.corrupt).toBe(false);
|
|
22
28
|
expect(r.state.revision).toBe(0);
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
// tests/core/no-color.test.ts
|
|
7
|
+
//
|
|
8
|
+
// Regression for R6 (2026-08-08): picocolors treats being on win32 OR having a `CI`
|
|
9
|
+
// env var set as automatic color support, REGARDLESS of TTY status — GitHub Actions
|
|
10
|
+
// sets `CI=true` on every runner (any OS) and this suite's own tests assert exact/
|
|
11
|
+
// substring CLI text output that silently breaks once ANSI escapes are interleaved
|
|
12
|
+
// into it. jest.setup.js forces `NO_COLOR=1` before any test file loads specifically
|
|
13
|
+
// to make this deterministic everywhere this suite runs, not just where it happens
|
|
14
|
+
// to be colorless by accident (no real TTY, no CI env). This test pins that: if
|
|
15
|
+
// jest.setup.js's NO_COLOR line were ever removed or the wiring in jest.config.js
|
|
16
|
+
// broke, this is the one test that fails on ITS OWN merits, not as a side effect of
|
|
17
|
+
// some other test's substring assertion breaking.
|
|
18
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
19
|
+
test('picocolors never emits ANSI escapes during this test run', () => {
|
|
20
|
+
expect(picocolors_1.default.isColorSupported).toBe(false);
|
|
21
|
+
expect(picocolors_1.default.red('x')).toBe('x');
|
|
22
|
+
expect(picocolors_1.default.green('x')).toBe('x');
|
|
23
|
+
});
|
|
@@ -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', () => {
|
|
@@ -9,6 +9,35 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t -c tag.gpgSign=false ${cmd}`, { cwd, stdio: 'pipe' });
|
|
12
|
+
// Real `git clone`/`pull` against local file:// fixtures, several times per
|
|
13
|
+
// test — on windows-latest CI this is measurably slower than on POSIX (NTFS
|
|
14
|
+
// overhead, antivirus scanning of freshly-written objects) and the jest
|
|
15
|
+
// default of 5000ms proved too tight in real CI (regression: real windows-latest
|
|
16
|
+
// run, "Exceeded timeout of 5000 ms"). Matches the pattern already used by
|
|
17
|
+
// other real-subprocess suites in this repo (supervisor-loop.test.ts: 60000,
|
|
18
|
+
// e2e-crash.test.ts: 180000).
|
|
19
|
+
jest.setTimeout(30000);
|
|
20
|
+
/** git en win32 puede mantener un handle abierto sobre `.git/objects` por un
|
|
21
|
+
* instante despues de que el proceso `git` retorna (buffering/flush del
|
|
22
|
+
* filesystem, o un git-index-lock que el SO tarda en soltar) — rmSync
|
|
23
|
+
* inmediato entonces produce EBUSY/ENOTEMPTY (regresion: real windows-latest
|
|
24
|
+
* CI). Mismo patron ya aplicado en tests/commands/watch/runner.test.ts para
|
|
25
|
+
* un problema de fondo identico (escritura async en vuelo vs cleanup
|
|
26
|
+
* inmediato). */
|
|
27
|
+
async function rmSyncRetryingBusy(target, attempts = 10, delayMs = 100) {
|
|
28
|
+
for (let i = 0; i < attempts; i++) {
|
|
29
|
+
try {
|
|
30
|
+
fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const code = error.code;
|
|
35
|
+
if ((code !== 'EBUSY' && code !== 'ENOTEMPTY') || i === attempts - 1)
|
|
36
|
+
throw error;
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
12
41
|
/** Creates a git source repo with a skill, returns its path (serves as local remote). */
|
|
13
42
|
function makeSourceRepo(base, skillName) {
|
|
14
43
|
const dir = path_1.default.join(base, `src-${skillName}`);
|
|
@@ -33,9 +62,9 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
33
62
|
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
34
63
|
jest.resetModules();
|
|
35
64
|
});
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
await rmSyncRetryingBusy(tmpHome);
|
|
67
|
+
await rmSyncRetryingBusy(tmpWork);
|
|
39
68
|
if (originalHome === undefined)
|
|
40
69
|
delete process.env.HOME;
|
|
41
70
|
else
|