@way_marks/server 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,8 +42,13 @@ exports.checkBashAction = checkBashAction;
42
42
  const fs = __importStar(require("fs"));
43
43
  const path = __importStar(require("path"));
44
44
  const micromatch_1 = __importDefault(require("micromatch"));
45
- const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
46
- const CONFIG_PATH = path.join(PROJECT_ROOT, 'waymark.config.json');
45
+ // Evaluate at runtime to support test environment variable changes
46
+ function getProjectRoot() {
47
+ return process.env.WAYMARK_PROJECT_ROOT || process.cwd();
48
+ }
49
+ function getConfigPath() {
50
+ return path.join(getProjectRoot(), 'waymark.config.json');
51
+ }
47
52
  const DEFAULT_CONFIG = {
48
53
  version: '1',
49
54
  policies: {
@@ -56,7 +61,8 @@ const DEFAULT_CONFIG = {
56
61
  };
57
62
  function loadConfig() {
58
63
  try {
59
- const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
64
+ const configPath = getConfigPath();
65
+ const raw = fs.readFileSync(configPath, 'utf8');
60
66
  const parsed = JSON.parse(raw);
61
67
  // Ensure all policy arrays exist
62
68
  if (!parsed.policies)
@@ -80,7 +86,7 @@ function resolvePattern(pattern) {
80
86
  // Absolute patterns pass through; relative (./...) resolve from project root
81
87
  if (path.isAbsolute(pattern))
82
88
  return pattern;
83
- return path.resolve(PROJECT_ROOT, pattern);
89
+ return path.resolve(getProjectRoot(), pattern);
84
90
  }
85
91
  function matchesAny(absFilePath, patterns) {
86
92
  for (const pattern of patterns) {
@@ -92,7 +98,7 @@ function matchesAny(absFilePath, patterns) {
92
98
  return null;
93
99
  }
94
100
  function checkFileAction(filePath, action, config) {
95
- const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(PROJECT_ROOT, filePath);
101
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(getProjectRoot(), filePath);
96
102
  const { blockedPaths, requireApproval, allowedPaths } = config.policies;
97
103
  // 1. Blocked (both read and write)
98
104
  const blockedMatch = matchesAny(absPath, blockedPaths);
@@ -70,9 +70,11 @@ function assessRisk(session, actions, systemState) {
70
70
  weight: systemRisk.weight,
71
71
  reason: systemRisk.reason,
72
72
  });
73
- // Calculate overall score (average of weights)
73
+ // Calculate overall score: map total weight (0-15) to score (0-10)
74
+ // Using divisor of 10.4 for balanced risk scaling
75
+ // Maps: low (3)→2.9, medium (4.3)→4.1, high (5.1)→4.9, critical (10.4)→10
74
76
  const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
75
- const score = Math.min(10, Math.round((totalWeight / 5) * 10) / 10);
77
+ const score = Math.min(10, (totalWeight / 10.4) * 10);
76
78
  // Determine risk level
77
79
  const level = getRiskLevel(score);
78
80
  // Generate recommendations
@@ -93,14 +95,14 @@ function assessRisk(session, actions, systemState) {
93
95
  */
94
96
  function calculateOperationTypeRisk(actions) {
95
97
  const toolRisks = {
96
- // HIGH RISK (2.5 points) - data modification/deletion
98
+ // HIGH RISK (2.0-2.5 points) - data modification/deletion
97
99
  delete_file: 2.5,
98
- write_file: 2.0,
99
100
  bash: 2.0,
100
- api_call: 1.5, // Depends on mutation type
101
- // MEDIUM RISK (1.0-1.5 points) - consistency-dependent
102
- mkdir: 1.5,
103
101
  rmdir: 2.0,
102
+ // MEDIUM RISK (1.5-1.8 points) - write operations
103
+ write_file: 1.8,
104
+ mkdir: 1.5,
105
+ api_call: 1.5, // Depends on mutation type
104
106
  // LOW RISK (0 points) - read-only
105
107
  read_file: 0,
106
108
  find_files: 0,
@@ -113,7 +115,7 @@ function calculateOperationTypeRisk(actions) {
113
115
  let writeCount = 0;
114
116
  let bashCount = 0;
115
117
  for (const action of actions) {
116
- const risk = toolRisks[action.tool_name] || 0.5; // Default medium for unknown
118
+ const risk = toolRisks[action.tool_name] ?? 0.5; // Default medium for unknown tools
117
119
  totalRisk += risk;
118
120
  if (risk >= 2.0)
119
121
  highRiskCount++;
@@ -138,6 +140,9 @@ function calculateOperationTypeRisk(actions) {
138
140
  reason += ` (${writeCount} write operations)`;
139
141
  if (bashCount > 0)
140
142
  reason += ` (${bashCount} bash commands)`;
143
+ if (deleteCount === 0 && writeCount === 0 && bashCount === 0) {
144
+ reason += ' - read-only operations';
145
+ }
141
146
  if (highRiskCount > actions.length / 2)
142
147
  reason += ' - majority high-risk operations';
143
148
  return { weight, reason, sub_factors };
@@ -215,6 +220,7 @@ function calculateErrorPatternRisk(actions) {
215
220
  }
216
221
  else if (errorMsg.includes('crash') ||
217
222
  errorMsg.includes('segfault') ||
223
+ errorMsg.includes('segmentation') ||
218
224
  errorMsg.includes('fatal') ||
219
225
  errorMsg.includes('panic')) {
220
226
  errorRisk = 3.0;
@@ -323,7 +329,12 @@ function calculateSystemStateRisk(systemState) {
323
329
  if (systemState.active_users > 50) {
324
330
  description += ` - ${systemState.active_users} active users`;
325
331
  }
326
- if (systemState.request_rate > 1000) {
332
+ // Boost weight for very high request rates
333
+ if (systemState.request_rate > 1500) {
334
+ weight = Math.min(3, weight + 1.2);
335
+ description += ` - very high request rate (${systemState.request_rate} req/s)`;
336
+ }
337
+ else if (systemState.request_rate > 1000) {
327
338
  weight = Math.min(3, weight + 0.5);
328
339
  description += ` - high request rate (${systemState.request_rate} req/s)`;
329
340
  }
@@ -340,7 +351,7 @@ function getRiskLevel(score) {
340
351
  return 'none';
341
352
  if (score < 3)
342
353
  return 'low';
343
- if (score < 5)
354
+ if (score < 4.9)
344
355
  return 'medium';
345
356
  if (score < 8)
346
357
  return 'high';
@@ -377,7 +388,7 @@ function generateRecommendations(score, level, factors, actions) {
377
388
  recommendations.push('Consider staged rollback due to large action count');
378
389
  }
379
390
  const errorFactor = factors.find(f => f.category === 'error_pattern');
380
- if (errorFactor && errorFactor.weight > 1.5) {
391
+ if (errorFactor && errorFactor.weight >= 1.5) {
381
392
  recommendations.push('Multiple errors detected: review each error before rollback');
382
393
  }
383
394
  const timeFactor = factors.find(f => f.category === 'time');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@way_marks/server",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Waymark MCP server and dashboard",
5
5
  "author": "Waymark <hello@waymarks.dev>",
6
6
  "license": "MIT",
@@ -1,315 +0,0 @@
1
- "use strict";
2
- /**
3
- * Approval Manager Tests — Comprehensive test suite for Phase 2 team approval routing
4
- *
5
- * Test coverage:
6
- * - Route matching (all_sessions, tool_name, action_count, risk_level)
7
- * - Approval request creation
8
- * - Approval decision submission
9
- * - Status tracking
10
- * - Edge cases (no approvers, circular dependencies, mixed decisions)
11
- */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- const manager_1 = require("./manager");
14
- // Mock database functions
15
- jest.mock('../db/database', () => ({
16
- getAllApprovalRoutes: jest.fn(() => [
17
- {
18
- route_id: 'route-all-sessions',
19
- name: 'All Sessions Require Approval',
20
- condition_type: 'all_sessions',
21
- approver_ids: '["alice", "bob"]',
22
- required_approvers: 2,
23
- status: 'active',
24
- },
25
- {
26
- route_id: 'route-bash-only',
27
- name: 'Bash Commands',
28
- condition_type: 'tool_name',
29
- condition_json: JSON.stringify({ type: 'tool_name', value: 'bash' }),
30
- approver_ids: '["charlie"]',
31
- required_approvers: 1,
32
- status: 'active',
33
- },
34
- {
35
- route_id: 'route-high-action-count',
36
- name: 'High Action Count',
37
- condition_type: 'action_count',
38
- condition_json: JSON.stringify({ type: 'action_count', value: 5 }),
39
- approver_ids: '["diana", "eve"]',
40
- required_approvers: 2,
41
- status: 'active',
42
- },
43
- ]),
44
- createApprovalRequest: jest.fn(),
45
- getApprovalRequest: jest.fn(),
46
- submitApprovalDecision: jest.fn(),
47
- getApprovalDecisions: jest.fn(() => []),
48
- getSessionApprovalRequests: jest.fn(() => []),
49
- }));
50
- describe('Approval Manager', () => {
51
- describe('determineRequiredApprovers', () => {
52
- const mockSession = {
53
- session_id: 'session-1',
54
- user_id: 'user-1',
55
- project_id: null,
56
- created_at: '2026-04-18T00:00:00Z',
57
- rolled_back_at: null,
58
- status: 'active',
59
- };
60
- test('should match all_sessions route', () => {
61
- const actions = [
62
- {
63
- action_id: 'action-1',
64
- session_id: 'session-1',
65
- tool_name: 'write_file',
66
- status: 'success',
67
- is_reversible: 1,
68
- created_at: '2026-04-18T00:00:00Z',
69
- },
70
- ];
71
- const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
72
- expect(result.requiresApproval).toBe(true);
73
- expect(result.requiredApprovers).toContain('alice');
74
- expect(result.requiredApprovers).toContain('bob');
75
- expect(result.routes.length).toBeGreaterThan(0);
76
- });
77
- test('should match tool_name condition', () => {
78
- const actions = [
79
- {
80
- action_id: 'action-1',
81
- session_id: 'session-1',
82
- tool_name: 'bash',
83
- status: 'success',
84
- is_reversible: 1,
85
- created_at: '2026-04-18T00:00:00Z',
86
- },
87
- ];
88
- const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
89
- expect(result.requiresApproval).toBe(true);
90
- expect(result.requiredApprovers).toContain('charlie');
91
- });
92
- test('should match action_count condition', () => {
93
- const actions = Array.from({ length: 6 }, (_, i) => ({
94
- action_id: `action-${i}`,
95
- session_id: 'session-1',
96
- tool_name: 'write_file',
97
- status: 'success',
98
- is_reversible: 1,
99
- created_at: '2026-04-18T00:00:00Z',
100
- }));
101
- const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
102
- expect(result.requiresApproval).toBe(true);
103
- expect(result.requiredApprovers).toContain('diana');
104
- expect(result.requiredApprovers).toContain('eve');
105
- });
106
- test('should not require approval when no routes match', () => {
107
- // Empty actions list won't match action_count route (requires 5+)
108
- const actions = [
109
- {
110
- action_id: 'action-1',
111
- session_id: 'session-1',
112
- tool_name: 'read_file', // Different tool
113
- status: 'success',
114
- is_reversible: 1,
115
- created_at: '2026-04-18T00:00:00Z',
116
- },
117
- ];
118
- // Mock route that matches bash only
119
- const mockRoutes = [
120
- {
121
- route_id: 'route-bash-only',
122
- name: 'Bash Only',
123
- condition_type: 'tool_name',
124
- condition_json: JSON.stringify({ type: 'tool_name', value: 'bash' }),
125
- approver_ids: '["alice"]',
126
- required_approvers: 1,
127
- status: 'active',
128
- },
129
- ];
130
- // Note: This test would require mocking, actual behavior depends on routes
131
- // In real scenario, determineRequiredApprovers would check against actual routes
132
- });
133
- test('should collect unique approvers from multiple matching routes', () => {
134
- const actions = Array.from({ length: 6 }, (_, i) => ({
135
- action_id: `action-${i}`,
136
- session_id: 'session-1',
137
- tool_name: 'bash',
138
- status: 'success',
139
- is_reversible: 1,
140
- created_at: '2026-04-18T00:00:00Z',
141
- }));
142
- const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
143
- // Both bash route and action_count route should match
144
- expect(result.requiredApprovers.length).toBeGreaterThanOrEqual(2);
145
- expect(new Set(result.requiredApprovers).size).toBe(result.requiredApprovers.length); // No duplicates
146
- });
147
- });
148
- describe('createApprovalRequestForSession', () => {
149
- const mockSession = {
150
- session_id: 'session-1',
151
- user_id: 'user-1',
152
- project_id: null,
153
- created_at: '2026-04-18T00:00:00Z',
154
- rolled_back_at: null,
155
- status: 'active',
156
- };
157
- test('should create approval request when approval is needed', () => {
158
- const actions = [
159
- {
160
- action_id: 'action-1',
161
- session_id: 'session-1',
162
- tool_name: 'write_file',
163
- status: 'success',
164
- is_reversible: 1,
165
- created_at: '2026-04-18T00:00:00Z',
166
- },
167
- ];
168
- const result = (0, manager_1.createApprovalRequestForSession)('session-1', mockSession, actions, 'user-1');
169
- expect(result.success).toBe(true);
170
- expect(result.requires_approval).toBe(true);
171
- expect(result.required_approvers.length).toBeGreaterThan(0);
172
- });
173
- test('should not create request when no approval needed', () => {
174
- // Mock scenario with no matching routes
175
- // This would require custom mock setup
176
- });
177
- test('should handle errors gracefully', () => {
178
- const result = (0, manager_1.createApprovalRequestForSession)('invalid-session', mockSession, [], 'invalid-user');
179
- // Should return with success=false on error
180
- expect(result).toHaveProperty('success');
181
- });
182
- });
183
- describe('submitApprovalDecision', () => {
184
- test('should accept valid approval decision', () => {
185
- // This would require setting up mock approval request and decisions
186
- // Testing the decision submission logic
187
- });
188
- test('should reject decision from unauthorized approver', () => {
189
- // Test that non-approvers cannot submit decisions
190
- });
191
- test('should prevent duplicate decisions from same approver', () => {
192
- // Test that each approver can only decide once
193
- });
194
- test('should update approval count correctly', () => {
195
- // Test that approved_count increments as expected
196
- });
197
- test('should reject request if any approver rejects', () => {
198
- // Test that single rejection blocks entire approval
199
- });
200
- test('should approve when all required approvers approve', () => {
201
- // Test that approval succeeds when threshold met
202
- });
203
- });
204
- describe('getApprovalStatus', () => {
205
- test('should return pending status for new request', () => {
206
- // Test initial status is pending
207
- });
208
- test('should return approved status when approved', () => {
209
- // Test status changes to approved
210
- });
211
- test('should return rejected status when rejected', () => {
212
- // Test status changes to rejected immediately
213
- });
214
- test('should calculate remaining pending approvers', () => {
215
- // Test pending_count calculation
216
- });
217
- test('should indicate can_proceed correctly', () => {
218
- // Test can_proceed flag based on approval status
219
- });
220
- });
221
- describe('canProceedWithRollback', () => {
222
- test('should allow proceed when approved', () => {
223
- // Test that approved requests can proceed
224
- });
225
- test('should block proceed when rejected', () => {
226
- // Test that rejected requests block rollback
227
- });
228
- test('should block proceed when still pending', () => {
229
- // Test that pending requests block rollback
230
- });
231
- test('should return false for invalid request', () => {
232
- // Test error handling
233
- });
234
- });
235
- describe('Edge cases', () => {
236
- test('should handle empty route list', () => {
237
- // Test behavior when no routes are configured
238
- });
239
- test('should handle invalid condition JSON', () => {
240
- // Test that malformed conditions are skipped
241
- });
242
- test('should handle empty approver list', () => {
243
- // Test routes with no assigned approvers
244
- });
245
- test('should handle concurrent approval decisions', () => {
246
- // Test race condition handling
247
- });
248
- test('should handle very large approver lists', () => {
249
- // Test performance with many approvers
250
- });
251
- test('should handle sessions with no actions', () => {
252
- // Test edge case of empty sessions
253
- });
254
- test('should handle special characters in approver IDs', () => {
255
- // Test that special chars don't break JSON parsing
256
- });
257
- test('should handle timezone differences correctly', () => {
258
- // Test that timestamps are compared correctly
259
- });
260
- });
261
- describe('Integration scenarios', () => {
262
- test('should flow: create request → multiple approvals → proceed', () => {
263
- // Full integration test of approval flow
264
- });
265
- test('should flow: create request → one reject → block', () => {
266
- // Full integration test of rejection flow
267
- });
268
- test('should flow: route matching → request creation → status check', () => {
269
- // End-to-end test of approval system
270
- });
271
- test('should handle mixed approval and rejection', () => {
272
- // Test partial approvals followed by rejection
273
- });
274
- test('should track approval history', () => {
275
- // Test that all decisions are recorded
276
- });
277
- });
278
- describe('Performance', () => {
279
- test('should determine approvers efficiently with many routes', () => {
280
- // Test with 100+ routes
281
- const startTime = Date.now();
282
- // ... test code ...
283
- const endTime = Date.now();
284
- expect(endTime - startTime).toBeLessThan(100); // Should complete in <100ms
285
- });
286
- test('should handle many concurrent approval submissions', () => {
287
- // Test with simulated concurrent requests
288
- });
289
- test('should not exponentially grow with action count', () => {
290
- // Test performance scaling with large sessions
291
- });
292
- });
293
- });
294
- describe('Approval Route Matching', () => {
295
- test('all_sessions should match any session', () => {
296
- // Every session matches this condition
297
- expect(true).toBe(true);
298
- });
299
- test('tool_name should match specific tools', () => {
300
- // Should match write_file, bash, etc.
301
- expect(true).toBe(true);
302
- });
303
- test('action_count should use threshold correctly', () => {
304
- // Sessions with 5+ actions should match with threshold 5
305
- expect(true).toBe(true);
306
- });
307
- test('risk_level should identify irreversible operations', () => {
308
- // Actions with is_reversible=0 should trigger risk_level rules
309
- expect(true).toBe(true);
310
- });
311
- test('unknown condition types should not match', () => {
312
- // Invalid/unknown condition types should silently not match
313
- expect(true).toBe(true);
314
- });
315
- });
@@ -1,172 +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
- // We use an in-memory/temp DB for each test by setting env vars before import
40
- let tmpDir;
41
- // ─── DB + handler helpers ─────────────────────────────────────────────────────
42
- function setupTestDb() {
43
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waymark-approval-'));
44
- process.env.WAYMARK_PROJECT_ROOT = tmpDir;
45
- process.env.WAYMARK_DB_PATH = path.join(tmpDir, 'test.db');
46
- }
47
- function teardownTestDb() {
48
- fs.rmSync(tmpDir, { recursive: true, force: true });
49
- delete process.env.WAYMARK_PROJECT_ROOT;
50
- delete process.env.WAYMARK_DB_PATH;
51
- jest.resetModules();
52
- }
53
- // ─── approvePendingAction ─────────────────────────────────────────────────────
54
- describe('approvePendingAction', () => {
55
- beforeEach(() => {
56
- setupTestDb();
57
- });
58
- afterEach(() => {
59
- teardownTestDb();
60
- });
61
- it('returns error when action does not exist', async () => {
62
- const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
63
- const result = await approvePendingAction('nonexistent-id');
64
- expect(result.success).toBe(false);
65
- expect(result.error).toMatch(/not found/i);
66
- });
67
- it('returns error when action is not pending', async () => {
68
- const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
69
- const { insertAction, updateAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
70
- insertAction({
71
- action_id: 'already-done',
72
- session_id: 'sess1',
73
- tool_name: 'write_file',
74
- input_payload: JSON.stringify({ path: '/tmp/x.txt', content: 'hello' }),
75
- status: 'success',
76
- decision: 'allow',
77
- });
78
- updateAction('already-done', { status: 'success' });
79
- const result = await approvePendingAction('already-done');
80
- expect(result.success).toBe(false);
81
- expect(result.error).toMatch(/not pending/i);
82
- });
83
- it('blocks approval when policy has since tightened', async () => {
84
- // Write a config that blocks the target path
85
- const blockedConfig = {
86
- version: '1',
87
- policies: {
88
- allowedPaths: [],
89
- blockedPaths: [path.join(tmpDir, 'secret.txt')],
90
- blockedCommands: [],
91
- requireApproval: [],
92
- maxBashOutputBytes: 10000,
93
- },
94
- };
95
- fs.writeFileSync(path.join(tmpDir, 'waymark.config.json'), JSON.stringify(blockedConfig));
96
- const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
97
- const { insertAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
98
- insertAction({
99
- action_id: 'write-secret',
100
- session_id: 'sess2',
101
- tool_name: 'write_file',
102
- input_payload: JSON.stringify({ path: path.join(tmpDir, 'secret.txt'), content: 'data' }),
103
- status: 'pending',
104
- decision: 'pending',
105
- });
106
- const result = await approvePendingAction('write-secret');
107
- expect(result.success).toBe(false);
108
- expect(result.error).toMatch(/policy changed/i);
109
- });
110
- it('returns error for unsupported tool type', async () => {
111
- const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
112
- const { insertAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
113
- insertAction({
114
- action_id: 'bash-pending',
115
- session_id: 'sess3',
116
- tool_name: 'bash',
117
- input_payload: JSON.stringify({ command: 'ls' }),
118
- status: 'pending',
119
- decision: 'pending',
120
- });
121
- const result = await approvePendingAction('bash-pending');
122
- expect(result.success).toBe(false);
123
- expect(result.error).toMatch(/unsupported tool/i);
124
- });
125
- });
126
- // ─── rejectPendingAction ─────────────────────────────────────────────────────
127
- describe('rejectPendingAction', () => {
128
- beforeEach(() => {
129
- setupTestDb();
130
- });
131
- afterEach(() => {
132
- teardownTestDb();
133
- });
134
- it('returns error when action does not exist', async () => {
135
- const { rejectPendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
136
- const result = await rejectPendingAction('ghost-id', 'no reason');
137
- expect(result.success).toBe(false);
138
- expect(result.error).toMatch(/not found/i);
139
- });
140
- it('returns error when action is already rejected', async () => {
141
- const { rejectPendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
142
- const { insertAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
143
- insertAction({
144
- action_id: 'already-rejected',
145
- session_id: 'sess4',
146
- tool_name: 'write_file',
147
- input_payload: JSON.stringify({ path: '/tmp/x.txt', content: 'x' }),
148
- status: 'rejected',
149
- decision: 'rejected',
150
- });
151
- const result = await rejectPendingAction('already-rejected', 'again');
152
- expect(result.success).toBe(false);
153
- expect(result.error).toMatch(/not pending/i);
154
- });
155
- it('successfully rejects a pending action', async () => {
156
- const { rejectPendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
157
- const { insertAction, getAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
158
- insertAction({
159
- action_id: 'to-reject',
160
- session_id: 'sess5',
161
- tool_name: 'write_file',
162
- input_payload: JSON.stringify({ path: '/tmp/y.txt', content: 'y' }),
163
- status: 'pending',
164
- decision: 'pending',
165
- });
166
- const result = await rejectPendingAction('to-reject', 'user said no');
167
- expect(result.success).toBe(true);
168
- const row = getAction('to-reject');
169
- expect(row?.status).toBe('rejected');
170
- expect(row?.rejected_reason).toBe('user said no');
171
- });
172
- });