agentic-workflow-manager 9.7.1 → 9.8.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 +5 -1
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- package/dist/src/commands/plan/index.js +124 -6
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +181 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +35 -1
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/plan/identity.js +75 -0
- package/dist/src/core/plan/validate.js +178 -64
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +630 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +87 -0
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -0
- package/dist/tests/core/plan/validate.test.js +320 -6
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +16 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -224,7 +224,7 @@ describe('computeSensorStatus', () => {
|
|
|
224
224
|
variants: [{
|
|
225
225
|
id: 'eslint-10', priority: 10, certifiedRange: '>=10.0.0 <11.0.0',
|
|
226
226
|
requirements: { tool: 'eslint', toolRange: '>=10.0.0 <11.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
227
|
-
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
|
|
227
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present', script: 'lint' },
|
|
228
228
|
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--config', 'eslint.config.awm.mjs'] },
|
|
229
229
|
}],
|
|
230
230
|
} },
|
|
@@ -291,7 +291,7 @@ describe('computeSensorStatus', () => {
|
|
|
291
291
|
const variant = (id, range) => ({
|
|
292
292
|
id, priority: 10, certifiedRange: range,
|
|
293
293
|
requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
294
|
-
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
|
|
294
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present', script: 'lint' },
|
|
295
295
|
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
296
296
|
});
|
|
297
297
|
fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
|
|
@@ -381,7 +381,7 @@ describe('computeSensorStatus', () => {
|
|
|
381
381
|
sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
|
|
382
382
|
id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
|
|
383
383
|
requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
|
|
384
|
-
assets: [], formatter: 'generic', probe: { kind: 'package-script-present' },
|
|
384
|
+
assets: [], formatter: 'generic', probe: { kind: 'package-script-present', script: 'lint' },
|
|
385
385
|
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
386
386
|
}] } },
|
|
387
387
|
};
|
|
@@ -9,6 +9,8 @@ const os_1 = __importDefault(require("os"));
|
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const process_1 = require("../../../src/core/journal/process");
|
|
11
11
|
const paths_1 = require("../../../src/core/paths");
|
|
12
|
+
const run_1 = require("../../../src/commands/sensors/run");
|
|
13
|
+
const live_1 = require("../../../src/commands/sensors/compatibility/live");
|
|
12
14
|
/** win32 has no POSIX process groups / negative-pid kill convention -- mirrors
|
|
13
15
|
* killTreeWindows's taskkill pattern already established and tested in
|
|
14
16
|
* core/journal/process.ts, applied here to this test's own cleanup (not
|
|
@@ -27,6 +29,36 @@ function killGroup(pgid) {
|
|
|
27
29
|
}
|
|
28
30
|
catch { /* ya muerto */ }
|
|
29
31
|
}
|
|
32
|
+
function groupAlive(pgid) {
|
|
33
|
+
if ((0, paths_1.isWindowsNative)())
|
|
34
|
+
return false; // taskkill above is synchronous enough for this fixture.
|
|
35
|
+
try {
|
|
36
|
+
process.kill(-pgid, 0);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function terminateFixtureGroups(groups) {
|
|
44
|
+
for (const pgid of groups)
|
|
45
|
+
killGroup(pgid);
|
|
46
|
+
await until(() => [...groups].every(pgid => !groupAlive(pgid)), 5_000, 'terminacion de grupos del fixture');
|
|
47
|
+
}
|
|
48
|
+
async function removeFixture(pathname) {
|
|
49
|
+
let last;
|
|
50
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
fs_1.default.rmSync(pathname, { recursive: true, force: true, maxRetries: 1, retryDelay: 25 });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw last;
|
|
61
|
+
}
|
|
30
62
|
jest.setTimeout(180000);
|
|
31
63
|
// R1.8 promete "el wrapper sobrevive incluso si el supervisor muere" — en
|
|
32
64
|
// POSIX esto se sostiene en `detached: true` (nueva sesion, sobrevive un
|
|
@@ -51,6 +83,37 @@ function readState(repo) {
|
|
|
51
83
|
return null;
|
|
52
84
|
}
|
|
53
85
|
}
|
|
86
|
+
/** A physical capability-only registry is intentional: sensor pack resolution
|
|
87
|
+
* rejects a linked registry root, because a link could later escape its
|
|
88
|
+
* configured authority. Keep the v3 logical source name and the registry
|
|
89
|
+
* configuration aligned so this fixture exercises the real resolver. */
|
|
90
|
+
function writeFixtureBaselineRegistry(awmHome) {
|
|
91
|
+
const packDir = path_1.default.join(awmHome, 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
92
|
+
fs_1.default.mkdirSync(packDir, { recursive: true });
|
|
93
|
+
fs_1.default.writeFileSync(path_1.default.join(packDir, 'pack.json'), JSON.stringify({
|
|
94
|
+
schemaVersion: 2,
|
|
95
|
+
name: 'js-ts',
|
|
96
|
+
description: 'E2E compact admission sensor fixture',
|
|
97
|
+
detects: ['package.json'],
|
|
98
|
+
sensors: {
|
|
99
|
+
test: {
|
|
100
|
+
applicability: { allFiles: ['package.json'] },
|
|
101
|
+
fast: false,
|
|
102
|
+
timeout: 30_000,
|
|
103
|
+
variants: [{
|
|
104
|
+
id: 'npm-script', priority: 1,
|
|
105
|
+
requirements: { tool: 'npm', toolRange: '>=1.0.0', runtime: 'node', runtimeRange: '>=20.0.0' },
|
|
106
|
+
certifiedRange: '>=1.0.0',
|
|
107
|
+
command: { executable: 'npm', resolution: 'path', args: ['test', '--', '--silent'], packageManager: 'npm' },
|
|
108
|
+
assets: [], formatter: 'test', probe: { kind: 'package-script-present', script: 'test' },
|
|
109
|
+
}],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
coverage: { schemaVersion: 1, classes: {
|
|
113
|
+
tests: { description: 'Fixture test execution', detectors: [{ sensor: 'test' }], remedy: { summary: 'Run fixture tests', command: 'npm test' } },
|
|
114
|
+
} },
|
|
115
|
+
}, null, 2) + '\n');
|
|
116
|
+
}
|
|
54
117
|
async function until(fn, ms = 60000, label = 'condicion') {
|
|
55
118
|
const t0 = Date.now();
|
|
56
119
|
while (!fn()) {
|
|
@@ -62,19 +125,40 @@ async function until(fn, ms = 60000, label = 'condicion') {
|
|
|
62
125
|
describe('E2E real: crash/restart del supervisor', () => {
|
|
63
126
|
let repo;
|
|
64
127
|
let stubBin;
|
|
128
|
+
let fixtureAwmHome;
|
|
65
129
|
let env;
|
|
66
130
|
const children = [];
|
|
67
131
|
beforeAll(() => {
|
|
68
132
|
if (!fs_1.default.existsSync(CLI))
|
|
69
133
|
throw new Error('dist ausente: corre `cd cli && npm run build` antes de esta suite (Task 20 Step 1)');
|
|
70
134
|
});
|
|
71
|
-
beforeEach(() => {
|
|
135
|
+
beforeEach(async () => {
|
|
72
136
|
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-'));
|
|
73
137
|
git(repo, 'init', '-q', '-b', 'main');
|
|
74
|
-
|
|
138
|
+
const npmVersion = (0, child_process_1.execFileSync)('npm', ['--version'], { encoding: 'utf8' }).trim();
|
|
139
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ name: 'fixture', packageManager: `npm@${npmVersion}`, scripts: { test: 'node -e "process.exit(0)" --' } }));
|
|
140
|
+
// The package manager is deliberately selected in package metadata.
|
|
141
|
+
// Discovery must certify the same bounded PATH npm that the sensor runs,
|
|
142
|
+
// rather than accept a shadow package under this fixture's node_modules.
|
|
143
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, 'plans'), { recursive: true });
|
|
144
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'plans', 'fixture.md'), fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8'));
|
|
145
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'source.md'), '## Canonical source\nfixture source\n');
|
|
146
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
147
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
|
|
148
|
+
schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' },
|
|
149
|
+
sensors: { test: {
|
|
150
|
+
enabled: true, variantId: 'npm-script',
|
|
151
|
+
command: { executable: 'npm', resolution: 'path', args: ['test', '--', '--silent'], packageManager: 'npm' },
|
|
152
|
+
initializedCompatibility: { state: 'certified', reason: 'fixture', variantId: 'npm-script', toolVersion: npmVersion, runtimeVersion: process.versions.node, certifiedRange: '>=8.0.0', evidence: [] },
|
|
153
|
+
} },
|
|
154
|
+
}));
|
|
75
155
|
git(repo, 'add', '.');
|
|
76
156
|
git(repo, 'commit', '-qm', 'c');
|
|
77
157
|
stubBin = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-bin-'));
|
|
158
|
+
fixtureAwmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-home-'));
|
|
159
|
+
writeFixtureBaselineRegistry(fixtureAwmHome);
|
|
160
|
+
fs_1.default.writeFileSync(path_1.default.join(fixtureAwmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'fixture://baseline' }]));
|
|
161
|
+
fs_1.default.writeFileSync(path_1.default.join(fixtureAwmHome, 'preferences.json'), JSON.stringify({ defaultAgent: 'codex', enabledAgents: ['codex', 'claude-code'], installMethod: 'symlink', defaultScope: 'local' }));
|
|
78
162
|
// A bare extensionless #!/bin/sh script only runs via POSIX kernel shebang
|
|
79
163
|
// interpretation -- Windows CreateProcess has none, so spawnStructured
|
|
80
164
|
// (shell:false, matching production) would silently fail to launch this stub
|
|
@@ -82,13 +166,41 @@ describe('E2E real: crash/restart del supervisor', () => {
|
|
|
82
166
|
// on both platforms (Node's spawn on win32 resolves via PATHEXT and transparently
|
|
83
167
|
// re-invokes a found .cmd through cmd.exe) without touching production code.
|
|
84
168
|
for (const name of ['codex', 'claude']) {
|
|
85
|
-
fs_1.default.writeFileSync(path_1.default.join(stubBin, name),
|
|
169
|
+
fs_1.default.writeFileSync(path_1.default.join(stubBin, name), `#!/bin/sh
|
|
170
|
+
# The supervisor invokes Codex as \`codex exec <prompt>\`; retain a distinct
|
|
171
|
+
# path for that real adapter contract instead of accidentally treating it as
|
|
172
|
+
# a bare binary invocation. Both controller forms deliberately stay alive:
|
|
173
|
+
# the test's requested \`node -e\` job is the process that must complete and
|
|
174
|
+
# leave its sidecar while the supervisor is killed.
|
|
175
|
+
if [ "$1" = "exec" ]; then shift; fi
|
|
176
|
+
while true; do sleep 1; done
|
|
177
|
+
`, { mode: 0o755 });
|
|
86
178
|
fs_1.default.writeFileSync(path_1.default.join(stubBin, `${name}.cmd`), '@echo off\r\n:loop\r\ntimeout /t 1 /nobreak >nul\r\ngoto loop\r\n');
|
|
87
179
|
}
|
|
88
|
-
env = { ...process.env, PATH: `${stubBin}${path_1.default.delimiter}${process.env.PATH}` };
|
|
89
|
-
(0, child_process_1.execFileSync)(process.execPath, [CLI, 'watch', '--init'], { cwd: repo, env });
|
|
180
|
+
env = { ...process.env, AWM_HOME: fixtureAwmHome, PATH: `${stubBin}${path_1.default.delimiter}${process.env.PATH}` };
|
|
181
|
+
(0, child_process_1.execFileSync)(process.execPath, [CLI, 'watch', '--init', '--plan', 'plans/fixture.md'], { cwd: repo, env });
|
|
182
|
+
const priorAwmHome = process.env.AWM_HOME;
|
|
183
|
+
process.env.AWM_HOME = fixtureAwmHome;
|
|
184
|
+
const sensorReport = await (0, run_1.runSensors)({ cwd: repo, all: true, readOnly: true });
|
|
185
|
+
if (priorAwmHome === undefined)
|
|
186
|
+
delete process.env.AWM_HOME;
|
|
187
|
+
else
|
|
188
|
+
process.env.AWM_HOME = priorAwmHome;
|
|
189
|
+
if (sensorReport.overall !== 'pass') {
|
|
190
|
+
process.env.AWM_HOME = fixtureAwmHome;
|
|
191
|
+
const live = await (0, live_1.resolveLiveCompatibility)(repo, 'js-ts');
|
|
192
|
+
if (priorAwmHome === undefined)
|
|
193
|
+
delete process.env.AWM_HOME;
|
|
194
|
+
else
|
|
195
|
+
process.env.AWM_HOME = priorAwmHome;
|
|
196
|
+
throw new Error(`fixture runSensors failed: ${JSON.stringify({ sensorReport, live })}`);
|
|
197
|
+
}
|
|
198
|
+
expect(sensorReport).toMatchObject({ source: { kind: 'logical', registry: 'baseline' } });
|
|
199
|
+
const admission = (0, child_process_1.spawnSync)(process.execPath, [CLI, 'plan', 'admit', 'plans/fixture.md', '--provider', 'codex', '--cwd', '.', '--execution-mode', 'desatendido', '--require-current', '--verify-sensors', '--json'], { cwd: repo, env, encoding: 'utf8' });
|
|
200
|
+
if (admission.status !== 0)
|
|
201
|
+
throw new Error(`fixture admission failed: ${admission.stdout}${admission.stderr}`);
|
|
90
202
|
});
|
|
91
|
-
afterEach(() => {
|
|
203
|
+
afterEach(async () => {
|
|
92
204
|
// higiene: terminar TODO grupo que hayamos originado (supervisores,
|
|
93
205
|
// stubs de controlador, wrappers) — cero huerfanos entre tests
|
|
94
206
|
const s = readState(repo);
|
|
@@ -107,14 +219,17 @@ describe('E2E real: crash/restart del supervisor', () => {
|
|
|
107
219
|
groups.add(j.processRef.processGroup);
|
|
108
220
|
}
|
|
109
221
|
}
|
|
110
|
-
|
|
111
|
-
killGroup(pgid);
|
|
222
|
+
await terminateFixtureGroups(groups);
|
|
112
223
|
children.length = 0;
|
|
113
|
-
|
|
114
|
-
|
|
224
|
+
await removeFixture(repo);
|
|
225
|
+
await removeFixture(stubBin);
|
|
226
|
+
await removeFixture(fixtureAwmHome);
|
|
115
227
|
});
|
|
116
228
|
function startSupervisor(provider) {
|
|
117
|
-
|
|
229
|
+
// Supervisor stdout is test harness output, not project evidence. An
|
|
230
|
+
// in-repo log would make the real currentness/admission gate reject
|
|
231
|
+
// the next tick before it can collect an exited job sidecar.
|
|
232
|
+
const out = fs_1.default.openSync(path_1.default.join(stubBin, `sup-${children.length}.log`), 'a');
|
|
118
233
|
const child = (0, child_process_1.spawn)(process.execPath, [CLI, 'watch', '--provider', provider], {
|
|
119
234
|
cwd: repo, env, detached: true, stdio: ['ignore', out, out],
|
|
120
235
|
});
|
|
@@ -12,6 +12,7 @@ const process_1 = require("../../../src/core/journal/process");
|
|
|
12
12
|
const store_1 = require("../../../src/core/journal/store");
|
|
13
13
|
const paths_1 = require("../../../src/core/journal/paths");
|
|
14
14
|
const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
|
|
15
|
+
const recovery = { schema: 'controller-recovery/v1', kind: 'resume-cycle' };
|
|
15
16
|
describe('generaciones', () => {
|
|
16
17
|
let repo;
|
|
17
18
|
beforeEach(() => { repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gen-')); (0, store_1.initJournal)(repo, 'rama'); });
|
|
@@ -56,7 +57,7 @@ describe('generaciones', () => {
|
|
|
56
57
|
const begun = (0, generations_1.beginGeneration)(repo, 'rama');
|
|
57
58
|
let captured;
|
|
58
59
|
const wrapperRef = { pid: 42, startTime: 't', spawnNonce: 'wrapper-nonce', argvDigest: 'a', processGroup: 42, psArgsDigest: 'p' };
|
|
59
|
-
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex',
|
|
60
|
+
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, (job, nonce) => {
|
|
60
61
|
const duringSpawn = (0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'rama').state);
|
|
61
62
|
expect(duringSpawn.controllerJobId).toBe(job.id); // persistido ANTES del spawn
|
|
62
63
|
expect(duringSpawn.spawnNonce).toBe(nonce);
|
|
@@ -67,9 +68,42 @@ describe('generaciones', () => {
|
|
|
67
68
|
expect(captured.argv.join(' ')).toContain(`--generation ${begun.token}`);
|
|
68
69
|
expect((0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'rama').state).wrapperRef).toEqual(wrapperRef);
|
|
69
70
|
});
|
|
71
|
+
test('an arbitrary prompt is rejected before durable launch intent or dispatch', () => {
|
|
72
|
+
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
73
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
74
|
+
let dispatched = false;
|
|
75
|
+
expect(() => (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', 'private source body and password=sentinel', () => { dispatched = true; })).toThrow(/recovery|intent|acción/i);
|
|
76
|
+
expect(dispatched).toBe(false);
|
|
77
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
78
|
+
expect(() => (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', { ...recovery, prompt: 'private body' }, () => { })).toThrow(/recovery/);
|
|
79
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
80
|
+
});
|
|
81
|
+
test('legacy prompt custody remains readable but cannot launch or replay', () => {
|
|
82
|
+
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
83
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
84
|
+
state.generations[0].resumePrompt = 'historical legacy body';
|
|
85
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
86
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
87
|
+
expect(() => (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, () => { })).toThrow(/legacy/);
|
|
88
|
+
expect(() => (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000)).toThrow(/legacy/);
|
|
89
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
90
|
+
});
|
|
91
|
+
test('closed recovery intent launches deterministically and persists no prompt body', () => {
|
|
92
|
+
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
93
|
+
const action = { schema: 'controller-recovery/v1', kind: 'resume-cycle' };
|
|
94
|
+
const attempts = [];
|
|
95
|
+
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', action, job => { attempts.push(job.argv); });
|
|
96
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
97
|
+
expect(state.generations[0]).toMatchObject({ resumeAction: action });
|
|
98
|
+
expect(state.generations[0].resumePrompt).toBeUndefined();
|
|
99
|
+
expect(JSON.stringify(state)).not.toContain('Sos el orquestador');
|
|
100
|
+
(0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', { schema: 'controller-recovery/v1', kind: 'reconcile-active-jobs' }, job => { attempts.push(job.argv); }, 10000);
|
|
101
|
+
expect(attempts).toHaveLength(2);
|
|
102
|
+
expect(attempts[1]).toEqual(attempts[0]);
|
|
103
|
+
});
|
|
70
104
|
test('claim sin identidad/resultado conserva ownership ambiguo; usa la edad del claim, no la de begin', () => {
|
|
71
105
|
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
72
|
-
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex',
|
|
106
|
+
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, () => { });
|
|
73
107
|
let s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
74
108
|
const gen = (0, generations_1.activeGeneration)(s);
|
|
75
109
|
gen.launchedAt = new Date(Date.now() - 3600000).toISOString();
|
|
@@ -77,15 +111,90 @@ describe('generaciones', () => {
|
|
|
77
111
|
fs_1.default.mkdirSync((0, paths_1.logsDir)(repo, 'rama'), { recursive: true });
|
|
78
112
|
fs_1.default.writeFileSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'rama'), gen.controllerJobId, gen.spawnNonce), '{}');
|
|
79
113
|
expect((0, generations_1.controllerGenerationHasUnresolvedClaim)(repo, 'rama', gen)).toBe(true);
|
|
80
|
-
(0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex',
|
|
114
|
+
(0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000);
|
|
81
115
|
expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('IN_PROGRESS'); // claim recien creado: aun dentro de gracia
|
|
82
116
|
const old = new Date(Date.now() - 20000);
|
|
83
117
|
fs_1.default.utimesSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'rama'), gen.controllerJobId, gen.spawnNonce), old, old);
|
|
84
|
-
(0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex',
|
|
118
|
+
(0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000);
|
|
85
119
|
expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('BLOCKED');
|
|
86
120
|
fs_1.default.writeFileSync((0, exec_wrapper_1.resultPath)((0, paths_1.logsDir)(repo, 'rama'), gen.controllerJobId, gen.spawnNonce), JSON.stringify({ exitCode: 1 }));
|
|
87
121
|
expect((0, generations_1.controllerGenerationHasUnresolvedClaim)(repo, 'rama', gen)).toBe(false);
|
|
88
122
|
});
|
|
123
|
+
test('replay preserves exact argv commitment and rejects environment command drift without mutation', () => {
|
|
124
|
+
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
125
|
+
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, () => { });
|
|
126
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
127
|
+
const previous = process.env.AWM_CONTROLLER_ARGV;
|
|
128
|
+
process.env.AWM_CONTROLLER_ARGV = JSON.stringify(['different-controller']);
|
|
129
|
+
try {
|
|
130
|
+
expect(() => (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000)).toThrow(/argv cambio/);
|
|
131
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (previous === undefined)
|
|
135
|
+
delete process.env.AWM_CONTROLLER_ARGV;
|
|
136
|
+
else
|
|
137
|
+
process.env.AWM_CONTROLLER_ARGV = previous;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
test('a real launched controller generation retains metadata but no prompt or source output', async () => {
|
|
141
|
+
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
142
|
+
const body = 'SOURCE_BODY_SENTINEL_private_controller_text';
|
|
143
|
+
const previous = process.env.AWM_CONTROLLER_ARGV;
|
|
144
|
+
process.env.AWM_CONTROLLER_ARGV = JSON.stringify([process.execPath, '-e', `process.stdout.write('${body}')`]);
|
|
145
|
+
let execution;
|
|
146
|
+
try {
|
|
147
|
+
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, (job, nonce, logsRoot, repoRoot) => {
|
|
148
|
+
execution = (0, exec_wrapper_1.runExecWrapper)({ logsRoot, jobId: job.id, nonce, argv: job.argv, cwd: job.cwd, repoRoot });
|
|
149
|
+
});
|
|
150
|
+
await execution;
|
|
151
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
152
|
+
expect(state.generations[0].launchArgvDigest).toMatch(/^[a-f0-9]{16}$/);
|
|
153
|
+
const artifacts = fs_1.default.readdirSync((0, paths_1.logsDir)(repo, 'rama')).map(name => fs_1.default.readFileSync(path_1.default.join((0, paths_1.logsDir)(repo, 'rama'), name), 'utf8')).join('\n');
|
|
154
|
+
expect(artifacts).toContain('argvDigest');
|
|
155
|
+
expect(artifacts).toContain('exitCode');
|
|
156
|
+
expect(artifacts + JSON.stringify(state)).not.toContain(body);
|
|
157
|
+
expect(artifacts + JSON.stringify(state)).not.toContain('Retoma el plan');
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (previous === undefined)
|
|
161
|
+
delete process.env.AWM_CONTROLLER_ARGV;
|
|
162
|
+
else
|
|
163
|
+
process.env.AWM_CONTROLLER_ARGV = previous;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
test('a user verifier named like a controller keeps ordinary bounded output without generation proof', async () => {
|
|
167
|
+
const logsRoot = (0, paths_1.logsDir)(repo, 'rama');
|
|
168
|
+
const argv = [process.execPath, '-e', "process.stdout.write('ordinary-verifier-output')"];
|
|
169
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
170
|
+
state.jobs['controller-gen-1'] = { id: 'controller-gen-1', argv, spawnNonce: 'normal-verifier', fingerprint: 'fixture', commandDigest: 'fixture',
|
|
171
|
+
cwd: '.', paths: [], expandedPaths: [], executionState: 'spawn-intent', observationState: 'progressing', phaseTimestamps: {} };
|
|
172
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
173
|
+
await (0, exec_wrapper_1.runExecWrapper)({ logsRoot, repoRoot: repo, jobId: 'controller-gen-1', nonce: 'normal-verifier', cwd: '.',
|
|
174
|
+
argv });
|
|
175
|
+
const artifacts = fs_1.default.readdirSync(logsRoot).map(name => fs_1.default.readFileSync(path_1.default.join(logsRoot, name), 'utf8')).join('\n');
|
|
176
|
+
expect(artifacts).toContain('ordinary-verifier-output');
|
|
177
|
+
});
|
|
178
|
+
test.each(['missing', 'corrupt', 'symlink', 'oversized', 'nonce-drift', 'argv-drift'])('controller %s custody fails closed before claim or launch artifacts', async (fault) => {
|
|
179
|
+
(0, generations_1.beginGeneration)(repo, 'rama');
|
|
180
|
+
let launched;
|
|
181
|
+
(0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, (job, nonce) => { launched = { id: job.id, nonce, argv: job.argv }; });
|
|
182
|
+
const stateFile = path_1.default.resolve((0, paths_1.logsDir)(repo, 'rama'), '..', 'state.json');
|
|
183
|
+
if (fault === 'missing')
|
|
184
|
+
fs_1.default.unlinkSync(stateFile);
|
|
185
|
+
if (fault === 'corrupt')
|
|
186
|
+
fs_1.default.writeFileSync(stateFile, '{bad json');
|
|
187
|
+
if (fault === 'oversized')
|
|
188
|
+
fs_1.default.writeFileSync(stateFile, ' '.repeat(1024 * 1024 + 1));
|
|
189
|
+
if (fault === 'symlink') {
|
|
190
|
+
fs_1.default.renameSync(stateFile, `${stateFile}.original`);
|
|
191
|
+
fs_1.default.symlinkSync(`${stateFile}.original`, stateFile, 'file');
|
|
192
|
+
}
|
|
193
|
+
const argv = fault === 'argv-drift' ? [process.execPath, '-e', "process.stdout.write('source body')"] : launched.argv;
|
|
194
|
+
const nonce = fault === 'nonce-drift' ? 'wrong-nonce' : launched.nonce;
|
|
195
|
+
await expect((0, exec_wrapper_1.runExecWrapper)({ logsRoot: (0, paths_1.logsDir)(repo, 'rama'), repoRoot: repo, jobId: launched.id, nonce, argv, cwd: '.' })).rejects.toThrow(/controller|custody|custodia|bounded|regular/i);
|
|
196
|
+
expect(fs_1.default.readdirSync((0, paths_1.logsDir)(repo, 'rama'))).toEqual([]);
|
|
197
|
+
});
|
|
89
198
|
test('resolveGeneration: muerte probada => proven-dead; vivo+indeterminate => custodia con estado BLOCKED (R4.2b)', async () => {
|
|
90
199
|
const adapter = (0, adapter_1.adapterFor)('codex');
|
|
91
200
|
(0, generations_1.beginGeneration)(repo, 'rama');
|