agentic-workflow-manager 3.13.0 → 3.13.1
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/hooks/shared.js +14 -1
- package/dist/src/commands/registry/add.js +10 -1
- 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/tests/commands/hooks/install-symlink-fallback.test.js +25 -0
- package/dist/tests/commands/hooks/status.test.js +23 -3
- 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/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/registries-sync.test.js +32 -3
- package/package.json +1 -1
|
@@ -15,7 +15,10 @@ describe('writeFileAtomicDurable', () => {
|
|
|
15
15
|
const f = path_1.default.join(dir, 'state.json');
|
|
16
16
|
(0, atomic_file_1.writeFileAtomicDurable)(f, '{"a":1}', 0o600);
|
|
17
17
|
expect(fs_1.default.readFileSync(f, 'utf8')).toBe('{"a":1}');
|
|
18
|
-
|
|
18
|
+
// Windows fs.chmod can only toggle the read-only attribute, not set
|
|
19
|
+
// granular POSIX bits — see tests/core/atomic-file.test.ts for the
|
|
20
|
+
// confirmed 0o600 -> 0o666 shape on win32.
|
|
21
|
+
expect(fs_1.default.statSync(f).mode & 0o777).toBe(process.platform === 'win32' ? 0o666 : 0o600);
|
|
19
22
|
});
|
|
20
23
|
test('sobrevive a reemplazos consecutivos sin residuo tmp (R1.2)', () => {
|
|
21
24
|
const f = path_1.default.join(dir, 'state.json');
|
|
@@ -39,4 +42,48 @@ describe('writeFileAtomicDurable', () => {
|
|
|
39
42
|
test('fsyncDirSync exitoso no lanza sobre un directorio real (R1.2)', () => {
|
|
40
43
|
expect(() => (0, atomic_file_1.fsyncDirSync)(dir)).not.toThrow();
|
|
41
44
|
});
|
|
45
|
+
describe('fsyncDirSync en Windows (R6 CI, 2026-08-08)', () => {
|
|
46
|
+
// Windows no soporta fsync-ear un fd de directorio en absoluto (confirmado en la
|
|
47
|
+
// primera corrida real de R6 en windows-latest: EPERM en TODAS las llamadas,
|
|
48
|
+
// nunca un fallo intermitente) — degrada a no-op solo para esa combinacion exacta
|
|
49
|
+
// (win32 + EPERM en el fsync mismo), nunca para otra plataforma ni otro error.
|
|
50
|
+
const realPlatform = process.platform;
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
53
|
+
});
|
|
54
|
+
test('EPERM en el fsync no lanza en win32 — degrada a no-op', () => {
|
|
55
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
56
|
+
const err = new Error('operation not permitted, fsync');
|
|
57
|
+
err.code = 'EPERM';
|
|
58
|
+
jest.spyOn(fs_1.default, 'fsyncSync').mockImplementation(() => { throw err; });
|
|
59
|
+
expect(() => (0, atomic_file_1.fsyncDirSync)(dir)).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
test('un error que NO es EPERM en el fsync sigue lanzando en win32', () => {
|
|
62
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
63
|
+
const err = new Error('disk full');
|
|
64
|
+
err.code = 'ENOSPC';
|
|
65
|
+
jest.spyOn(fs_1.default, 'fsyncSync').mockImplementation(() => { throw err; });
|
|
66
|
+
expect(() => (0, atomic_file_1.fsyncDirSync)(dir)).toThrow(/fsync de directorio/);
|
|
67
|
+
});
|
|
68
|
+
test('EPERM en el fsync sigue lanzando fuera de win32 — la excepcion es exclusiva de Windows', () => {
|
|
69
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
70
|
+
const err = new Error('operation not permitted, fsync');
|
|
71
|
+
err.code = 'EPERM';
|
|
72
|
+
jest.spyOn(fs_1.default, 'fsyncSync').mockImplementation(() => { throw err; });
|
|
73
|
+
expect(() => (0, atomic_file_1.fsyncDirSync)(dir)).toThrow(/fsync de directorio/);
|
|
74
|
+
});
|
|
75
|
+
test('un fallo en el open (no en el fsync) sigue lanzando incluso en win32', () => {
|
|
76
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
77
|
+
const realOpen = fs_1.default.openSync;
|
|
78
|
+
jest.spyOn(fs_1.default, 'openSync').mockImplementation(((p, flags, mode) => {
|
|
79
|
+
if (flags === 'r') {
|
|
80
|
+
const e = new Error('EPERM simulado');
|
|
81
|
+
e.code = 'EPERM';
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
return realOpen(p, flags, mode);
|
|
85
|
+
}));
|
|
86
|
+
expect(() => (0, atomic_file_1.fsyncDirSync)(dir)).toThrow(/fsync de directorio/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
42
89
|
});
|
|
@@ -28,7 +28,15 @@ describe('writeFileAtomic', () => {
|
|
|
28
28
|
expect(open.mock.calls[0][1]).toBe('wx');
|
|
29
29
|
expect(rename).toHaveBeenCalledWith(temporary, file);
|
|
30
30
|
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('content');
|
|
31
|
-
|
|
31
|
+
// Windows/NTFS has no POSIX permission bits: fs.fchmodSync there can only
|
|
32
|
+
// toggle the read-only attribute, so a *writable* file always reports back
|
|
33
|
+
// mode 0o666 regardless of the finer-grained mode requested (0o600 here) —
|
|
34
|
+
// confirmed directly from windows-latest CI output (R6, 2026-08-08: expected
|
|
35
|
+
// 384/0o600, got 438/0o666). This is a genuine platform capability gap, not a
|
|
36
|
+
// production bug: see the matching note in tests/core/artifact-state.test.ts
|
|
37
|
+
// for why the data this guards doesn't need Windows-specific hardening.
|
|
38
|
+
const expectedMode = process.platform === 'win32' ? 0o666 : 0o600;
|
|
39
|
+
expect(fs_1.default.statSync(file).mode & 0o777).toBe(expectedMode);
|
|
32
40
|
expect(fs_1.default.existsSync(temporary)).toBe(false);
|
|
33
41
|
});
|
|
34
42
|
it('cleans only its temporary file when rename fails', () => {
|
|
@@ -53,14 +61,17 @@ describe('writeFileAtomic', () => {
|
|
|
53
61
|
});
|
|
54
62
|
expect(() => (0, atomic_file_1.writeFileAtomic)(file, 'replacement', 0o644)).toThrow('rename failed');
|
|
55
63
|
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('original');
|
|
56
|
-
|
|
64
|
+
// Windows fs.chmod can only toggle the read-only attribute, not set
|
|
65
|
+
// granular POSIX bits (see the platform-aware assertion above in this
|
|
66
|
+
// same file for the confirmed 0o600 -> 0o666 shape).
|
|
67
|
+
expect(fs_1.default.statSync(file).mode & 0o777).toBe(process.platform === 'win32' ? 0o666 : 0o600);
|
|
57
68
|
});
|
|
58
69
|
it('preserves existing target permissions after replacement', () => {
|
|
59
70
|
const file = path_1.default.join(dir, 'AGENTS.md');
|
|
60
71
|
fs_1.default.writeFileSync(file, 'original', { mode: 0o600 });
|
|
61
72
|
(0, atomic_file_1.writeFileAtomic)(file, 'replacement');
|
|
62
73
|
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('replacement');
|
|
63
|
-
expect(fs_1.default.statSync(file).mode & 0o777).toBe(0o600);
|
|
74
|
+
expect(fs_1.default.statSync(file).mode & 0o777).toBe(process.platform === 'win32' ? 0o666 : 0o600);
|
|
64
75
|
});
|
|
65
76
|
it('rejects a target symlink without severing it or changing its victim', () => {
|
|
66
77
|
const victim = path_1.default.join(dir, 'victim');
|
|
@@ -61,4 +61,62 @@ describe('Executor Engine', () => {
|
|
|
61
61
|
expect(fs_1.default.existsSync(path_1.default.join(target, 'test.txt'))).toBe(true);
|
|
62
62
|
expect(fs_1.default.existsSync(staged)).toBe(false);
|
|
63
63
|
});
|
|
64
|
+
// Regression: a directory *symlink* ('dir') needs SeCreateSymbolicLinkPrivilege
|
|
65
|
+
// on native Windows, which unprivileged accounts (incl. GitHub Actions'
|
|
66
|
+
// windows-latest runner) don't have — every `awm init` install of a skill/
|
|
67
|
+
// agent directory threw EPERM there, failing the step and exiting the whole
|
|
68
|
+
// run with code 2 (see tests/integration/codex-provider-isolated.test.ts).
|
|
69
|
+
// A 'junction' is a different NTFS reparse-point kind that any account can
|
|
70
|
+
// create, and Node reports it the same way a symlink is reported
|
|
71
|
+
// (isSymbolicLink() true, readlinkSync() resolves it), so this only needs
|
|
72
|
+
// to change the `type` argument passed to fs.symlinkSync on win32 — nothing
|
|
73
|
+
// downstream (verify(), R19 hashing, doctor checks) needs to change.
|
|
74
|
+
describe('on native Windows', () => {
|
|
75
|
+
const realPlatform = process.platform;
|
|
76
|
+
let symlinkSpy;
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
79
|
+
symlinkSpy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => undefined);
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
83
|
+
symlinkSpy.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
it('stages a directory symlink as a junction instead of a dir-symlink', () => {
|
|
86
|
+
const target = path_1.default.join(targetDir, 'win-skill');
|
|
87
|
+
(0, executor_1.stageArtifact)(sourceDir, target, 'symlink');
|
|
88
|
+
expect(symlinkSpy).toHaveBeenCalledTimes(1);
|
|
89
|
+
const [calledSource, , calledType] = symlinkSpy.mock.calls[0];
|
|
90
|
+
expect(calledSource).toBe(sourceDir);
|
|
91
|
+
expect(calledType).toBe('junction');
|
|
92
|
+
});
|
|
93
|
+
// Regression (Failure 4/5, R6 round 3): a junction is a NTFS
|
|
94
|
+
// *directory* reparse point — it has no equivalent for a single FILE
|
|
95
|
+
// artifact (an agent/workflow .md). Before this branch existed, a file
|
|
96
|
+
// source got the exact same 'junction' treatment as a directory, which
|
|
97
|
+
// does not correctly resolve back to the file — see
|
|
98
|
+
// tests/core/bundle-install.test.ts's "claude-code agents" case, which
|
|
99
|
+
// reproduced this on windows-latest as a silently-missing target file.
|
|
100
|
+
it('stages a FILE source with a file-typed symlink, not a junction', () => {
|
|
101
|
+
const fileSource = path_1.default.join(sourceDir, 'test.txt');
|
|
102
|
+
const target = path_1.default.join(targetDir, 'win-agent.md');
|
|
103
|
+
(0, executor_1.stageArtifact)(fileSource, target, 'symlink');
|
|
104
|
+
expect(symlinkSpy).toHaveBeenCalledTimes(1);
|
|
105
|
+
const [calledSource, , calledType] = symlinkSpy.mock.calls[0];
|
|
106
|
+
expect(calledSource).toBe(fileSource);
|
|
107
|
+
expect(calledType).toBe('file');
|
|
108
|
+
});
|
|
109
|
+
it('falls back to a plain copy for a FILE source when the file symlink throws (EPERM — no SeCreateSymbolicLinkPrivilege)', () => {
|
|
110
|
+
symlinkSpy.mockImplementation(() => {
|
|
111
|
+
const err = new Error('EPERM: operation not permitted, symlink');
|
|
112
|
+
err.code = 'EPERM';
|
|
113
|
+
throw err;
|
|
114
|
+
});
|
|
115
|
+
const fileSource = path_1.default.join(sourceDir, 'test.txt');
|
|
116
|
+
const target = path_1.default.join(targetDir, 'win-agent-fallback.md');
|
|
117
|
+
const staged = (0, executor_1.stageArtifact)(fileSource, target, 'symlink');
|
|
118
|
+
expect(fs_1.default.lstatSync(staged).isSymbolicLink()).toBe(false);
|
|
119
|
+
expect(fs_1.default.readFileSync(staged, 'utf8')).toBe('hello');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
64
122
|
});
|
|
@@ -307,6 +307,52 @@ describe('defaultTransactionDeps — cursor-mdc / copilot-instructions renderers
|
|
|
307
307
|
expect(() => (0, install_transaction_1.applyInstallPlan)(plan, deps)).toThrow('does not look like rendered Copilot instructions');
|
|
308
308
|
});
|
|
309
309
|
});
|
|
310
|
+
// Regression (Failure 4/5, R6 round 3): a 'link'-renderer op whose sourcePath
|
|
311
|
+
// is a FILE (an agent/workflow .md, not a skill directory) used to be staged
|
|
312
|
+
// with the same directory-oriented junction/dir symlink type as a directory
|
|
313
|
+
// source — a junction has no equivalent for a single file, so it silently
|
|
314
|
+
// failed to resolve back to the target on native Windows. executor.ts's
|
|
315
|
+
// stageArtifact now dispatches on source kind and falls back to a plain copy
|
|
316
|
+
// for files when the real file-symlink throws (no privilege-free Windows
|
|
317
|
+
// equivalent to a directory junction exists for files); verify() here must
|
|
318
|
+
// accept that fallback rather than failing "not a symlink" for an install
|
|
319
|
+
// that landed correctly, just not as a symlink — see
|
|
320
|
+
// tests/core/executor.test.ts for the stageArtifact-level unit coverage and
|
|
321
|
+
// tests/integration/codex-provider-isolated.test.ts for the real end-to-end
|
|
322
|
+
// flow this was breaking (`InitStepsFailedError` -> exit code 2).
|
|
323
|
+
describe('defaultTransactionDeps — win32 FILE-symlink fallback (Failure 4/5 regression)', () => {
|
|
324
|
+
const realPlatform = process.platform;
|
|
325
|
+
let symlinkSpy;
|
|
326
|
+
beforeEach(() => {
|
|
327
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
328
|
+
symlinkSpy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => {
|
|
329
|
+
const err = new Error('EPERM: operation not permitted, symlink');
|
|
330
|
+
err.code = 'EPERM';
|
|
331
|
+
throw err;
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
afterEach(() => {
|
|
335
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
336
|
+
symlinkSpy.mockRestore();
|
|
337
|
+
});
|
|
338
|
+
it('completes the full validate/stage/replace/verify pipeline for a FILE `link` op via the copy fallback, instead of failing verification', () => {
|
|
339
|
+
const sourcePath = path_1.default.join(tmpWork, 'agent-source.md');
|
|
340
|
+
fs_1.default.writeFileSync(sourcePath, '---\nname: sample-agent\n---\nDo the thing.\n');
|
|
341
|
+
const targetPath = path_1.default.join(tmpWork, 'sample-agent.md');
|
|
342
|
+
const plan = {
|
|
343
|
+
operations: [makeOp('sample-agent', {
|
|
344
|
+
type: 'agent', renderer: 'link', output: 'link', method: 'symlink',
|
|
345
|
+
sourcePath, targetPath,
|
|
346
|
+
})],
|
|
347
|
+
records: [],
|
|
348
|
+
reports: [{ owner: 'claude-code', targetPath, action: 'install' }],
|
|
349
|
+
};
|
|
350
|
+
const summary = (0, install_transaction_1.applyInstallPlan)(plan);
|
|
351
|
+
expect(summary.modifiedFiles).toEqual([targetPath]);
|
|
352
|
+
expect(fs_1.default.lstatSync(targetPath).isSymbolicLink()).toBe(false); // copy fallback, not a symlink
|
|
353
|
+
expect(fs_1.default.readFileSync(targetPath, 'utf8')).toContain('Do the thing.');
|
|
354
|
+
});
|
|
355
|
+
});
|
|
310
356
|
describe('beginBackupSession / restoreBackup', () => {
|
|
311
357
|
it('backs up existing targets before mutation and restores them on rollback', () => {
|
|
312
358
|
const fileA = path_1.default.join(tmpWork, 'a.json');
|
|
@@ -349,8 +395,19 @@ describe('beginBackupSession / restoreBackup', () => {
|
|
|
349
395
|
const dirMode = fs_1.default.statSync(backupDir).mode & 0o777;
|
|
350
396
|
const manifestPath = path_1.default.join(backupDir, 'manifest.json');
|
|
351
397
|
const manifestMode = fs_1.default.statSync(manifestPath).mode & 0o777;
|
|
352
|
-
|
|
353
|
-
|
|
398
|
+
// Windows fs.chmod can only toggle the read-only attribute, not set granular
|
|
399
|
+
// POSIX bits (see tests/core/atomic-file.test.ts for the confirmed 0o600 ->
|
|
400
|
+
// 0o666 file shape on win32, verified against real windows-latest CI). Directory
|
|
401
|
+
// mode is reasoned by the same mechanism but not yet independently confirmed
|
|
402
|
+
// against real Windows CI for THIS exact 0o700 -> 0o777 case -- libuv derives
|
|
403
|
+
// directory mode on win32 by always setting the execute bit for every class
|
|
404
|
+
// (traversal isn't gated by chmod there), so 0o777 is the expected shape for a
|
|
405
|
+
// non-read-only directory; flag for correction if a real CI run disagrees.
|
|
406
|
+
// Confirmed against real windows-latest CI (2026-08-08): directories get the same
|
|
407
|
+
// 0o666 shape as files there, not 0o777 as first reasoned — libuv does not
|
|
408
|
+
// synthesize a distinct execute bit for directories on win32 either.
|
|
409
|
+
expect(dirMode).toBe(process.platform === 'win32' ? 0o666 : 0o700);
|
|
410
|
+
expect(manifestMode).toBe(process.platform === 'win32' ? 0o666 : 0o600);
|
|
354
411
|
const manifestRaw = fs_1.default.readFileSync(manifestPath, 'utf8');
|
|
355
412
|
expect(manifestRaw).not.toContain('secret-content');
|
|
356
413
|
});
|
|
@@ -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
|
+
});
|