agentic-workflow-manager 9.12.0 → 9.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/job/gate.js +6 -1
- package/dist/src/commands/job/index.js +22 -1
- package/dist/src/commands/watch/admission-recovery.js +161 -0
- package/dist/src/commands/watch/apply.js +7 -1
- package/dist/src/commands/watch/archive-unused.js +2 -0
- package/dist/src/commands/watch/index.js +55 -0
- package/dist/src/commands/watch/init.js +2 -1
- package/dist/src/commands/watch/request-recovery.js +153 -0
- package/dist/src/commands/watch/supervisor.js +105 -46
- package/dist/src/core/journal/requests.js +1 -0
- package/dist/src/core/journal/types.js +67 -2
- package/dist/tests/commands/watch/admission-recovery.test.js +278 -0
- package/dist/tests/commands/watch/request-recovery.test.js +253 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +88 -0
- package/package.json +1 -1
- 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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const os_1 = __importDefault(require("os"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const commander_1 = require("commander");
|
|
44
|
+
const init_1 = require("../../../src/commands/watch/init");
|
|
45
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
46
|
+
const validate_1 = require("../../../src/core/plan/validate");
|
|
47
|
+
const process_1 = require("../../../src/core/journal/process");
|
|
48
|
+
const eventStore = __importStar(require("../../../src/core/model-policy/event-store"));
|
|
49
|
+
const localScope = __importStar(require("../../../src/core/model-policy/local-event-scope"));
|
|
50
|
+
const supervisor_1 = require("../../../src/commands/watch/supervisor");
|
|
51
|
+
const admission_recovery_1 = require("../../../src/commands/watch/admission-recovery");
|
|
52
|
+
const watch_1 = require("../../../src/commands/watch");
|
|
53
|
+
const supervisorModule = __importStar(require("../../../src/commands/watch/supervisor"));
|
|
54
|
+
const DIGEST = 'a'.repeat(64);
|
|
55
|
+
const identity = { kind: 'native', version: '1.0.0', accountScopeDigest: DIGEST };
|
|
56
|
+
const input = { provider: 'codex', routingIdentity: identity, controllerAutonomy: 'approval-free',
|
|
57
|
+
generationToken: 'legacy-1', reason: 'Operador verificó currentness y recibos vigentes' };
|
|
58
|
+
const admitted = async () => ({ state: 'admitted', planState: 'valid', executionMode: 'desatendido', journal: 'current', currentness: 'current', sensors: 'pass', diagnostics: [] });
|
|
59
|
+
const blocked = (code, currentness, sensors) => async () => ({ state: 'blocked', planState: 'valid', executionMode: 'desatendido', journal: 'not-required', currentness, sensors, diagnostics: [{ code, message: 'still blocked' }] });
|
|
60
|
+
describe('issue #196: admission custody recovery', () => {
|
|
61
|
+
let repo;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admission-recovery-'));
|
|
64
|
+
(0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'main'], { cwd: repo });
|
|
65
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '--allow-empty', '-qm', 'seed'], { cwd: repo });
|
|
66
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, 'plans'), { recursive: true });
|
|
67
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'plans', 'cycle.md'), fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8'));
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'source.md'), '## Canonical source\nfixture source\n');
|
|
69
|
+
(0, init_1.initWatch)(repo, 'main');
|
|
70
|
+
const plan = (0, validate_1.validatePlanFile)('plans/cycle.md', repo);
|
|
71
|
+
if (plan.state !== 'valid')
|
|
72
|
+
throw new Error('invalid fixture');
|
|
73
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
74
|
+
state.schema = 2;
|
|
75
|
+
state.planBinding = { path: 'plans/cycle.md', digest: plan.planDigest, schema: plan.schema, executionMode: 'desatendido', boundAt: new Date().toISOString() };
|
|
76
|
+
state.generations.push({ n: 1, token: input.generationToken, state: 'terminated', launchedAt: new Date().toISOString(), provider: 'codex' });
|
|
77
|
+
state.cycle.status = 'BLOCKED';
|
|
78
|
+
state.cycle.blockedReason = 'admisión compacta desatendida bloqueada antes de dispatch: ADMISSION_CURRENTNESS_BLOCKED: Consumed contract currentness is unverifiable: registry:baseline.';
|
|
79
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
80
|
+
jest.spyOn(eventStore, 'readStoredEventReceipt').mockImplementation(runtime => ({ state: 'present', receipt: {
|
|
81
|
+
schema: 'routing-capabilities/v2', runtime, binaryDigest: DIGEST, configDigest: DIGEST,
|
|
82
|
+
recordedAt: new Date().toISOString(), claims: [],
|
|
83
|
+
} }));
|
|
84
|
+
jest.spyOn(localScope, 'queryLocalEventScope').mockImplementation(async (_cwd, target, kind) => ({ state: 'current', scope: {
|
|
85
|
+
runtime: { target, kind, version: identity.version, accountScopeDigest: identity.accountScopeDigest },
|
|
86
|
+
binaryDigest: DIGEST, configDigest: DIGEST,
|
|
87
|
+
} }));
|
|
88
|
+
});
|
|
89
|
+
afterEach(() => { jest.restoreAllMocks(); fs_1.default.rmSync(repo, { recursive: true, force: true }); });
|
|
90
|
+
test('legacy currentness custody resumes once with original reason, generation and nextAction retained', async () => {
|
|
91
|
+
const original = (0, store_1.readJournal)(repo, 'main').state;
|
|
92
|
+
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).toBe('recovered');
|
|
93
|
+
const result = (0, store_1.readJournal)(repo, 'main').state;
|
|
94
|
+
expect(result.cycle.status).toBe('IN_PROGRESS');
|
|
95
|
+
expect(result.cycle.nextAction).toEqual(original.cycle.nextAction);
|
|
96
|
+
expect(result.generations).toEqual(original.generations);
|
|
97
|
+
expect(result.tasks).toEqual(original.tasks);
|
|
98
|
+
expect(result.dispatches).toEqual(original.dispatches);
|
|
99
|
+
expect(result.jobs).toEqual(original.jobs);
|
|
100
|
+
expect(result).toHaveProperty('admissionRecoveries.0.blockedReason', original.cycle.blockedReason);
|
|
101
|
+
expect(result).toHaveProperty('admissionRecoveries.0.generationToken', input.generationToken);
|
|
102
|
+
const revision = result.revision;
|
|
103
|
+
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).toBe('already-recovered');
|
|
104
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revision);
|
|
105
|
+
});
|
|
106
|
+
test('persistent unverifiable currentness → custody without generation → explicit recovery → one launch', async () => {
|
|
107
|
+
const initial = (0, store_1.readJournal)(repo, 'main').state;
|
|
108
|
+
initial.generations = [];
|
|
109
|
+
initial.cycle.status = 'IN_PROGRESS';
|
|
110
|
+
initial.cycle.blockedReason = undefined;
|
|
111
|
+
(0, store_1.writeJournal)(repo, 'main', initial);
|
|
112
|
+
let now = Date.now();
|
|
113
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
114
|
+
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, routingIdentity: identity, controllerAutonomy: 'approval-free' };
|
|
115
|
+
const transient = blocked('ADMISSION_CURRENTNESS_BLOCKED', 'unverifiable', 'not-required');
|
|
116
|
+
let spawns = 0;
|
|
117
|
+
for (let i = 0; i < supervisor_1.MAX_SENSOR_DEFERRALS + 1; i++) {
|
|
118
|
+
const outcome = await new supervisor_1.Supervisor(repo, 'main', cfg, () => { spawns++; }, undefined, transient, true).tick();
|
|
119
|
+
expect(outcome).toBe(i < supervisor_1.MAX_SENSOR_DEFERRALS ? 'continue' : 'custody');
|
|
120
|
+
now += 5000;
|
|
121
|
+
}
|
|
122
|
+
const held = (0, store_1.readJournal)(repo, 'main').state;
|
|
123
|
+
expect(held.generations).toHaveLength(0);
|
|
124
|
+
expect(held.tasks).toHaveLength(0);
|
|
125
|
+
expect(spawns).toBe(0);
|
|
126
|
+
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', { ...input, generationToken: undefined }, admitted)).toBe('recovered');
|
|
127
|
+
expect(await new supervisor_1.Supervisor(repo, 'main', cfg, () => { spawns++; }, undefined, admitted, true).tick()).toBe('continue');
|
|
128
|
+
const resumed = (0, store_1.readJournal)(repo, 'main').state;
|
|
129
|
+
expect(resumed.generations).toHaveLength(1);
|
|
130
|
+
expect(resumed.cycle.nextAction).toEqual(held.cycle.nextAction);
|
|
131
|
+
expect(resumed.jobs).toEqual({});
|
|
132
|
+
expect(resumed.dispatches).toEqual([]);
|
|
133
|
+
expect(spawns).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
test('persistent currentness and sensor failures leave custody untouched', async () => {
|
|
136
|
+
for (const [code, currentness, sensors] of [
|
|
137
|
+
['ADMISSION_CURRENTNESS_BLOCKED', 'unverifiable', 'not-required'],
|
|
138
|
+
['ADMISSION_SENSORS_BLOCKED', 'current', 'not-certified'],
|
|
139
|
+
]) {
|
|
140
|
+
const revision = (0, store_1.readJournal)(repo, 'main').state.revision;
|
|
141
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, blocked(code, currentness, sensors))).rejects.toThrow(/admisión|currentness|sensores/i);
|
|
142
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revision);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
test('legacy recovery refuses a missing native receipt even when v1 plan admission says admitted', async () => {
|
|
146
|
+
jest.spyOn(eventStore, 'readStoredEventReceipt').mockReturnValue({ state: 'absent' });
|
|
147
|
+
const revision = (0, store_1.readJournal)(repo, 'main').state.revision;
|
|
148
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).rejects.toThrow(/recibo|receipt/i);
|
|
149
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revision);
|
|
150
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
151
|
+
});
|
|
152
|
+
test('sensor custody can recover only after sensors pass', async () => {
|
|
153
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
154
|
+
state.cycle.blockedReason = 'admisión sensors no verificable tras 3 reintentos: ADMISSION_SENSORS_BLOCKED: not-certified';
|
|
155
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
156
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, blocked('ADMISSION_SENSORS_BLOCKED', 'current', 'not-certified'))).rejects.toThrow();
|
|
157
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
158
|
+
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).toBe('recovered');
|
|
159
|
+
});
|
|
160
|
+
test('changed binding, active request problem, pending job and identity mismatch all fail closed', async () => {
|
|
161
|
+
const check = async (mutate, pattern) => {
|
|
162
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
163
|
+
mutate(state);
|
|
164
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
165
|
+
const revision = (0, store_1.readJournal)(repo, 'main').state.revision;
|
|
166
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).rejects.toThrow(pattern);
|
|
167
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revision);
|
|
168
|
+
};
|
|
169
|
+
await check(state => { state.planBinding.digest = DIGEST; }, /binding|plan/i);
|
|
170
|
+
const validPlan = (0, validate_1.validatePlanFile)('plans/cycle.md', repo);
|
|
171
|
+
if (validPlan.state !== 'valid')
|
|
172
|
+
throw new Error('fixture changed');
|
|
173
|
+
const s1 = (0, store_1.readJournal)(repo, 'main').state;
|
|
174
|
+
s1.planBinding.digest = validPlan.planDigest;
|
|
175
|
+
(0, store_1.writeJournal)(repo, 'main', s1);
|
|
176
|
+
await check(state => { state.requestProblems.push({ file: 'rejected', kind: 'rejected', detail: 'x', at: new Date().toISOString() }); }, /request/i);
|
|
177
|
+
const s2 = (0, store_1.readJournal)(repo, 'main').state;
|
|
178
|
+
s2.requestProblems = [];
|
|
179
|
+
(0, store_1.writeJournal)(repo, 'main', s2);
|
|
180
|
+
await check(state => { state.jobs['J1'] = { id: 'J1', fingerprint: 'x', commandDigest: 'x', argv: ['true'], cwd: '.', paths: [], expandedPaths: [], executionState: 'received', observationState: 'progressing', phaseTimestamps: {} }; }, /job/i);
|
|
181
|
+
const sJob = (0, store_1.readJournal)(repo, 'main').state;
|
|
182
|
+
sJob.jobs = {};
|
|
183
|
+
(0, store_1.writeJournal)(repo, 'main', sJob);
|
|
184
|
+
await check(state => { state.generations[0].processRef = (0, process_1.captureSelfRef)('recovery-test'); }, /controller/i);
|
|
185
|
+
const sController = (0, store_1.readJournal)(repo, 'main').state;
|
|
186
|
+
sController.generations[0].processRef = undefined;
|
|
187
|
+
(0, store_1.writeJournal)(repo, 'main', sController);
|
|
188
|
+
await check(state => { state.generations[0].provider = 'claude-code'; }, /provider|identidad/i);
|
|
189
|
+
const s3 = (0, store_1.readJournal)(repo, 'main').state;
|
|
190
|
+
s3.generations[0].provider = 'codex';
|
|
191
|
+
(0, store_1.writeJournal)(repo, 'main', s3);
|
|
192
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', { ...input, routingIdentity: { ...identity, accountScopeDigest: 'bad' } }, admitted)).rejects.toThrow(/runtime|digest/i);
|
|
193
|
+
});
|
|
194
|
+
test('an independent missing required verifier keeps the cycle BLOCKED', async () => {
|
|
195
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
196
|
+
state.requiredVerifiers = ['test'];
|
|
197
|
+
state.tasks.push({ id: 'S1', title: 'complete claim', status: 'done', attempts: 1, verificationPlan: [], reviewObligations: [] });
|
|
198
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
199
|
+
const revision = (0, store_1.readJournal)(repo, 'main').state.revision;
|
|
200
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).rejects.toThrow(/verificador|required/i);
|
|
201
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revision);
|
|
202
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
203
|
+
});
|
|
204
|
+
test('a long-lived cycle can recover again after 128 prior audited decisions', async () => {
|
|
205
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
206
|
+
state.admissionRecoveries = Array.from({ length: 128 }, (_, index) => ({
|
|
207
|
+
at: new Date().toISOString(), reason: `prior-${index}`, blockedReason: state.cycle.blockedReason,
|
|
208
|
+
generationToken: input.generationToken, context: { provider: 'codex', runtime: identity, controllerAutonomy: 'approval-free' },
|
|
209
|
+
planDigest: state.planBinding.digest, assertion: 'legacy-operator-asserted',
|
|
210
|
+
}));
|
|
211
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
212
|
+
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).toBe('recovered');
|
|
213
|
+
expect((0, store_1.readJournal)(repo, 'main').state.admissionRecoveries).toHaveLength(129);
|
|
214
|
+
});
|
|
215
|
+
test('after audited recovery supervisor still rechecks admission before any dispatch', async () => {
|
|
216
|
+
await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted);
|
|
217
|
+
let spawns = 0;
|
|
218
|
+
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', routingIdentity: identity, controllerAutonomy: 'approval-free' };
|
|
219
|
+
const sup = new supervisor_1.Supervisor(repo, 'main', cfg, () => { spawns++; }, undefined, blocked('ADMISSION_CURRENTNESS_BLOCKED', 'unverifiable', 'not-required'));
|
|
220
|
+
expect(await sup.tick()).toBe('continue');
|
|
221
|
+
expect(spawns).toBe(0);
|
|
222
|
+
expect((0, store_1.readJournal)(repo, 'main').state.tasks).toHaveLength(0);
|
|
223
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.nextAction?.actionId).toBe('bootstrap-cycle');
|
|
224
|
+
});
|
|
225
|
+
test('public watch recover-admission CLI records one offline transition', async () => {
|
|
226
|
+
const before = (0, store_1.readJournal)(repo, 'main').state;
|
|
227
|
+
before.tasks.push({ id: 'S1', title: 'pending work', status: 'pending', attempts: 0, verificationPlan: [], reviewObligations: [] });
|
|
228
|
+
(0, store_1.writeJournal)(repo, 'main', before);
|
|
229
|
+
jest.spyOn(supervisorModule, 'admissionForConfig').mockReturnValue(admitted);
|
|
230
|
+
const cwd = jest.spyOn(process, 'cwd').mockReturnValue(repo);
|
|
231
|
+
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
232
|
+
const program = new commander_1.Command();
|
|
233
|
+
program.exitOverride();
|
|
234
|
+
(0, watch_1.registerWatchCommand)(program);
|
|
235
|
+
try {
|
|
236
|
+
await program.parseAsync(['node', 'awm', 'watch', 'recover-admission',
|
|
237
|
+
'--provider', 'codex', '--runtime-kind', identity.kind, '--runtime-version', identity.version,
|
|
238
|
+
'--account-scope-digest', identity.accountScopeDigest, '--controller-autonomy', 'approval-free',
|
|
239
|
+
'--generation', input.generationToken, '--reason', input.reason]);
|
|
240
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('IN_PROGRESS');
|
|
241
|
+
expect(stdout.mock.calls.map(call => String(call[0])).join('')).toContain('"recovered":true');
|
|
242
|
+
let spawned = 0;
|
|
243
|
+
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', routingIdentity: identity, controllerAutonomy: 'approval-free' };
|
|
244
|
+
const outcome = await new supervisor_1.Supervisor(repo, 'main', cfg, () => { spawned++; }, undefined, admitted, true).tick();
|
|
245
|
+
expect({ outcome, blockedReason: (0, store_1.readJournal)(repo, 'main').state.cycle.blockedReason }).toEqual({ outcome: 'continue', blockedReason: undefined });
|
|
246
|
+
const resumed = (0, store_1.readJournal)(repo, 'main').state;
|
|
247
|
+
expect(resumed.tasks.map(task => task.id)).toEqual(['S1']);
|
|
248
|
+
expect(resumed.jobs).toEqual({});
|
|
249
|
+
expect(resumed.dispatches).toEqual([]);
|
|
250
|
+
expect(resumed.cycle.nextAction?.actionId).toBe('bootstrap-cycle');
|
|
251
|
+
expect(resumed.generations).toHaveLength(2);
|
|
252
|
+
expect(spawned).toBeGreaterThan(0);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
cwd.mockRestore();
|
|
256
|
+
stdout.mockRestore();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
test('CLI requires provider to be asserted, not inherited from the watch default', async () => {
|
|
260
|
+
const cwd = jest.spyOn(process, 'cwd').mockReturnValue(repo);
|
|
261
|
+
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
262
|
+
const program = new commander_1.Command();
|
|
263
|
+
(0, watch_1.registerWatchCommand)(program);
|
|
264
|
+
try {
|
|
265
|
+
await program.parseAsync(['node', 'awm', 'watch', 'recover-admission',
|
|
266
|
+
'--runtime-kind', identity.kind, '--runtime-version', identity.version,
|
|
267
|
+
'--account-scope-digest', identity.accountScopeDigest, '--controller-autonomy', 'approval-free',
|
|
268
|
+
'--generation', input.generationToken, '--reason', input.reason]);
|
|
269
|
+
expect(stderr.mock.calls.map(call => String(call[0])).join('')).toMatch(/--provider/);
|
|
270
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
cwd.mockRestore();
|
|
274
|
+
stderr.mockRestore();
|
|
275
|
+
process.exitCode = 0;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
11
|
+
const requests_1 = require("../../../src/core/journal/requests");
|
|
12
|
+
const apply_1 = require("../../../src/commands/watch/apply");
|
|
13
|
+
const gate_1 = require("../../../src/commands/job/gate");
|
|
14
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
15
|
+
const lock_1 = require("../../../src/commands/watch/lock");
|
|
16
|
+
const request_recovery_1 = require("../../../src/commands/watch/request-recovery");
|
|
17
|
+
const supervisor_1 = require("../../../src/commands/watch/supervisor");
|
|
18
|
+
const archive_unused_1 = require("../../../src/commands/watch/archive-unused");
|
|
19
|
+
const generations_1 = require("../../../src/commands/watch/generations");
|
|
20
|
+
describe('issue #194: recovery of a rejected task request', () => {
|
|
21
|
+
let repo;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-recover-request-'));
|
|
24
|
+
(0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'rama'], { cwd: repo });
|
|
25
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '--allow-empty', '-qm', 'seed'], { cwd: repo });
|
|
26
|
+
(0, store_1.initJournal)(repo, 'rama');
|
|
27
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
28
|
+
state.schema = 2;
|
|
29
|
+
state.planBinding = {
|
|
30
|
+
path: 'docs/plans/cycle.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1',
|
|
31
|
+
executionMode: 'desatendido', boundAt: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
state.requiredVerifiers = ['test', 'sensors'];
|
|
34
|
+
state.generations.push({ n: 1, token: 'g1', state: 'active', launchedAt: new Date().toISOString() });
|
|
35
|
+
state.cycle.status = 'BLOCKED';
|
|
36
|
+
state.cycle.blockedReason = 'recovery no autorizado: hay conflictos durables de requests';
|
|
37
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => fs_1.default.rmSync(repo, { recursive: true, force: true }));
|
|
40
|
+
function rejectedTask() {
|
|
41
|
+
const rejected = (0, requests_1.emitRequest)(repo, 'rama', {
|
|
42
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'bad-task',
|
|
43
|
+
payload: { entity: 'task', taskId: 'S1', title: 'slice', verificationPlan: [{ id: 'test-s1', kind: 'test' }], reviewObligations: [] },
|
|
44
|
+
});
|
|
45
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').rejectedInvalid).toBe(1);
|
|
46
|
+
expect(fs_1.default.existsSync(`${rejected.file}.rejected`)).toBe(true);
|
|
47
|
+
expect(fs_1.default.readFileSync((0, paths_1.eventsPath)(repo, 'rama'), 'utf8')).toMatch(/request-rejected-invalid/);
|
|
48
|
+
return rejected;
|
|
49
|
+
}
|
|
50
|
+
function correctedTask(taskId = 'S1', kinds = ['test', 'sensors']) {
|
|
51
|
+
return (0, requests_1.emitRequest)(repo, 'rama', {
|
|
52
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: `corrected-${taskId}-${kinds.join('-')}`,
|
|
53
|
+
payload: { entity: 'task', taskId, title: 'slice', verificationPlan: kinds.map(kind => ({ id: `${kind}-${taskId}`, kind })), reviewObligations: [] },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
test('rejection → corrected request → audited recovery → next action remains gated by real work', () => {
|
|
57
|
+
const rejected = rejectedTask();
|
|
58
|
+
const replacement = correctedTask();
|
|
59
|
+
expect((0, gate_1.computeGate)((0, store_1.readJournal)(repo, 'rama').state, false, () => null).reasons.some(reason => reason.category === 'request-problem')).toBe(true);
|
|
60
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', {
|
|
61
|
+
rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
62
|
+
generationToken: 'g1', reason: 'corregido plan de verificación', resume: true,
|
|
63
|
+
});
|
|
64
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
65
|
+
expect(state.tasks.map(task => task.id)).toEqual(['S1']);
|
|
66
|
+
expect(state.appliedRequests[replacement.requestId]).toMatchObject({ outcome: 'applied', resultRef: 'S1' });
|
|
67
|
+
expect(state.requestProblems[0]).toMatchObject({ kind: 'rejected', resolution: { replacementRequestId: replacement.requestId, generationToken: 'g1', reason: 'corregido plan de verificación' } });
|
|
68
|
+
expect(state.cycle.status).toBe('IN_PROGRESS');
|
|
69
|
+
expect(state.custodyDecisions).toEqual([expect.objectContaining({ decision: 'resume', generationToken: 'g1' })]);
|
|
70
|
+
expect(fs_1.default.existsSync(`${rejected.file}.rejected`)).toBe(true);
|
|
71
|
+
expect(fs_1.default.existsSync(replacement.file)).toBe(false);
|
|
72
|
+
expect(fs_1.default.readFileSync((0, paths_1.eventsPath)(repo, 'rama'), 'utf8')).toMatch(/request-recovered/);
|
|
73
|
+
const gate = (0, gate_1.computeGate)(state, false, () => null);
|
|
74
|
+
expect(gate.reasons.some(reason => reason.category === 'request-problem')).toBe(false);
|
|
75
|
+
expect(gate.pass).toBe(false);
|
|
76
|
+
expect(gate.reasons.some(reason => reason.category === 'pending-task')).toBe(true);
|
|
77
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', {
|
|
78
|
+
rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
79
|
+
generationToken: 'g1', reason: 'corregido plan de verificación', resume: true,
|
|
80
|
+
});
|
|
81
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(1);
|
|
82
|
+
// Crash after state publication but before queue cleanup: supervisor
|
|
83
|
+
// replay consumes the same request ID without registering S1 twice.
|
|
84
|
+
fs_1.default.writeFileSync(replacement.file, JSON.stringify({ requestId: replacement.requestId,
|
|
85
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'corrected-S1-test-sensors',
|
|
86
|
+
payload: { entity: 'task', taskId: 'S1', title: 'slice', verificationPlan: [
|
|
87
|
+
{ id: 'test-S1', kind: 'test' }, { id: 'sensors-S1', kind: 'sensors' }
|
|
88
|
+
], reviewObligations: [] } }));
|
|
89
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(0);
|
|
90
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(1);
|
|
91
|
+
});
|
|
92
|
+
test('wrong task and still-invalid correction leave rejection active without partial writes', () => {
|
|
93
|
+
const rejected = rejectedTask();
|
|
94
|
+
const wrong = correctedTask('S2');
|
|
95
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId, replacementRequestId: wrong.requestId, generationToken: 'g1', reason: 'wrong task', resume: true })).toThrow(/mismo taskId/);
|
|
96
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems[0].resolution).toBeUndefined();
|
|
97
|
+
fs_1.default.rmSync(wrong.file);
|
|
98
|
+
const invalid = correctedTask('S1', ['test']);
|
|
99
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId, replacementRequestId: invalid.requestId, generationToken: 'g1', reason: 'still invalid', resume: true })).toThrow(/sensors/);
|
|
100
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
101
|
+
expect(state.tasks).toHaveLength(0);
|
|
102
|
+
expect(state.requestProblems[0].resolution).toBeUndefined();
|
|
103
|
+
expect(state.cycle.status).toBe('BLOCKED');
|
|
104
|
+
});
|
|
105
|
+
test('stale generation, missing archive, and live supervisor lock cannot resolve a rejection', () => {
|
|
106
|
+
const rejected = rejectedTask();
|
|
107
|
+
const replacement = correctedTask();
|
|
108
|
+
const input = { rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
109
|
+
generationToken: 'g1', reason: 'corregido', resume: true };
|
|
110
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { ...input, generationToken: 'g0' })).toThrow(/generation/);
|
|
111
|
+
const lock = (0, lock_1.acquireLock)(repo);
|
|
112
|
+
try {
|
|
113
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', input)).toThrow(/supervisor activo/);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
(0, lock_1.releaseLock)(repo, lock);
|
|
117
|
+
}
|
|
118
|
+
fs_1.default.renameSync(`${rejected.file}.rejected`, `${rejected.file}.held`);
|
|
119
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', input)).toThrow();
|
|
120
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems[0].resolution).toBeUndefined();
|
|
121
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
test('an unrelated active problem prevents --resume and leaves the correction queued', () => {
|
|
124
|
+
const rejected = rejectedTask();
|
|
125
|
+
const replacement = correctedTask();
|
|
126
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
127
|
+
state.requestProblems.push({ file: path_1.default.join((0, paths_1.requestsDir)(repo, 'rama'), 'other.json'), kind: 'corrupt', detail: 'bad envelope', at: new Date().toISOString() });
|
|
128
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
129
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId,
|
|
130
|
+
replacementRequestId: replacement.requestId, generationToken: 'g1', reason: 'corregido', resume: true })).toThrow(/todas las requests/);
|
|
131
|
+
expect(fs_1.default.existsSync(replacement.file)).toBe(true);
|
|
132
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
test('a resolution-shaped state without an applied replacement is still an active gate problem', () => {
|
|
135
|
+
rejectedTask();
|
|
136
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
137
|
+
state.requestProblems[0].resolution = { replacementRequestId: 'req-1-0000000000-deadbeef', replacementPayloadDigest: 'a'.repeat(64), reason: 'forged', generationToken: 'g1', at: new Date().toISOString() };
|
|
138
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
139
|
+
expect((0, gate_1.computeGate)((0, store_1.readJournal)(repo, 'rama').state, false, () => null).reasons.some(reason => reason.category === 'request-problem')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
test('an unrelated applied task cannot be cited as the replacement of S1', () => {
|
|
142
|
+
rejectedTask();
|
|
143
|
+
const unrelated = correctedTask('S2');
|
|
144
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(1);
|
|
145
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
146
|
+
state.requestProblems[0].resolution = { replacementRequestId: unrelated.requestId, replacementPayloadDigest: state.appliedRequests[unrelated.requestId].payloadDigest, reason: 'wrong link', generationToken: 'g1', at: new Date().toISOString() };
|
|
147
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
148
|
+
expect((0, gate_1.computeGate)((0, store_1.readJournal)(repo, 'rama').state, false, () => null).reasons.some(reason => reason.category === 'request-problem')).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
test('a corrupt request cannot be laundered through a task-shaped resolution', () => {
|
|
151
|
+
const replacement = correctedTask('S1');
|
|
152
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(1);
|
|
153
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
154
|
+
state.requestProblems.push({ file: path_1.default.join((0, paths_1.requestsDir)(repo, 'rama'), 'req-1-0000000000-deadbeef.json'),
|
|
155
|
+
kind: 'corrupt', detail: 'bad JSON', at: new Date().toISOString(), requestId: 'req-1-0000000000-deadbeef',
|
|
156
|
+
taskId: 'S1', generationToken: 'g1', resolution: { replacementRequestId: replacement.requestId,
|
|
157
|
+
replacementPayloadDigest: state.appliedRequests[replacement.requestId].payloadDigest,
|
|
158
|
+
reason: 'forged', generationToken: 'g1', at: new Date().toISOString() } });
|
|
159
|
+
expect(() => (0, store_1.writeJournal)(repo, 'rama', state)).toThrow(/forma invalida/);
|
|
160
|
+
});
|
|
161
|
+
test('resume rejects a different custody cause even if its text mentions requests', () => {
|
|
162
|
+
const rejected = rejectedTask();
|
|
163
|
+
const replacement = correctedTask();
|
|
164
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
165
|
+
state.cycle.blockedReason = 'other failure; also mentions conflictos durables de requests';
|
|
166
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
167
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId,
|
|
168
|
+
replacementRequestId: replacement.requestId, generationToken: 'g1', reason: 'corrected', resume: true })).toThrow(/custodia BLOCKED por requests/);
|
|
169
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
test('a blocked request remains the specific custody cause across supervisor ticks', () => {
|
|
172
|
+
rejectedTask();
|
|
173
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
174
|
+
expect((0, supervisor_1.recoveryWhitelistBlocker)(state)).toBe('hay conflictos durables de requests');
|
|
175
|
+
});
|
|
176
|
+
test('an independent custody cause is never overwritten by an active request problem', () => {
|
|
177
|
+
rejectedTask();
|
|
178
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
179
|
+
state.cycle.blockedReason = 'veredicto de sensores no concluyente';
|
|
180
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
181
|
+
expect((0, supervisor_1.recoveryWhitelistBlocker)((0, store_1.readJournal)(repo, 'rama').state)).toBe('el ciclo está bloqueado');
|
|
182
|
+
});
|
|
183
|
+
test('public journal status exposes the latest generation token after supervisor shutdown', () => {
|
|
184
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
185
|
+
state.generations[0].state = 'terminated';
|
|
186
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
187
|
+
expect((0, archive_unused_1.watchJournalStatus)(repo, 'rama')).toMatchObject({ latestGeneration: { n: 1, token: 'g1', state: 'terminated' } });
|
|
188
|
+
});
|
|
189
|
+
test('recovery without resume remains eligible for a later explicit resume', () => {
|
|
190
|
+
const rejected = rejectedTask();
|
|
191
|
+
const replacement = correctedTask();
|
|
192
|
+
const input = { rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
193
|
+
generationToken: 'g1', reason: 'corregido', resume: false };
|
|
194
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', input);
|
|
195
|
+
const blocker = (0, supervisor_1.recoveryWhitelistBlocker)((0, store_1.readJournal)(repo, 'rama').state);
|
|
196
|
+
(0, generations_1.enterCustody)(repo, 'rama', `recovery no autorizado: ${blocker}`);
|
|
197
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { ...input, resume: true });
|
|
198
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('IN_PROGRESS');
|
|
199
|
+
});
|
|
200
|
+
test('a correction from a newer generation cannot silently resolve the archived rejection', () => {
|
|
201
|
+
const rejected = rejectedTask();
|
|
202
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
203
|
+
state.generations[0].state = 'superseded';
|
|
204
|
+
state.generations.push({ n: 2, token: 'g2', state: 'active', launchedAt: new Date().toISOString() });
|
|
205
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
206
|
+
const replacement = (0, requests_1.emitRequest)(repo, 'rama', { kind: 'register-entity', generationToken: 'g2', idempotencyKey: 'corrected-g2',
|
|
207
|
+
payload: { entity: 'task', taskId: 'S1', verificationPlan: [{ id: 't2', kind: 'test' }, { id: 's2', kind: 'sensors' }], reviewObligations: [] } });
|
|
208
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId,
|
|
209
|
+
replacementRequestId: replacement.requestId, generationToken: 'g2', reason: 'different generation', resume: true })).toThrow(/generation/);
|
|
210
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems[0].resolution).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
test('public watch recover-request command applies the correction under custody', () => {
|
|
213
|
+
const rejected = rejectedTask();
|
|
214
|
+
const replacement = correctedTask();
|
|
215
|
+
const cli = path_1.default.resolve(__dirname, '../../../src/index.ts');
|
|
216
|
+
const tsNode = require.resolve('ts-node/dist/bin.js');
|
|
217
|
+
const output = (0, child_process_1.execFileSync)(process.execPath, [tsNode, '--compiler-options', '{"module":"CommonJS"}', cli, 'watch', 'recover-request',
|
|
218
|
+
'--rejected', rejected.requestId, '--replacement', replacement.requestId,
|
|
219
|
+
'--generation', 'g1', '--reason', 'operador verificó reemplazo', '--resume'], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
220
|
+
expect(JSON.parse(output)).toMatchObject({ recovered: true, replacementRequestId: replacement.requestId });
|
|
221
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('IN_PROGRESS');
|
|
222
|
+
});
|
|
223
|
+
test('job register rejects missing sensors before writing a request file', () => {
|
|
224
|
+
const cli = path_1.default.resolve(__dirname, '../../../src/index.ts');
|
|
225
|
+
const tsNode = require.resolve('ts-node/dist/bin.js');
|
|
226
|
+
const payload = JSON.stringify({ taskId: 'S1', verificationPlan: [{ id: 't1', kind: 'test' }], reviewObligations: [] });
|
|
227
|
+
let diagnostic = '';
|
|
228
|
+
try {
|
|
229
|
+
(0, child_process_1.execFileSync)(process.execPath, [tsNode, '--compiler-options', '{"module":"CommonJS"}', cli,
|
|
230
|
+
'job', 'register', '--generation', 'g1', '--entity', 'task', '--json', payload], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
diagnostic = String(error.stderr ?? '');
|
|
234
|
+
}
|
|
235
|
+
expect(diagnostic).toMatch(/verificationPlan.*sensors/);
|
|
236
|
+
expect(fs_1.default.readdirSync((0, paths_1.requestsDir)(repo, 'rama')).filter(name => name.endsWith('.json'))).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
test('JSON cannot override --entity to bypass task prevalidation', () => {
|
|
239
|
+
const cli = path_1.default.resolve(__dirname, '../../../src/index.ts');
|
|
240
|
+
const tsNode = require.resolve('ts-node/dist/bin.js');
|
|
241
|
+
const payload = JSON.stringify({ entity: 'task', taskId: 'S1', verificationPlan: [{ id: 't1', kind: 'test' }], reviewObligations: [] });
|
|
242
|
+
let diagnostic = '';
|
|
243
|
+
try {
|
|
244
|
+
(0, child_process_1.execFileSync)(process.execPath, [tsNode, '--compiler-options', '{"module":"CommonJS"}', cli,
|
|
245
|
+
'job', 'register', '--generation', 'g1', '--entity', 'cycle-plan', '--json', payload], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
diagnostic = String(error.stderr ?? '');
|
|
249
|
+
}
|
|
250
|
+
expect(diagnostic).toMatch(/entity.*coincidir/);
|
|
251
|
+
expect(fs_1.default.readdirSync((0, paths_1.requestsDir)(repo, 'rama')).filter(name => name.endsWith('.json'))).toHaveLength(0);
|
|
252
|
+
});
|
|
253
|
+
});
|