agentic-workflow-manager 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/src/commands/job/gate.js +37 -13
- package/dist/src/commands/job/index.js +48 -8
- package/dist/src/commands/job/request.js +44 -6
- package/dist/src/commands/track/emit.js +26 -0
- package/dist/src/commands/track/index.js +194 -0
- package/dist/src/commands/track/status.js +63 -0
- package/dist/src/commands/track/supervisor-wrapper.js +89 -0
- package/dist/src/commands/watch/apply.js +201 -13
- package/dist/src/commands/watch/index.js +14 -0
- package/dist/src/commands/watch/runner.js +20 -1
- package/dist/src/commands/watch/supervisor.js +251 -27
- package/dist/src/commands/watch/teardown-driver.js +189 -0
- package/dist/src/commands/watch/tracks.js +1100 -0
- package/dist/src/core/diagnostics/provider-checks.js +12 -9
- package/dist/src/core/journal/adapter.js +46 -6
- package/dist/src/core/journal/paths.js +9 -0
- package/dist/src/core/journal/process.js +101 -1
- package/dist/src/core/journal/requests.js +5 -1
- package/dist/src/core/journal/store.js +34 -2
- package/dist/src/core/journal/types.js +93 -4
- package/dist/src/core/paths.js +51 -0
- package/dist/src/core/profile.js +15 -1
- package/dist/src/core/tracks/concurrency.js +85 -0
- package/dist/src/core/tracks/context.js +89 -0
- package/dist/src/core/tracks/descriptor.js +40 -0
- package/dist/src/core/tracks/git.js +318 -0
- package/dist/src/core/tracks/join.js +185 -0
- package/dist/src/core/tracks/ownership.js +100 -0
- package/dist/src/core/tracks/plan-parser.js +108 -0
- package/dist/src/core/tracks/protocol.js +466 -0
- package/dist/src/core/tracks/teardown.js +34 -0
- package/dist/src/core/tracks/types.js +14 -0
- package/dist/src/core/update-check.js +5 -1
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
- package/dist/tests/commands/track/fixtures.js +13 -0
- package/dist/tests/commands/track/status.test.js +157 -0
- package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
- package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
- package/dist/tests/commands/track/verbs.test.js +326 -0
- package/dist/tests/commands/watch/apply.test.js +116 -4
- package/dist/tests/commands/watch/runner.test.js +22 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
- package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
- package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
- package/dist/tests/commands/watch/track-finalize.test.js +643 -0
- package/dist/tests/commands/watch/track-freeze.test.js +591 -0
- package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
- package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
- package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
- package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
- package/dist/tests/core/journal/adapter-override.test.js +51 -0
- package/dist/tests/core/journal/process.test.js +37 -0
- package/dist/tests/core/journal/requests.test.js +21 -0
- package/dist/tests/core/journal/store.test.js +28 -0
- package/dist/tests/core/journal/types.test.js +89 -0
- package/dist/tests/core/profile.test.js +9 -2
- package/dist/tests/core/same-existing-path.test.js +48 -0
- package/dist/tests/core/tracks/concurrency.test.js +134 -0
- package/dist/tests/core/tracks/context.test.js +177 -0
- package/dist/tests/core/tracks/descriptor.test.js +76 -0
- package/dist/tests/core/tracks/git.test.js +132 -0
- package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
- package/dist/tests/core/tracks/join.test.js +197 -0
- package/dist/tests/core/tracks/ownership.test.js +96 -0
- package/dist/tests/core/tracks/plan-parser.test.js +94 -0
- package/dist/tests/core/tracks/protocol.test.js +416 -0
- package/dist/tests/core/tracks/teardown.test.js +62 -0
- package/dist/tests/core/update-check.test.js +34 -3
- package/dist/tests/helpers/git-fixture.js +35 -0
- package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
- package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
- package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const protocol_1 = require("../../../src/core/tracks/protocol");
|
|
4
|
+
const ids = ['cli', 'docs'];
|
|
5
|
+
describe('parallel-track protocol model', () => {
|
|
6
|
+
test('no activa parcialmente: todos deben estar ARMED (R4.4, C1)', () => {
|
|
7
|
+
let s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
8
|
+
s.tracks.cli.phase = 'ARMED';
|
|
9
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).not.toEqual({ kind: 'activate-cohort' });
|
|
10
|
+
s.tracks.docs.phase = 'ARMED';
|
|
11
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'activate-cohort' });
|
|
12
|
+
});
|
|
13
|
+
test('un fallo pre-ACTIVE entra a teardown y solo luego a serial (R4.5, C2)', () => {
|
|
14
|
+
let s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
15
|
+
s.tracks.cli.phase = 'WORKTREE_CREATED';
|
|
16
|
+
s.tracks.docs.phase = 'WORKTREE_CREATED';
|
|
17
|
+
s = (0, protocol_1.reconcileProtocol)(s, { kind: 'prepare-failed', trackId: 'docs', detail: 'disk full' });
|
|
18
|
+
expect(s.cohortPhase).toBe('FALLBACK_PENDING');
|
|
19
|
+
expect(s.tracks.docs.phase).toBe('TEARDOWN_REQUESTED'); // no BLOCKED: el fallo es demostrable
|
|
20
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'begin-teardown', trackId: 'cli' });
|
|
21
|
+
s.tracks.cli.phase = 'REMOVED';
|
|
22
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'begin-teardown', trackId: 'docs' });
|
|
23
|
+
s.tracks.docs.phase = 'REMOVED';
|
|
24
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'enter-serial', reason: 'prepare-failed:docs' });
|
|
25
|
+
});
|
|
26
|
+
test('un track BLOCKED nunca habilita serial (R4.6, C2)', () => {
|
|
27
|
+
let s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
28
|
+
s.tracks.cli.phase = 'REMOVED';
|
|
29
|
+
s.tracks.docs.phase = 'BLOCKED';
|
|
30
|
+
s.cohortPhase = 'FALLBACK_PENDING';
|
|
31
|
+
s.fallbackReason = 'prepare-failed:cli';
|
|
32
|
+
// Propiedad indemostrable: la cohorte se detiene, no serializa con recursos vivos.
|
|
33
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toBeNull();
|
|
34
|
+
expect(() => (0, protocol_1.observeProtocolEffect)(s, { kind: 'enter-serial', reason: 'x' }, { kind: 'effect-applied', effect: { kind: 'enter-serial', reason: 'x' } }))
|
|
35
|
+
.toThrow(/SERIAL prohíbe recursos paralelos activos/);
|
|
36
|
+
});
|
|
37
|
+
test('ningún merge empieza antes de que todos los tracks estén FROZEN (C3)', () => {
|
|
38
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
39
|
+
s.cohortPhase = 'JOINING';
|
|
40
|
+
s.planHeadSha = 'plan-head';
|
|
41
|
+
s.tracks.cli.phase = 'FROZEN';
|
|
42
|
+
s.tracks.cli.frozenHeadSha = 'cli-head';
|
|
43
|
+
s.tracks.docs.phase = 'ACTIVE';
|
|
44
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toBeNull(); // docs todavía trabaja: no se persiste join intent
|
|
45
|
+
s.tracks.docs.phase = 'JOIN_REQUESTED';
|
|
46
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'freeze-track', trackId: 'docs' });
|
|
47
|
+
});
|
|
48
|
+
test('los joins son serie estricta: un solo JOIN_INTENT vivo (R6.3, C7)', () => {
|
|
49
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
50
|
+
s.cohortPhase = 'JOINING';
|
|
51
|
+
s.planHeadSha = 'plan-head';
|
|
52
|
+
for (const t of Object.values(s.tracks)) {
|
|
53
|
+
t.phase = 'FROZEN';
|
|
54
|
+
t.frozenHeadSha = `${t.trackId}-head`;
|
|
55
|
+
}
|
|
56
|
+
const first = (0, protocol_1.nextProtocolEffect)(s);
|
|
57
|
+
expect(first).toEqual({
|
|
58
|
+
kind: 'persist-join-intent', trackId: 'cli',
|
|
59
|
+
expectedPlanHeadSha: 'plan-head', expectedTrackHeadSha: 'cli-head',
|
|
60
|
+
});
|
|
61
|
+
const armed = (0, protocol_1.observeProtocolEffect)(s, first, { kind: 'effect-applied', effect: first });
|
|
62
|
+
expect((0, protocol_1.nextProtocolEffect)(armed)).toMatchObject({ kind: 'merge-track', trackId: 'cli' });
|
|
63
|
+
expect(() => (0, protocol_1.assertProtocolInvariants)({
|
|
64
|
+
...armed, tracks: { ...armed.tracks, docs: { ...armed.tracks.docs, phase: 'JOIN_INTENT' } },
|
|
65
|
+
})).toThrow(/un solo JOIN_INTENT/);
|
|
66
|
+
});
|
|
67
|
+
test('el orden final no produce evidencia antes del HEAD final (R7.7, C3)', () => {
|
|
68
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
69
|
+
s.cohortPhase = 'JOINING';
|
|
70
|
+
s.planHeadSha = 'plan-head';
|
|
71
|
+
s.tracks.cli.phase = 'MERGED_UNVERIFIED';
|
|
72
|
+
s.tracks.cli.frozenHeadSha = 'cli-head';
|
|
73
|
+
s.tracks.docs.phase = 'FROZEN';
|
|
74
|
+
s.tracks.docs.frozenHeadSha = 'docs-head';
|
|
75
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).not.toMatchObject({ kind: 'request-global-qa' });
|
|
76
|
+
s.tracks.docs.phase = 'MERGED_UNVERIFIED';
|
|
77
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'request-global-qa' });
|
|
78
|
+
});
|
|
79
|
+
test('una observación indemostrable bloquea sin efecto destructivo (R6.9, C11)', () => {
|
|
80
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
81
|
+
s.cohortPhase = 'JOINING';
|
|
82
|
+
s.planHeadSha = 'plan-head';
|
|
83
|
+
for (const t of Object.values(s.tracks)) {
|
|
84
|
+
t.phase = 'FROZEN';
|
|
85
|
+
t.frozenHeadSha = `${t.trackId}-head`;
|
|
86
|
+
}
|
|
87
|
+
s.tracks.cli.phase = 'JOIN_INTENT';
|
|
88
|
+
s.tracks.cli.expectedPlanHeadSha = 'plan-head';
|
|
89
|
+
s.tracks.cli.expectedTrackHeadSha = 'cli-head';
|
|
90
|
+
const out = (0, protocol_1.reconcileProtocol)(s, { kind: 'join-observation', trackId: 'cli', mergeHead: 'other', planHead: 'other' });
|
|
91
|
+
expect(out.tracks.cli.phase).toBe('BLOCKED');
|
|
92
|
+
expect(out.cohortPhase).toBe('BLOCKED');
|
|
93
|
+
expect((0, protocol_1.nextProtocolEffect)(out)).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
test('ningún estado alcanzable viola invariantes al crashear entre fronteras (R4.2, R6.8, C9, C11)', () => {
|
|
96
|
+
const { states } = exploreCohort(ids);
|
|
97
|
+
for (const state of states)
|
|
98
|
+
expect(() => (0, protocol_1.assertProtocolInvariants)(state)).not.toThrow();
|
|
99
|
+
expect(states.length).toBeGreaterThan(50);
|
|
100
|
+
});
|
|
101
|
+
test('SERIAL solo es alcanzable con la cohorte desmantelada (R4.5, C2)', () => {
|
|
102
|
+
const serial = exploreCohort(ids).states.filter((s) => s.cohortPhase === 'SERIAL');
|
|
103
|
+
expect(serial.length).toBeGreaterThan(0);
|
|
104
|
+
for (const s of serial) {
|
|
105
|
+
expect(Object.values(s.tracks).every((t) => ['REMOVED', 'DECLARED'].includes(t.phase))).toBe(true);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
test('Step 6: el recorrido cubre las 14 fronteras', () => {
|
|
109
|
+
expect([...exploreCohort(ids).effects].sort()).toEqual([
|
|
110
|
+
'activate-cohort', 'activate-track', 'begin-teardown', 'create-track-journal', 'create-worktree',
|
|
111
|
+
'enter-serial', 'freeze-track', 'merge-track', 'persist-join-intent',
|
|
112
|
+
'persist-prepare-intent', 'request-final-integration', 'request-global-qa',
|
|
113
|
+
'run-final-interlock', 'spawn-track-supervisor',
|
|
114
|
+
].sort());
|
|
115
|
+
});
|
|
116
|
+
test('Step 3 (Task 9): el recorrido también ejercita decidePrepare, no solo nextProtocolEffect', () => {
|
|
117
|
+
// 'begin-fallback' queda deliberadamente afuera: es el fail-closed de
|
|
118
|
+
// una fase que `nextProtocolEffect` nunca selecciona para un efecto de
|
|
119
|
+
// PREPARING (ver el comentario de `decidePrepare`) — por diseño, la
|
|
120
|
+
// exploración (que solo llama `decidePrepare` con la fase que
|
|
121
|
+
// `nextProtocolEffect` ya eligió) jamás la alcanza; la matriz de
|
|
122
|
+
// arriba ('decidePrepare — Task 9') sí la cubre directamente.
|
|
123
|
+
expect([...exploreCohort(ids).prepareDecisions].sort()).toEqual([
|
|
124
|
+
'accept-readiness', 'accept-worktree', 'block-foreign',
|
|
125
|
+
'retry-supervisor-same-intent', 'retry-worktree', 'write-descriptor',
|
|
126
|
+
].sort());
|
|
127
|
+
});
|
|
128
|
+
test('Step 5 (Task 13): el recorrido también ejercita decideTeardown con las 7 decisiones posibles', () => {
|
|
129
|
+
// Mismo criterio que el test análogo de `prepareDecisions` (Step 3,
|
|
130
|
+
// Task 9): sin esta aserción, una regresión que dejara de alcanzar
|
|
131
|
+
// alguna `TeardownDecision` durante la exploración pasaría
|
|
132
|
+
// desapercibida, aunque el set se siguiera recolectando.
|
|
133
|
+
expect([...exploreCohort(ids).teardownDecisions].sort()).toEqual([
|
|
134
|
+
'accept-supervisor-stopped', 'block-foreign', 'mark-removed', 'persist-intent',
|
|
135
|
+
'remove-owned-branch', 'remove-owned-worktree', 'stop-own-supervisor',
|
|
136
|
+
].sort());
|
|
137
|
+
});
|
|
138
|
+
describe('decidePrepare — Task 9 (R4.2, R4.6, C11): matriz observable de recuperación de crash', () => {
|
|
139
|
+
const track = (phase) => ({
|
|
140
|
+
trackId: 'a', phase, fencingToken: 'f'.repeat(32), readinessNonce: 'r'.repeat(32),
|
|
141
|
+
});
|
|
142
|
+
const cases = [
|
|
143
|
+
// PREPARE_INTENT (create-worktree): ownedWorktreeExists manda por
|
|
144
|
+
// sobre "no vacío" — un destino nuestro nunca se re-crea ni bloquea.
|
|
145
|
+
['PREPARE_INTENT', {}, 'retry-worktree'],
|
|
146
|
+
['PREPARE_INTENT', { worktreeOwned: true }, 'accept-worktree'],
|
|
147
|
+
['PREPARE_INTENT', { worktreeOwned: true, worktreeForeignNonEmpty: true }, 'accept-worktree'],
|
|
148
|
+
['PREPARE_INTENT', { worktreeForeignNonEmpty: true }, 'block-foreign'],
|
|
149
|
+
['PREPARE_INTENT', { worktreeOwned: false, worktreeForeignNonEmpty: false }, 'retry-worktree'],
|
|
150
|
+
// WORKTREE_CREATED (create-track-journal): `initTrackJournal` ya es
|
|
151
|
+
// idempotente por construcción (initJournal no pisa un state.json
|
|
152
|
+
// existente, writeDescriptor sobreescribe igual siempre) — una
|
|
153
|
+
// sola decisión alcanza, sin necesitar distinguir "primera vez" de
|
|
154
|
+
// "reintento tras crash".
|
|
155
|
+
['WORKTREE_CREATED', {}, 'write-descriptor'],
|
|
156
|
+
// SUPERVISOR_STARTING (spawn-track-supervisor, con supervisorIntent
|
|
157
|
+
// ya persistido): 'absent' es el ÚNICO caso seguro para
|
|
158
|
+
// (re)spawnear — cualquier otra cosa es evidencia real de un
|
|
159
|
+
// intento previo (mismo intent) que jamás se debe duplicar.
|
|
160
|
+
['SUPERVISOR_STARTING', { supervisorArtifact: 'absent' }, 'retry-supervisor-same-intent'],
|
|
161
|
+
['SUPERVISOR_STARTING', {}, 'retry-supervisor-same-intent'],
|
|
162
|
+
['SUPERVISOR_STARTING', { supervisorArtifact: 'claimed' }, 'accept-readiness'],
|
|
163
|
+
['SUPERVISOR_STARTING', { supervisorArtifact: 'ready' }, 'accept-readiness'],
|
|
164
|
+
['SUPERVISOR_STARTING', { supervisorArtifact: 'foreign' }, 'block-foreign'],
|
|
165
|
+
// Fase inesperada para un efecto de PREPARING: fail-closed a fallback.
|
|
166
|
+
['ACTIVE', {}, 'begin-fallback'],
|
|
167
|
+
];
|
|
168
|
+
test.each(cases)('fase %s + observación %j -> %s', (phase, observed, expected) => {
|
|
169
|
+
expect((0, protocol_1.decidePrepare)(track(phase), observed)).toBe(expected);
|
|
170
|
+
});
|
|
171
|
+
test('la matriz cubre las 7 decisiones posibles', () => {
|
|
172
|
+
const seen = new Set(cases.map(([, , decision]) => decision));
|
|
173
|
+
expect([...seen].sort()).toEqual([
|
|
174
|
+
'accept-readiness', 'accept-worktree', 'begin-fallback', 'block-foreign',
|
|
175
|
+
'retry-supervisor-same-intent', 'retry-worktree', 'write-descriptor',
|
|
176
|
+
].sort());
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
/** Recorrido acotado del espacio de estados con crash inyectado en cada frontera. */
|
|
181
|
+
function exploreCohort(ids) {
|
|
182
|
+
const queue = [{ ...(0, protocol_1.initialCohort)('journal-1', ids), planHeadSha: 'plan-head' }];
|
|
183
|
+
const seen = new Map();
|
|
184
|
+
const effects = new Set();
|
|
185
|
+
const prepareDecisions = new Set();
|
|
186
|
+
const teardownDecisions = new Set();
|
|
187
|
+
while (queue.length > 0 && seen.size < 20000) {
|
|
188
|
+
const state = queue.shift();
|
|
189
|
+
const key = JSON.stringify(state);
|
|
190
|
+
if (seen.has(key))
|
|
191
|
+
continue;
|
|
192
|
+
seen.set(key, state);
|
|
193
|
+
const effect = (0, protocol_1.nextProtocolEffect)(state);
|
|
194
|
+
if (effect === null) {
|
|
195
|
+
// Quiescencia: solo avanza si llega un request externo (un track pide unirse).
|
|
196
|
+
for (const observation of externalObservationsFor(state)) {
|
|
197
|
+
queue.push((0, protocol_1.reconcileProtocol)(structuredClone(state), observation));
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
effects.add(effect.kind);
|
|
202
|
+
for (const observation of observationsFor(effect, state, prepareDecisions, teardownDecisions)) {
|
|
203
|
+
queue.push((0, protocol_1.observeProtocolEffect)(structuredClone(state), effect, observation));
|
|
204
|
+
queue.push((0, protocol_1.reconcileProtocol)(structuredClone(state), observation)); // crash antes de result
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { states: [...seen.values()], effects, prepareDecisions, teardownDecisions };
|
|
208
|
+
}
|
|
209
|
+
/** Requests que nacen fuera del reducer y por eso no son consecuencia de ningún effect. */
|
|
210
|
+
function externalObservationsFor(state) {
|
|
211
|
+
return Object.values(state.tracks)
|
|
212
|
+
.filter((t) => t.phase === 'ACTIVE')
|
|
213
|
+
.map((t) => ({ kind: 'join-requested', trackId: t.trackId }));
|
|
214
|
+
}
|
|
215
|
+
/** Los 7 valores que `decidePrepare` puede devolver (mismo catálogo que
|
|
216
|
+
* `PrepareDecision`) — usado para que la exploración denuncie si algún día
|
|
217
|
+
* devuelve algo fuera de este conjunto. */
|
|
218
|
+
const VALID_PREPARE_DECISIONS = [
|
|
219
|
+
'retry-worktree', 'accept-worktree', 'write-descriptor',
|
|
220
|
+
'retry-supervisor-same-intent', 'accept-readiness', 'block-foreign', 'begin-fallback',
|
|
221
|
+
];
|
|
222
|
+
/** Variantes de `PrepareObservation` que un driver real reuniría para
|
|
223
|
+
* `create-worktree` (fase PREPARE_INTENT) — mismo catálogo que la matriz
|
|
224
|
+
* `decidePrepare — Task 9` de más abajo. */
|
|
225
|
+
const PREPARE_OBSERVATIONS_FOR_WORKTREE = [
|
|
226
|
+
{}, { worktreeOwned: true }, { worktreeOwned: true, worktreeForeignNonEmpty: true },
|
|
227
|
+
{ worktreeForeignNonEmpty: true }, { worktreeOwned: false, worktreeForeignNonEmpty: false },
|
|
228
|
+
];
|
|
229
|
+
/** Las 7 decisiones que `decideTeardown` puede devolver (Task 13) — usado
|
|
230
|
+
* para que la exploración denuncie si algún día devuelve algo fuera de este
|
|
231
|
+
* conjunto, mismo criterio que `VALID_PREPARE_DECISIONS`. */
|
|
232
|
+
const VALID_TEARDOWN_DECISIONS = [
|
|
233
|
+
'persist-intent', 'stop-own-supervisor', 'accept-supervisor-stopped',
|
|
234
|
+
'remove-owned-worktree', 'remove-owned-branch', 'mark-removed', 'block-foreign',
|
|
235
|
+
];
|
|
236
|
+
/** Variantes de `TeardownObservation` relevantes a CADA fase de teardown —
|
|
237
|
+
* mismo criterio que `PREPARE_OBSERVATIONS_FOR_WORKTREE`/`_SUPERVISOR`:
|
|
238
|
+
* cubre lo que `gatherTeardownObservation` (`tracks.ts`) realmente puede
|
|
239
|
+
* reunir en esa fase, más las variantes "ajeno" defensivas que
|
|
240
|
+
* `decideTeardown` también contempla (Step 1 del plan T13). */
|
|
241
|
+
const TEARDOWN_OBSERVATIONS_BY_PHASE = {
|
|
242
|
+
TEARDOWN_REQUESTED: [{}],
|
|
243
|
+
TEARDOWN_INTENT: [{}, { ownSupervisorAlive: true }, { ownSupervisorAlive: false }, { foreignSupervisor: true }],
|
|
244
|
+
SUPERVISOR_STOPPED: [{}, { ownedWorktreeExists: true }, { ownedWorktreeExists: false }, { foreignWorktree: true }],
|
|
245
|
+
WORKTREE_REMOVED: [{}, { ownedBranchExists: true }, { ownedBranchExists: false }],
|
|
246
|
+
BRANCH_REMOVED: [{}],
|
|
247
|
+
};
|
|
248
|
+
/** Idem para `spawn-track-supervisor` (fase SUPERVISOR_STARTING). */
|
|
249
|
+
const PREPARE_OBSERVATIONS_FOR_SUPERVISOR = [
|
|
250
|
+
{}, { supervisorArtifact: 'absent' }, { supervisorArtifact: 'claimed' },
|
|
251
|
+
{ supervisorArtifact: 'ready' }, { supervisorArtifact: 'foreign' },
|
|
252
|
+
];
|
|
253
|
+
/**
|
|
254
|
+
* Task 9 (Step 3 del plan): llama a `decidePrepare` — la MISMA autoridad que
|
|
255
|
+
* usa `tracks.ts` — para cada `PrepareObservation` relevante a la fase que
|
|
256
|
+
* `nextProtocolEffect` ya eligió, y traduce cada decisión a la
|
|
257
|
+
* `ProtocolObservation` que un driver real produciría con ella (mismo mapeo,
|
|
258
|
+
* letra por letra, que `runCreateWorktree`/`runSpawnTrackSupervisor` en
|
|
259
|
+
* `tracks.ts`). Sin esto, `nextProtocolEffect` (qué efecto sigue) y
|
|
260
|
+
* `decidePrepare` (qué hacer con ESE efecto) podían divergir en silencio —
|
|
261
|
+
* ningún test lo hubiera visto porque la exploración nunca invocaba
|
|
262
|
+
* `decidePrepare`. Las observaciones devueltas se mezclan con las de
|
|
263
|
+
* `observationsFor` y entran al mismo BFS: si alguna hiciera que
|
|
264
|
+
* `reconcileProtocol`/`observeProtocolEffect` tiraran, o llevara a un estado
|
|
265
|
+
* que viola invariantes, el test de exploración ya existente lo denuncia.
|
|
266
|
+
*/
|
|
267
|
+
function decidePrepareObservationsFor(effect, state, prepareDecisions) {
|
|
268
|
+
if (effect.kind !== 'create-worktree' && effect.kind !== 'create-track-journal' && effect.kind !== 'spawn-track-supervisor') {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const track = state.tracks[effect.trackId];
|
|
272
|
+
// `spawn-track-supervisor` cubre DOS fases (JOURNAL_CREATED |
|
|
273
|
+
// SUPERVISOR_STARTING, ver `nextProtocolEffect`), pero `tracks.ts`
|
|
274
|
+
// (`runSpawnTrackSupervisor`) solo invoca `decidePrepare` en la SEGUNDA
|
|
275
|
+
// — la primera vez (JOURNAL_CREATED, `ref.supervisorIntent === undefined`)
|
|
276
|
+
// persiste el intent y retorna ANTES de siquiera considerar `decidePrepare`,
|
|
277
|
+
// porque el protocolo puro no distingue "intent ya persistido" (eso vive
|
|
278
|
+
// solo en `TrackRef.supervisorIntent`, un dato del journal — regla de
|
|
279
|
+
// capas de T1, `protocol.ts` no lo conoce). Llamar `decidePrepare` acá con
|
|
280
|
+
// JOURNAL_CREATED probaría un caso que el driver real NUNCA ejercita
|
|
281
|
+
// (decidePrepare cae a 'begin-fallback' porque no tiene rama para esa
|
|
282
|
+
// fase) — se salta, fiel al comportamiento real, no a lo que `protocol.ts`
|
|
283
|
+
// podría en teoría hacer con ella.
|
|
284
|
+
if (effect.kind === 'spawn-track-supervisor' && track.phase !== 'SUPERVISOR_STARTING')
|
|
285
|
+
return [];
|
|
286
|
+
const variants = effect.kind === 'create-worktree' ? PREPARE_OBSERVATIONS_FOR_WORKTREE
|
|
287
|
+
: effect.kind === 'spawn-track-supervisor' ? PREPARE_OBSERVATIONS_FOR_SUPERVISOR
|
|
288
|
+
: [{}];
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const observed of variants) {
|
|
291
|
+
const decision = (0, protocol_1.decidePrepare)(track, observed);
|
|
292
|
+
expect(VALID_PREPARE_DECISIONS).toContain(decision);
|
|
293
|
+
prepareDecisions.add(decision);
|
|
294
|
+
if (effect.kind === 'create-worktree') {
|
|
295
|
+
if (decision === 'accept-worktree')
|
|
296
|
+
out.push({ kind: 'worktree-observed', trackId: effect.trackId, owned: true });
|
|
297
|
+
else if (decision === 'block-foreign')
|
|
298
|
+
out.push({ kind: 'worktree-observed', trackId: effect.trackId, owned: false });
|
|
299
|
+
// 'retry-worktree': recién ahí `tracks.ts` toca `runtime` de
|
|
300
|
+
// verdad — su resultado real (éxito u `effect-failed`) ya está
|
|
301
|
+
// en el `case 'create-worktree'` de `observationsFor`.
|
|
302
|
+
}
|
|
303
|
+
else if (effect.kind === 'spawn-track-supervisor') {
|
|
304
|
+
if (decision === 'block-foreign')
|
|
305
|
+
out.push({ kind: 'supervisor-observed', trackId: effect.trackId, identity: 'other' });
|
|
306
|
+
else if (decision === 'accept-readiness' && observed.supervisorArtifact === 'ready') {
|
|
307
|
+
out.push({ kind: 'supervisor-observed', trackId: effect.trackId, identity: 'expected', readinessNonce: effect.readinessNonce });
|
|
308
|
+
}
|
|
309
|
+
// 'retry-supervisor-same-intent' y 'accept-readiness' con
|
|
310
|
+
// 'claimed': ningún tick produce todavía una observación de
|
|
311
|
+
// protocolo (esperan al próximo, o recién ahí tocan `runtime`).
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// create-track-journal: `write-descriptor` es la única decisión
|
|
315
|
+
// posible en WORKTREE_CREATED (initTrackJournal ya es idempotente
|
|
316
|
+
// por construcción) — su resultado ya lo cubre el `default` de
|
|
317
|
+
// `observationsFor`.
|
|
318
|
+
expect(decision).toBe('write-descriptor');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Task 13 (mismo criterio que `decidePrepareObservationsFor`, Task 9): llama
|
|
325
|
+
* a `decideTeardown` — la MISMA autoridad que usa `runBeginTeardown` — para
|
|
326
|
+
* cada `TeardownObservation` relevante a la fase ACTUAL del track en
|
|
327
|
+
* `begin-teardown`, y traduce cada decisión "ya resuelta" (`block-foreign`,
|
|
328
|
+
* `accept-supervisor-stopped`, `remove-owned-branch` desde SUPERVISOR_STOPPED,
|
|
329
|
+
* `mark-removed`) a la `teardown-observation` que un driver real produciría
|
|
330
|
+
* con ella (mismo mapeo que `runBeginTeardown` en `tracks.ts`). Las
|
|
331
|
+
* decisiones "falta trabajo real" (`stop-own-supervisor`,
|
|
332
|
+
* `remove-owned-worktree`, `remove-owned-branch` desde WORKTREE_REMOVED,
|
|
333
|
+
* `persist-intent`) no producen observación acá — su resultado real (éxito
|
|
334
|
+
* u `effect-failed`) ya lo cubre el `case 'begin-teardown'` de
|
|
335
|
+
* `observationsFor`.
|
|
336
|
+
*/
|
|
337
|
+
function decideTeardownObservationsFor(effect, state, teardownDecisions) {
|
|
338
|
+
if (effect.kind !== 'begin-teardown')
|
|
339
|
+
return [];
|
|
340
|
+
const track = state.tracks[effect.trackId];
|
|
341
|
+
const variants = TEARDOWN_OBSERVATIONS_BY_PHASE[track.phase] ?? [{}];
|
|
342
|
+
const out = [];
|
|
343
|
+
for (const observed of variants) {
|
|
344
|
+
const decision = (0, protocol_1.decideTeardown)(track, observed);
|
|
345
|
+
expect(VALID_TEARDOWN_DECISIONS).toContain(decision);
|
|
346
|
+
teardownDecisions.add(decision);
|
|
347
|
+
if (decision === 'block-foreign' || decision === 'accept-supervisor-stopped' || decision === 'mark-removed'
|
|
348
|
+
|| (decision === 'remove-owned-branch' && track.phase === 'SUPERVISOR_STOPPED')) {
|
|
349
|
+
out.push({ kind: 'teardown-observation', trackId: effect.trackId, ...observed });
|
|
350
|
+
}
|
|
351
|
+
// 'persist-intent' / 'stop-own-supervisor' / 'remove-owned-worktree'
|
|
352
|
+
// / 'remove-owned-branch' (desde WORKTREE_REMOVED, branch real por
|
|
353
|
+
// remover): el driver TODAVÍA no completó el efecto real acá.
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
function observationsFor(effect, state, prepareDecisions, teardownDecisions) {
|
|
358
|
+
if (effect === null)
|
|
359
|
+
return [];
|
|
360
|
+
const base = (() => {
|
|
361
|
+
switch (effect.kind) {
|
|
362
|
+
case 'create-worktree': return [
|
|
363
|
+
{ kind: 'effect-applied', effect },
|
|
364
|
+
{ kind: 'worktree-observed', trackId: effect.trackId, owned: true },
|
|
365
|
+
{ kind: 'worktree-observed', trackId: effect.trackId, owned: false },
|
|
366
|
+
{ kind: 'effect-failed', trackId: effect.trackId, effect: effect.kind, detail: 'injected' },
|
|
367
|
+
];
|
|
368
|
+
case 'merge-track': return [
|
|
369
|
+
{ kind: 'join-observation', trackId: effect.trackId, mergeHead: null, planHead: 'merged', trackIsAncestor: true },
|
|
370
|
+
{ kind: 'join-observation', trackId: effect.trackId, mergeHead: effect.expectedTrackHeadSha, planHead: effect.expectedPlanHeadSha },
|
|
371
|
+
];
|
|
372
|
+
case 'freeze-track': return [
|
|
373
|
+
{ kind: 'effect-applied', effect },
|
|
374
|
+
{ kind: 'freeze-observation', trackId: effect.trackId, frozenHeadSha: `${effect.trackId}-head` },
|
|
375
|
+
];
|
|
376
|
+
case 'spawn-track-supervisor': return [
|
|
377
|
+
{ kind: 'effect-applied', effect },
|
|
378
|
+
{ kind: 'supervisor-observed', trackId: effect.trackId, identity: 'expected', readinessNonce: effect.readinessNonce },
|
|
379
|
+
{ kind: 'supervisor-observed', trackId: effect.trackId, identity: 'expected', readinessNonce: 'nonce-ajeno' },
|
|
380
|
+
{ kind: 'supervisor-observed', trackId: effect.trackId, identity: 'other' },
|
|
381
|
+
];
|
|
382
|
+
// T13: reemplaza las observaciones gruesas `track-removed`/
|
|
383
|
+
// `teardown-blocked` por el vocabulario fino de
|
|
384
|
+
// `TeardownObservation` — `decideTeardownObservationsFor` abajo
|
|
385
|
+
// agrega, además, las variantes específicas de la fase actual
|
|
386
|
+
// (mismo criterio que `decidePrepareObservationsFor`).
|
|
387
|
+
case 'begin-teardown': return [
|
|
388
|
+
{ kind: 'effect-applied', effect },
|
|
389
|
+
{ kind: 'effect-failed', trackId: effect.trackId, effect: effect.kind, detail: 'worktree sucio: x' },
|
|
390
|
+
];
|
|
391
|
+
case 'request-global-qa': return [{ kind: 'global-qa-pass', headSha: 'H-qa', clean: true }];
|
|
392
|
+
case 'request-final-integration': return [{ kind: 'integration-pass', jobId: 'job-1', headSha: 'H-qa' }];
|
|
393
|
+
case 'run-final-interlock': return [{ kind: 'interlock-pass', headSha: 'H-qa' }];
|
|
394
|
+
default: return [{ kind: 'effect-applied', effect }];
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
397
|
+
// Task 9 (Step 3): agrega las observaciones que `decidePrepare` ejercita
|
|
398
|
+
// para las 3 fronteras de PREPARING que gobierna, deduplicando contra las
|
|
399
|
+
// que este mismo `switch` ya listaba a mano.
|
|
400
|
+
const seen = new Set(base.map((o) => JSON.stringify(o)));
|
|
401
|
+
for (const observation of decidePrepareObservationsFor(effect, state, prepareDecisions)) {
|
|
402
|
+
const key = JSON.stringify(observation);
|
|
403
|
+
if (!seen.has(key)) {
|
|
404
|
+
base.push(observation);
|
|
405
|
+
seen.add(key);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const observation of decideTeardownObservationsFor(effect, state, teardownDecisions)) {
|
|
409
|
+
const key = JSON.stringify(observation);
|
|
410
|
+
if (!seen.has(key)) {
|
|
411
|
+
base.push(observation);
|
|
412
|
+
seen.add(key);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return base;
|
|
416
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): matriz PURA de `decideTeardown` —
|
|
4
|
+
// mismo criterio que `join-reconcile.test.ts` (T11): importa el re-export de
|
|
5
|
+
// `teardown.ts`, así que cualquier fix que esta matriz descubra vive en
|
|
6
|
+
// `protocol.ts` (autoridad única) y `protocol.test.ts` vuelve a correr su
|
|
7
|
+
// exploración en el mismo commit.
|
|
8
|
+
const teardown_1 = require("../../../src/core/tracks/teardown");
|
|
9
|
+
const phase = (p) => ({
|
|
10
|
+
trackId: 'a', phase: p, fencingToken: 'f'.repeat(32), readinessNonce: 'r'.repeat(32),
|
|
11
|
+
});
|
|
12
|
+
const observed = (o = {}) => o;
|
|
13
|
+
describe('decideTeardown — Task 13 (R4.2/R4.3/R4.6/R4.10, C2/C9): matriz pura', () => {
|
|
14
|
+
const cases = [
|
|
15
|
+
['request sin intent', phase('TEARDOWN_REQUESTED'), observed(), 'persist-intent'],
|
|
16
|
+
['supervisor propio vivo', phase('TEARDOWN_INTENT'), observed({ ownSupervisorAlive: true }), 'stop-own-supervisor'],
|
|
17
|
+
['identidad ajena', phase('TEARDOWN_INTENT'), observed({ foreignSupervisor: true }), 'block-foreign'],
|
|
18
|
+
['supervisor ausente', phase('TEARDOWN_INTENT'), observed({ ownSupervisorAlive: false }), 'accept-supervisor-stopped'],
|
|
19
|
+
['worktree propio', phase('SUPERVISOR_STOPPED'), observed({ ownedWorktreeExists: true }), 'remove-owned-worktree'],
|
|
20
|
+
['worktree ajeno', phase('SUPERVISOR_STOPPED'), observed({ foreignWorktree: true }), 'block-foreign'],
|
|
21
|
+
['branch propia', phase('WORKTREE_REMOVED'), observed({ ownedBranchExists: true }), 'remove-owned-branch'],
|
|
22
|
+
['todo ausente', phase('BRANCH_REMOVED'), observed(), 'mark-removed'],
|
|
23
|
+
];
|
|
24
|
+
test.each(cases)('%s (R4.3, C9)', (_name, ref, observation, action) => {
|
|
25
|
+
expect((0, teardown_1.decideTeardown)(ref, observation)).toBe(action);
|
|
26
|
+
});
|
|
27
|
+
// Cobertura complementaria (no listada en el snippet del plan, pero
|
|
28
|
+
// exigida por "identidad ajena SIEMPRE gana primero, sin importar la
|
|
29
|
+
// fase" del comentario de `decideTeardown`): `foreignSupervisor`/
|
|
30
|
+
// `foreignWorktree` bloquean sin importar en qué paso del teardown esté
|
|
31
|
+
// el track — nunca se toca algo ajeno solo porque la fase "no le toca".
|
|
32
|
+
test('ajeno gana primero sin importar la fase (R4.6, C9)', () => {
|
|
33
|
+
expect((0, teardown_1.decideTeardown)(phase('SUPERVISOR_STOPPED'), { foreignSupervisor: true })).toBe('block-foreign');
|
|
34
|
+
expect((0, teardown_1.decideTeardown)(phase('WORKTREE_REMOVED'), { foreignWorktree: true })).toBe('block-foreign');
|
|
35
|
+
});
|
|
36
|
+
test('sin worktree ni branch propios, SUPERVISOR_STOPPED avanza directo a remove-owned-branch (mecánico, C9)', () => {
|
|
37
|
+
expect((0, teardown_1.decideTeardown)(phase('SUPERVISOR_STOPPED'), {})).toBe('remove-owned-branch');
|
|
38
|
+
});
|
|
39
|
+
test('WORKTREE_REMOVED sin branch propia marca removed (mecánico, C9)', () => {
|
|
40
|
+
expect((0, teardown_1.decideTeardown)(phase('WORKTREE_REMOVED'), {})).toBe('mark-removed');
|
|
41
|
+
});
|
|
42
|
+
// R4.6/C9: BLOCKED es un dead end DELIBERADO — ninguna fase alcanzable
|
|
43
|
+
// por `nextProtocolEffect` para `begin-teardown` es BLOCKED (se filtra
|
|
44
|
+
// antes, ver `protocol.ts`), así que `decideTeardown` jamás la resuelve
|
|
45
|
+
// sola: lanza fail-closed en vez de inventar una salida automática. Salir
|
|
46
|
+
// de BLOCKED es siempre un acto explícito del operador (volver a
|
|
47
|
+
// TEARDOWN_REQUESTED), nunca algo que esta función decida por sí misma.
|
|
48
|
+
test('BLOCKED nunca es una fase de entrada válida (R4.6, C9): fail-closed', () => {
|
|
49
|
+
expect(() => (0, teardown_1.decideTeardown)(phase('BLOCKED'), {})).toThrow(/teardown no válido desde BLOCKED/);
|
|
50
|
+
});
|
|
51
|
+
test('fase de trabajo normal (nunca en teardown) también fail-closed', () => {
|
|
52
|
+
expect(() => (0, teardown_1.decideTeardown)(phase('ACTIVE'), {})).toThrow(/teardown no válido desde ACTIVE/);
|
|
53
|
+
});
|
|
54
|
+
test('la matriz cubre las 7 decisiones posibles', () => {
|
|
55
|
+
const seen = new Set(cases.map(([, , , decision]) => decision));
|
|
56
|
+
seen.add((0, teardown_1.decideTeardown)(phase('SUPERVISOR_STOPPED'), {})); // remove-owned-branch (variante mecánica)
|
|
57
|
+
expect([...seen].sort()).toEqual([
|
|
58
|
+
'accept-supervisor-stopped', 'block-foreign', 'mark-removed',
|
|
59
|
+
'persist-intent', 'remove-owned-branch', 'remove-owned-worktree', 'stop-own-supervisor',
|
|
60
|
+
].sort());
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -59,12 +59,14 @@ describe('update-check', () => {
|
|
|
59
59
|
it('maybeNotifyUpdate avisa si el cache trae versión más nueva y NO refresca cache fresco', () => {
|
|
60
60
|
const m = require('../../src/core/update-check');
|
|
61
61
|
m.writeUpdateCache({ lastCheck: 1_000_000, latest: '99.0.0' });
|
|
62
|
-
|
|
62
|
+
// stderr, no stdout: el aviso sale al final de cualquier comando y en stdout
|
|
63
|
+
// rompia la salida de `--json`.
|
|
64
|
+
const err = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
63
65
|
const spawnWorker = jest.fn();
|
|
64
66
|
m.maybeNotifyUpdate({ now: 1_000_000 + 1000, spawnWorker });
|
|
65
|
-
expect(
|
|
67
|
+
expect(err.mock.calls.flat().join('\n')).toContain('99.0.0');
|
|
66
68
|
expect(spawnWorker).not.toHaveBeenCalled();
|
|
67
|
-
|
|
69
|
+
err.mockRestore();
|
|
68
70
|
});
|
|
69
71
|
it('cache viejo (>24h) dispara refresh en background', () => {
|
|
70
72
|
const m = require('../../src/core/update-check');
|
|
@@ -104,3 +106,32 @@ describe('update-check', () => {
|
|
|
104
106
|
expect(cache.lastCheck).toBeGreaterThan(0);
|
|
105
107
|
});
|
|
106
108
|
});
|
|
109
|
+
// El aviso de version nueva va por stderr, no por stdout.
|
|
110
|
+
//
|
|
111
|
+
// Se imprime al final de CUALQUIER comando, asi que en stdout se mezclaba con la salida
|
|
112
|
+
// de `--json` y rompia a cualquiera que parsee. Encontrado tropezando con el:
|
|
113
|
+
// `awm doctor --json | node -e 'JSON.parse(...)'` fallaba con un SyntaxError que no
|
|
114
|
+
// menciona la causa. stdout es la interfaz de maquina.
|
|
115
|
+
describe('the update banner never contaminates stdout', () => {
|
|
116
|
+
it('writes to stderr so `--json` output stays parseable', () => {
|
|
117
|
+
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
118
|
+
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
119
|
+
const log = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
120
|
+
try {
|
|
121
|
+
const { maybeNotifyUpdate } = require('../../src/core/update-check');
|
|
122
|
+
maybeNotifyUpdate({ spawnWorker: () => { } });
|
|
123
|
+
const onStdout = stdout.mock.calls.map((c) => String(c[0])).join('')
|
|
124
|
+
+ log.mock.calls.map((c) => String(c[0])).join('');
|
|
125
|
+
expect(onStdout).not.toContain('available');
|
|
126
|
+
// Y si habia aviso, salio por stderr — no se perdio, se movio.
|
|
127
|
+
const emitted = stderr.mock.calls.map((c) => String(c[0])).join('');
|
|
128
|
+
if (emitted.length > 0)
|
|
129
|
+
expect(emitted).toContain('available');
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
stdout.mockRestore();
|
|
133
|
+
stderr.mockRestore();
|
|
134
|
+
log.mockRestore();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
exports.initRepo = initRepo;
|
|
7
|
+
exports.commitFile = commitFile;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const process_1 = require("../../src/core/journal/process");
|
|
13
|
+
/** Identidad de commit local al tmpdir del fixture — jamas toca ~/.gitconfig
|
|
14
|
+
* ni ninguna config global (CLAUDE.md: `~/.awm` y el entorno real del dueño
|
|
15
|
+
* son territorio prohibido para sesiones de desarrollo/tests). `EXEC_STDIO`
|
|
16
|
+
* explicito por la misma razon que en git.ts (ver process.ts): sin esto
|
|
17
|
+
* execFileSync hereda el stderr del proceso llamante. */
|
|
18
|
+
const git = (repo, args) => (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd: repo, encoding: 'utf8', stdio: process_1.EXEC_STDIO });
|
|
19
|
+
/** Crea un repo git aislado en un tmpdir propio. El caller es responsable de
|
|
20
|
+
* `fs.rmSync(repo, { recursive: true, force: true })` en su `afterEach`. */
|
|
21
|
+
function initRepo() {
|
|
22
|
+
const repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-git-fixture-'));
|
|
23
|
+
// `-b main` fija el nombre de la rama inicial explicitamente: sin esto el
|
|
24
|
+
// default depende de `init.defaultBranch` (global, no tocable desde aca
|
|
25
|
+
// per CLAUDE.md) y varia entre instalaciones de git (master vs main).
|
|
26
|
+
git(repo, ['init', '-q', '-b', 'main']);
|
|
27
|
+
return repo;
|
|
28
|
+
}
|
|
29
|
+
/** Escribe `name` con `content`, lo commitea y devuelve el SHA resultante. */
|
|
30
|
+
function commitFile(repo, name, content) {
|
|
31
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, name), content);
|
|
32
|
+
git(repo, ['add', '--', name]);
|
|
33
|
+
git(repo, ['commit', '-qm', `add ${name}`]);
|
|
34
|
+
return git(repo, ['rev-parse', 'HEAD']).trim();
|
|
35
|
+
}
|