@way_marks/server 1.0.0 → 2.0.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/dist/db/database.js +459 -433
- package/dist/policies/engine.js +11 -5
- package/dist/risk/analyzer.js +22 -11
- package/package.json +1 -1
- package/dist/approval/manager.test.js +0 -315
- package/dist/approvals/handler.test.js +0 -172
- package/dist/escalation/manager.test.js +0 -519
- package/dist/policies/engine.test.js +0 -241
- package/dist/risk/analyzer.test.js +0 -482
- package/dist/rollback/manager.test.js +0 -552
|
@@ -1,552 +0,0 @@
|
|
|
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
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const fs = __importStar(require("fs"));
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const os = __importStar(require("os"));
|
|
39
|
-
const manager_1 = require("./manager");
|
|
40
|
-
// ─── Test Helpers ───────────────────────────────────────────────────────────
|
|
41
|
-
function makeTempDir() {
|
|
42
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'waymark-test-'));
|
|
43
|
-
}
|
|
44
|
-
function cleanupTempDir(dir) {
|
|
45
|
-
if (fs.existsSync(dir)) {
|
|
46
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function makeTestAction(overrides = {}) {
|
|
50
|
-
return {
|
|
51
|
-
id: 1,
|
|
52
|
-
action_id: 'test-action-1',
|
|
53
|
-
session_id: 'test-session-1',
|
|
54
|
-
tool_name: 'write_file',
|
|
55
|
-
target_path: '/tmp/test.txt',
|
|
56
|
-
input_payload: '{}',
|
|
57
|
-
before_snapshot: null,
|
|
58
|
-
after_snapshot: null,
|
|
59
|
-
status: 'success',
|
|
60
|
-
error_message: null,
|
|
61
|
-
stdout: null,
|
|
62
|
-
stderr: null,
|
|
63
|
-
rolled_back: 0,
|
|
64
|
-
rolled_back_at: null,
|
|
65
|
-
created_at: new Date().toISOString(),
|
|
66
|
-
decision: 'allow',
|
|
67
|
-
policy_reason: null,
|
|
68
|
-
matched_rule: null,
|
|
69
|
-
approved_at: null,
|
|
70
|
-
approved_by: null,
|
|
71
|
-
rejected_at: null,
|
|
72
|
-
rejected_reason: null,
|
|
73
|
-
event_type: 'execution',
|
|
74
|
-
observation_context: null,
|
|
75
|
-
request_source: 'direct',
|
|
76
|
-
source: 'mcp',
|
|
77
|
-
is_reversible: 1,
|
|
78
|
-
...overrides,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// ─── Tests: validateRollbackable ─────────────────────────────────────────────
|
|
82
|
-
describe('rollback/manager.ts', () => {
|
|
83
|
-
describe('validateRollbackable', () => {
|
|
84
|
-
it('should accept all reversible actions', () => {
|
|
85
|
-
const actions = [
|
|
86
|
-
makeTestAction({ action_id: 'a1', is_reversible: 1 }),
|
|
87
|
-
makeTestAction({ action_id: 'a2', is_reversible: 1 }),
|
|
88
|
-
makeTestAction({ action_id: 'a3', is_reversible: 1 }),
|
|
89
|
-
];
|
|
90
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
91
|
-
expect(result.isValid).toBe(true);
|
|
92
|
-
expect(result.errors).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
it('should reject non-reversible write_file actions', () => {
|
|
95
|
-
const actions = [
|
|
96
|
-
makeTestAction({
|
|
97
|
-
action_id: 'a1',
|
|
98
|
-
is_reversible: 1,
|
|
99
|
-
before_snapshot: JSON.stringify({ file_path: '/tmp/test.txt', content: 'old', existed: true }),
|
|
100
|
-
}),
|
|
101
|
-
makeTestAction({
|
|
102
|
-
action_id: 'a2',
|
|
103
|
-
is_reversible: 0, // Not reversible!
|
|
104
|
-
before_snapshot: null,
|
|
105
|
-
}),
|
|
106
|
-
];
|
|
107
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
108
|
-
expect(result.isValid).toBe(false);
|
|
109
|
-
expect(result.errors.length).toBe(1);
|
|
110
|
-
expect(result.errors[0].action_id).toBe('a2');
|
|
111
|
-
expect(result.errors[0].reason).toMatch(/non-reversible/i);
|
|
112
|
-
});
|
|
113
|
-
it('should reject write_file without before_snapshot', () => {
|
|
114
|
-
const actions = [
|
|
115
|
-
makeTestAction({
|
|
116
|
-
action_id: 'a1',
|
|
117
|
-
tool_name: 'write_file',
|
|
118
|
-
is_reversible: 1,
|
|
119
|
-
before_snapshot: null, // Missing!
|
|
120
|
-
}),
|
|
121
|
-
];
|
|
122
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
123
|
-
expect(result.isValid).toBe(false);
|
|
124
|
-
expect(result.errors.length).toBe(1);
|
|
125
|
-
expect(result.errors[0].reason).toMatch(/before_snapshot/i);
|
|
126
|
-
});
|
|
127
|
-
it('should warn about irreversible bash commands (DELETE)', () => {
|
|
128
|
-
const actions = [
|
|
129
|
-
makeTestAction({
|
|
130
|
-
action_id: 'a1',
|
|
131
|
-
tool_name: 'bash',
|
|
132
|
-
is_reversible: 1,
|
|
133
|
-
input_payload: JSON.stringify({ command: 'DELETE FROM users WHERE id = 1' }),
|
|
134
|
-
}),
|
|
135
|
-
];
|
|
136
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
137
|
-
expect(result.isValid).toBe(false);
|
|
138
|
-
expect(result.errors[0].reason).toMatch(/DELETE/);
|
|
139
|
-
});
|
|
140
|
-
it('should warn about irreversible bash commands (rm -rf)', () => {
|
|
141
|
-
const actions = [
|
|
142
|
-
makeTestAction({
|
|
143
|
-
action_id: 'a1',
|
|
144
|
-
tool_name: 'bash',
|
|
145
|
-
is_reversible: 1,
|
|
146
|
-
input_payload: JSON.stringify({ command: 'rm -rf /important/data' }),
|
|
147
|
-
}),
|
|
148
|
-
];
|
|
149
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
150
|
-
expect(result.isValid).toBe(false);
|
|
151
|
-
expect(result.errors[0].reason).toMatch(/rm -rf/);
|
|
152
|
-
});
|
|
153
|
-
it('should warn about DROP TABLE', () => {
|
|
154
|
-
const actions = [
|
|
155
|
-
makeTestAction({
|
|
156
|
-
action_id: 'a1',
|
|
157
|
-
tool_name: 'bash',
|
|
158
|
-
is_reversible: 1,
|
|
159
|
-
input_payload: JSON.stringify({ command: 'DROP TABLE users' }),
|
|
160
|
-
}),
|
|
161
|
-
];
|
|
162
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
163
|
-
expect(result.isValid).toBe(false);
|
|
164
|
-
expect(result.errors[0].reason).toMatch(/DROP\s+TABLE/);
|
|
165
|
-
});
|
|
166
|
-
it('should allow safe bash commands', () => {
|
|
167
|
-
const actions = [
|
|
168
|
-
makeTestAction({
|
|
169
|
-
action_id: 'a1',
|
|
170
|
-
tool_name: 'bash',
|
|
171
|
-
is_reversible: 1,
|
|
172
|
-
input_payload: JSON.stringify({ command: 'echo hello > /tmp/log.txt' }),
|
|
173
|
-
}),
|
|
174
|
-
];
|
|
175
|
-
const result = (0, manager_1.validateRollbackable)(actions);
|
|
176
|
-
expect(result.isValid).toBe(true);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
// ─── Tests: createRollbackTransaction ────────────────────────────────────
|
|
180
|
-
describe('createRollbackTransaction', () => {
|
|
181
|
-
it('should create transaction with no file restores for read_file', () => {
|
|
182
|
-
const actions = [
|
|
183
|
-
makeTestAction({
|
|
184
|
-
action_id: 'a1',
|
|
185
|
-
tool_name: 'read_file',
|
|
186
|
-
is_reversible: 1,
|
|
187
|
-
}),
|
|
188
|
-
];
|
|
189
|
-
const transaction = (0, manager_1.createRollbackTransaction)('session-1', actions);
|
|
190
|
-
expect(transaction.session_id).toBe('session-1');
|
|
191
|
-
expect(transaction.actions).toEqual(actions);
|
|
192
|
-
expect(transaction.fileRestores).toEqual([]);
|
|
193
|
-
});
|
|
194
|
-
it('should extract file restores from write_file actions', () => {
|
|
195
|
-
const snapshot = {
|
|
196
|
-
file_path: '/tmp/test.txt',
|
|
197
|
-
content: 'original content',
|
|
198
|
-
existed: true,
|
|
199
|
-
};
|
|
200
|
-
const actions = [
|
|
201
|
-
makeTestAction({
|
|
202
|
-
action_id: 'a1',
|
|
203
|
-
tool_name: 'write_file',
|
|
204
|
-
before_snapshot: JSON.stringify(snapshot),
|
|
205
|
-
}),
|
|
206
|
-
];
|
|
207
|
-
const transaction = (0, manager_1.createRollbackTransaction)('session-1', actions);
|
|
208
|
-
expect(transaction.fileRestores).toHaveLength(1);
|
|
209
|
-
expect(transaction.fileRestores[0].file_path).toBe('/tmp/test.txt');
|
|
210
|
-
expect(transaction.fileRestores[0].snapshot).toEqual(snapshot);
|
|
211
|
-
});
|
|
212
|
-
it('should handle multiple file restores', () => {
|
|
213
|
-
const actions = [
|
|
214
|
-
makeTestAction({
|
|
215
|
-
action_id: 'a1',
|
|
216
|
-
tool_name: 'write_file',
|
|
217
|
-
target_path: '/tmp/file1.txt',
|
|
218
|
-
before_snapshot: JSON.stringify({
|
|
219
|
-
file_path: '/tmp/file1.txt',
|
|
220
|
-
content: 'content1',
|
|
221
|
-
existed: true,
|
|
222
|
-
}),
|
|
223
|
-
}),
|
|
224
|
-
makeTestAction({
|
|
225
|
-
action_id: 'a2',
|
|
226
|
-
tool_name: 'write_file',
|
|
227
|
-
target_path: '/tmp/file2.txt',
|
|
228
|
-
before_snapshot: JSON.stringify({
|
|
229
|
-
file_path: '/tmp/file2.txt',
|
|
230
|
-
content: 'content2',
|
|
231
|
-
existed: true,
|
|
232
|
-
}),
|
|
233
|
-
}),
|
|
234
|
-
];
|
|
235
|
-
const transaction = (0, manager_1.createRollbackTransaction)('session-1', actions);
|
|
236
|
-
expect(transaction.fileRestores).toHaveLength(2);
|
|
237
|
-
expect(transaction.fileRestores.map((r) => r.file_path)).toEqual(['/tmp/file1.txt', '/tmp/file2.txt']);
|
|
238
|
-
});
|
|
239
|
-
it('should handle file deletion (file did not exist before)', () => {
|
|
240
|
-
const snapshot = {
|
|
241
|
-
file_path: '/tmp/newfile.txt',
|
|
242
|
-
content: null,
|
|
243
|
-
existed: false,
|
|
244
|
-
};
|
|
245
|
-
const actions = [
|
|
246
|
-
makeTestAction({
|
|
247
|
-
action_id: 'a1',
|
|
248
|
-
tool_name: 'write_file',
|
|
249
|
-
before_snapshot: JSON.stringify(snapshot),
|
|
250
|
-
}),
|
|
251
|
-
];
|
|
252
|
-
const transaction = (0, manager_1.createRollbackTransaction)('session-1', actions);
|
|
253
|
-
expect(transaction.fileRestores[0].snapshot.existed).toBe(false);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
// ─── Tests: executeRollbackTransaction ───────────────────────────────────
|
|
257
|
-
describe('executeRollbackTransaction', () => {
|
|
258
|
-
let tempDir;
|
|
259
|
-
beforeEach(() => {
|
|
260
|
-
tempDir = makeTempDir();
|
|
261
|
-
process.env.WAYMARK_PROJECT_ROOT = tempDir;
|
|
262
|
-
});
|
|
263
|
-
afterEach(() => {
|
|
264
|
-
cleanupTempDir(tempDir);
|
|
265
|
-
});
|
|
266
|
-
it('should restore a file from snapshot', () => {
|
|
267
|
-
const filePath = path.join(tempDir, 'test.txt');
|
|
268
|
-
const originalContent = 'original content';
|
|
269
|
-
// Create original file
|
|
270
|
-
fs.writeFileSync(filePath, originalContent, 'utf-8');
|
|
271
|
-
// Modify it (simulating agent action)
|
|
272
|
-
fs.writeFileSync(filePath, 'modified content', 'utf-8');
|
|
273
|
-
// Create rollback transaction
|
|
274
|
-
const transaction = {
|
|
275
|
-
session_id: 'session-1',
|
|
276
|
-
actions: [
|
|
277
|
-
makeTestAction({
|
|
278
|
-
action_id: 'a1',
|
|
279
|
-
tool_name: 'write_file',
|
|
280
|
-
}),
|
|
281
|
-
],
|
|
282
|
-
fileRestores: [
|
|
283
|
-
{
|
|
284
|
-
action_id: 'a1',
|
|
285
|
-
file_path: 'test.txt',
|
|
286
|
-
snapshot: {
|
|
287
|
-
file_path: 'test.txt',
|
|
288
|
-
content: originalContent,
|
|
289
|
-
existed: true,
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
};
|
|
294
|
-
// Execute rollback
|
|
295
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
296
|
-
expect(result.success).toBe(true);
|
|
297
|
-
expect(result.filesRestored).toBe(1);
|
|
298
|
-
// Verify file was restored
|
|
299
|
-
const restoredContent = fs.readFileSync(filePath, 'utf-8');
|
|
300
|
-
expect(restoredContent).toBe(originalContent);
|
|
301
|
-
});
|
|
302
|
-
it('should delete a file that did not exist before', () => {
|
|
303
|
-
const filePath = path.join(tempDir, 'newfile.txt');
|
|
304
|
-
// Agent creates a new file
|
|
305
|
-
fs.writeFileSync(filePath, 'new content', 'utf-8');
|
|
306
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
307
|
-
// Create rollback transaction (file didn't exist before)
|
|
308
|
-
const transaction = {
|
|
309
|
-
session_id: 'session-1',
|
|
310
|
-
actions: [
|
|
311
|
-
makeTestAction({
|
|
312
|
-
action_id: 'a1',
|
|
313
|
-
tool_name: 'write_file',
|
|
314
|
-
}),
|
|
315
|
-
],
|
|
316
|
-
fileRestores: [
|
|
317
|
-
{
|
|
318
|
-
action_id: 'a1',
|
|
319
|
-
file_path: 'newfile.txt',
|
|
320
|
-
snapshot: {
|
|
321
|
-
file_path: 'newfile.txt',
|
|
322
|
-
content: null,
|
|
323
|
-
existed: false,
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
],
|
|
327
|
-
};
|
|
328
|
-
// Execute rollback
|
|
329
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
330
|
-
expect(result.success).toBe(true);
|
|
331
|
-
expect(result.filesRestored).toBe(1);
|
|
332
|
-
// Verify file was deleted
|
|
333
|
-
expect(fs.existsSync(filePath)).toBe(false);
|
|
334
|
-
});
|
|
335
|
-
it('should create directories if needed', () => {
|
|
336
|
-
const filePath = path.join(tempDir, 'subdir', 'nested', 'test.txt');
|
|
337
|
-
const content = 'test content';
|
|
338
|
-
// Create rollback transaction for nested file
|
|
339
|
-
const transaction = {
|
|
340
|
-
session_id: 'session-1',
|
|
341
|
-
actions: [
|
|
342
|
-
makeTestAction({
|
|
343
|
-
action_id: 'a1',
|
|
344
|
-
tool_name: 'write_file',
|
|
345
|
-
}),
|
|
346
|
-
],
|
|
347
|
-
fileRestores: [
|
|
348
|
-
{
|
|
349
|
-
action_id: 'a1',
|
|
350
|
-
file_path: 'subdir/nested/test.txt',
|
|
351
|
-
snapshot: {
|
|
352
|
-
file_path: 'subdir/nested/test.txt',
|
|
353
|
-
content,
|
|
354
|
-
existed: true,
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
],
|
|
358
|
-
};
|
|
359
|
-
// Execute rollback
|
|
360
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
361
|
-
expect(result.success).toBe(true);
|
|
362
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
363
|
-
expect(fs.readFileSync(filePath, 'utf-8')).toBe(content);
|
|
364
|
-
});
|
|
365
|
-
it('should handle multiple file restores atomically', () => {
|
|
366
|
-
const file1 = path.join(tempDir, 'file1.txt');
|
|
367
|
-
const file2 = path.join(tempDir, 'file2.txt');
|
|
368
|
-
fs.writeFileSync(file1, 'original1', 'utf-8');
|
|
369
|
-
fs.writeFileSync(file2, 'original2', 'utf-8');
|
|
370
|
-
const transaction = {
|
|
371
|
-
session_id: 'session-1',
|
|
372
|
-
actions: [
|
|
373
|
-
makeTestAction({ action_id: 'a1' }),
|
|
374
|
-
makeTestAction({ action_id: 'a2' }),
|
|
375
|
-
],
|
|
376
|
-
fileRestores: [
|
|
377
|
-
{
|
|
378
|
-
action_id: 'a1',
|
|
379
|
-
file_path: 'file1.txt',
|
|
380
|
-
snapshot: { file_path: 'file1.txt', content: 'original1', existed: true },
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
action_id: 'a2',
|
|
384
|
-
file_path: 'file2.txt',
|
|
385
|
-
snapshot: { file_path: 'file2.txt', content: 'original2', existed: true },
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
};
|
|
389
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
390
|
-
expect(result.success).toBe(true);
|
|
391
|
-
expect(result.filesRestored).toBe(2);
|
|
392
|
-
expect(fs.readFileSync(file1, 'utf-8')).toBe('original1');
|
|
393
|
-
expect(fs.readFileSync(file2, 'utf-8')).toBe('original2');
|
|
394
|
-
});
|
|
395
|
-
it('should fail if file is not accessible', () => {
|
|
396
|
-
const filePath = path.join(tempDir, 'readonly', 'test.txt');
|
|
397
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
398
|
-
fs.writeFileSync(filePath, 'test', 'utf-8');
|
|
399
|
-
// Make directory read-only (Unix only)
|
|
400
|
-
if (process.platform !== 'win32') {
|
|
401
|
-
fs.chmodSync(path.dirname(filePath), 0o444);
|
|
402
|
-
}
|
|
403
|
-
const transaction = {
|
|
404
|
-
session_id: 'session-1',
|
|
405
|
-
actions: [makeTestAction({ action_id: 'a1' })],
|
|
406
|
-
fileRestores: [
|
|
407
|
-
{
|
|
408
|
-
action_id: 'a1',
|
|
409
|
-
file_path: 'readonly/test.txt',
|
|
410
|
-
snapshot: { file_path: 'readonly/test.txt', content: 'modified', existed: true },
|
|
411
|
-
},
|
|
412
|
-
],
|
|
413
|
-
};
|
|
414
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
415
|
-
// Restore permissions
|
|
416
|
-
if (process.platform !== 'win32') {
|
|
417
|
-
fs.chmodSync(path.dirname(filePath), 0o755);
|
|
418
|
-
}
|
|
419
|
-
// Should fail on non-Windows systems
|
|
420
|
-
if (process.platform !== 'win32') {
|
|
421
|
-
expect(result.success).toBe(false);
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
// ─── Tests: rollbackSession (high-level API) ────────────────────────────
|
|
426
|
-
describe('rollbackSession', () => {
|
|
427
|
-
let tempDir;
|
|
428
|
-
beforeEach(() => {
|
|
429
|
-
tempDir = makeTempDir();
|
|
430
|
-
process.env.WAYMARK_PROJECT_ROOT = tempDir;
|
|
431
|
-
});
|
|
432
|
-
afterEach(() => {
|
|
433
|
-
cleanupTempDir(tempDir);
|
|
434
|
-
});
|
|
435
|
-
it('should rollback a simple session', () => {
|
|
436
|
-
const filePath = path.join(tempDir, 'test.txt');
|
|
437
|
-
fs.writeFileSync(filePath, 'original', 'utf-8');
|
|
438
|
-
// Mock getSessionActions to return test actions
|
|
439
|
-
const testActions = [
|
|
440
|
-
makeTestAction({
|
|
441
|
-
action_id: 'a1',
|
|
442
|
-
session_id: 'session-1',
|
|
443
|
-
tool_name: 'write_file',
|
|
444
|
-
before_snapshot: JSON.stringify({
|
|
445
|
-
file_path: 'test.txt',
|
|
446
|
-
content: 'original',
|
|
447
|
-
existed: true,
|
|
448
|
-
}),
|
|
449
|
-
is_reversible: 1,
|
|
450
|
-
}),
|
|
451
|
-
];
|
|
452
|
-
// Simulate file modification
|
|
453
|
-
fs.writeFileSync(filePath, 'modified', 'utf-8');
|
|
454
|
-
// Since rollbackSession calls getSessionActions which hits the DB,
|
|
455
|
-
// we can't easily mock it in this test framework.
|
|
456
|
-
// For full E2E, we'd need a test database or mock.
|
|
457
|
-
// For now, we test the logic components individually.
|
|
458
|
-
expect(true).toBe(true);
|
|
459
|
-
});
|
|
460
|
-
it('should return error if no actions in session', () => {
|
|
461
|
-
const result = (0, manager_1.rollbackSession)('empty-session');
|
|
462
|
-
expect(result.success).toBe(false);
|
|
463
|
-
expect(result.message).toMatch(/no actions/i);
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
// ─── Integration Tests ───────────────────────────────────────────────────
|
|
467
|
-
describe('integration: full rollback workflow', () => {
|
|
468
|
-
let tempDir;
|
|
469
|
-
beforeEach(() => {
|
|
470
|
-
tempDir = makeTempDir();
|
|
471
|
-
process.env.WAYMARK_PROJECT_ROOT = tempDir;
|
|
472
|
-
});
|
|
473
|
-
afterEach(() => {
|
|
474
|
-
cleanupTempDir(tempDir);
|
|
475
|
-
});
|
|
476
|
-
it('should handle a 3-action session rollback', () => {
|
|
477
|
-
// Setup: Create original files
|
|
478
|
-
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
|
|
479
|
-
files.forEach((f) => fs.writeFileSync(path.join(tempDir, f), `original ${f}`, 'utf-8'));
|
|
480
|
-
// Create actions
|
|
481
|
-
const actions = files.map((f, i) => makeTestAction({
|
|
482
|
-
action_id: `a${i}`,
|
|
483
|
-
session_id: 'test-session',
|
|
484
|
-
tool_name: 'write_file',
|
|
485
|
-
target_path: f,
|
|
486
|
-
before_snapshot: JSON.stringify({
|
|
487
|
-
file_path: f,
|
|
488
|
-
content: `original ${f}`,
|
|
489
|
-
existed: true,
|
|
490
|
-
}),
|
|
491
|
-
is_reversible: 1,
|
|
492
|
-
}));
|
|
493
|
-
// Validate
|
|
494
|
-
const validation = (0, manager_1.validateRollbackable)(actions);
|
|
495
|
-
expect(validation.isValid).toBe(true);
|
|
496
|
-
// Create transaction
|
|
497
|
-
const transaction = (0, manager_1.createRollbackTransaction)('test-session', actions);
|
|
498
|
-
expect(transaction.fileRestores).toHaveLength(3);
|
|
499
|
-
// Simulate modifications
|
|
500
|
-
files.forEach((f) => fs.writeFileSync(path.join(tempDir, f), `modified ${f}`, 'utf-8'));
|
|
501
|
-
// Execute rollback
|
|
502
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
503
|
-
expect(result.success).toBe(true);
|
|
504
|
-
expect(result.filesRestored).toBe(3);
|
|
505
|
-
// Verify all files restored
|
|
506
|
-
files.forEach((f) => {
|
|
507
|
-
const content = fs.readFileSync(path.join(tempDir, f), 'utf-8');
|
|
508
|
-
expect(content).toBe(`original ${f}`);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
it('should correctly handle mix of file creation and modification', () => {
|
|
512
|
-
const modifiedFile = path.join(tempDir, 'modified.txt');
|
|
513
|
-
const newFile = path.join(tempDir, 'new.txt');
|
|
514
|
-
// Setup
|
|
515
|
-
fs.writeFileSync(modifiedFile, 'original', 'utf-8');
|
|
516
|
-
// Actions
|
|
517
|
-
const actions = [
|
|
518
|
-
makeTestAction({
|
|
519
|
-
action_id: 'a1',
|
|
520
|
-
tool_name: 'write_file',
|
|
521
|
-
before_snapshot: JSON.stringify({
|
|
522
|
-
file_path: 'modified.txt',
|
|
523
|
-
content: 'original',
|
|
524
|
-
existed: true,
|
|
525
|
-
}),
|
|
526
|
-
is_reversible: 1,
|
|
527
|
-
}),
|
|
528
|
-
makeTestAction({
|
|
529
|
-
action_id: 'a2',
|
|
530
|
-
tool_name: 'write_file',
|
|
531
|
-
before_snapshot: JSON.stringify({
|
|
532
|
-
file_path: 'new.txt',
|
|
533
|
-
content: null,
|
|
534
|
-
existed: false,
|
|
535
|
-
}),
|
|
536
|
-
is_reversible: 1,
|
|
537
|
-
}),
|
|
538
|
-
];
|
|
539
|
-
// Simulate agent actions
|
|
540
|
-
fs.writeFileSync(modifiedFile, 'modified', 'utf-8');
|
|
541
|
-
fs.writeFileSync(newFile, 'created', 'utf-8');
|
|
542
|
-
// Rollback
|
|
543
|
-
const validation = (0, manager_1.validateRollbackable)(actions);
|
|
544
|
-
expect(validation.isValid).toBe(true);
|
|
545
|
-
const transaction = (0, manager_1.createRollbackTransaction)('session', actions);
|
|
546
|
-
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
547
|
-
expect(result.success).toBe(true);
|
|
548
|
-
expect(fs.readFileSync(modifiedFile, 'utf-8')).toBe('original');
|
|
549
|
-
expect(fs.existsSync(newFile)).toBe(false);
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
});
|