actual-mcp-server 0.6.14 → 0.6.15
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
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.15",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
31
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
32
|
"direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
|
|
33
|
-
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js",
|
|
34
34
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
35
|
"test:e2e": "npx playwright test",
|
|
36
36
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|
|
@@ -130,7 +130,12 @@ export async function shutdownActualForSession(sessionId) {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
try {
|
|
133
|
-
|
|
133
|
+
// evict: true so the pool tears down the matching httpServer transport too
|
|
134
|
+
// (#167). This wrapper is only used for session-ending events (explicit
|
|
135
|
+
// session_close, server shutdown); the adapter's switchBudget / infra-drop
|
|
136
|
+
// paths call connectionPool.shutdownConnection directly without evict so the
|
|
137
|
+
// transport survives a budget switch or transient error.
|
|
138
|
+
await connectionPool.shutdownConnection(sessionId, { evict: true });
|
|
134
139
|
logger.info(`Actual API connection shutdown for session: ${sessionId}`);
|
|
135
140
|
}
|
|
136
141
|
catch (err) {
|
|
@@ -23,9 +23,16 @@ class ActualConnectionPool {
|
|
|
23
23
|
cleanupInterval = null;
|
|
24
24
|
IDLE_TIMEOUT; // Configurable via SESSION_IDLE_TIMEOUT_MINUTES env var (default: 5 minutes)
|
|
25
25
|
CLEANUP_INTERVAL; // Check frequency (default: 2 minutes)
|
|
26
|
-
MAX_CONCURRENT_SESSIONS; // Configurable via MAX_CONCURRENT_SESSIONS env var (default:
|
|
26
|
+
MAX_CONCURRENT_SESSIONS; // Configurable via MAX_CONCURRENT_SESSIONS env var (default: 15)
|
|
27
27
|
sharedConnection = null;
|
|
28
28
|
initializationPromise = null;
|
|
29
|
+
// Eviction listeners. The pool is the single source of truth for session
|
|
30
|
+
// liveness and idle timing (#167); when it removes a session it notifies the
|
|
31
|
+
// transport layer (httpServer) so the transport object is torn down in the
|
|
32
|
+
// same window. This is callback-based eager teardown, not lazy query-on-demand:
|
|
33
|
+
// a lazily-cleaned table would leak transport objects for sessions a client
|
|
34
|
+
// abandons without reconnecting.
|
|
35
|
+
evictionCallbacks = [];
|
|
29
36
|
constructor() {
|
|
30
37
|
// Read from environment variable or default to 15
|
|
31
38
|
// @actual-app/api is a singleton, so concurrent sessions cause conflicts
|
|
@@ -76,6 +83,61 @@ class ActualConnectionPool {
|
|
|
76
83
|
const conn = this.connections.get(sessionId);
|
|
77
84
|
return conn?.initialized ?? false;
|
|
78
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Single source of truth for session liveness (#167). Returns true only if
|
|
88
|
+
* the session has an initialized connection that has not passed the idle
|
|
89
|
+
* timeout. Returns false for unknown or expired sessions, and never creates
|
|
90
|
+
* an entry as a side effect. httpServer uses this as a per-request defensive
|
|
91
|
+
* guard against the race where the idle sweep evicts a session while a
|
|
92
|
+
* request for it is already in flight.
|
|
93
|
+
*/
|
|
94
|
+
isLive(sessionId) {
|
|
95
|
+
const conn = this.connections.get(sessionId);
|
|
96
|
+
if (!conn || !conn.initialized)
|
|
97
|
+
return false;
|
|
98
|
+
return !this.isExpired(conn);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Single definition of "past the idle window". Shared by isLive and the idle
|
|
102
|
+
* sweep so the two can never disagree about where the boundary is (#167).
|
|
103
|
+
*/
|
|
104
|
+
isExpired(conn) {
|
|
105
|
+
return (Date.now() - conn.lastActivity) > this.IDLE_TIMEOUT;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Refresh a session's activity timestamp. Called by the transport layer on
|
|
109
|
+
* every request so the pool's idle clock reflects real usage (#167). Before
|
|
110
|
+
* this, the pool only stamped lastActivity at init/switch, so an actively
|
|
111
|
+
* used session's pool clock never advanced; httpServer kept a parallel
|
|
112
|
+
* activity map to compensate, which is the drift this consolidation removes.
|
|
113
|
+
* No-op for unknown sessions (does NOT create an entry).
|
|
114
|
+
*/
|
|
115
|
+
touch(sessionId) {
|
|
116
|
+
const conn = this.connections.get(sessionId);
|
|
117
|
+
if (conn) {
|
|
118
|
+
conn.lastActivity = Date.now();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Register a listener invoked when the pool removes a session (idle sweep or
|
|
123
|
+
* explicit close). httpServer registers one that closes the transport and
|
|
124
|
+
* drops its table entries, keeping both tables consistent (#167). Listeners
|
|
125
|
+
* must not throw; any error is logged and swallowed so one bad listener
|
|
126
|
+
* cannot abort the removal of others.
|
|
127
|
+
*/
|
|
128
|
+
onSessionEvicted(cb) {
|
|
129
|
+
this.evictionCallbacks.push(cb);
|
|
130
|
+
}
|
|
131
|
+
fireEviction(sessionId) {
|
|
132
|
+
for (const cb of this.evictionCallbacks) {
|
|
133
|
+
try {
|
|
134
|
+
cb(sessionId);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
logger.error(`[ConnectionPool] Eviction listener threw for session ${sessionId} (ignoring):`, err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
79
141
|
/**
|
|
80
142
|
* Check if we can accept a new session (under the concurrent limit)
|
|
81
143
|
* Returns true if limit not reached, false otherwise
|
|
@@ -243,9 +305,17 @@ class ActualConnectionPool {
|
|
|
243
305
|
}
|
|
244
306
|
}
|
|
245
307
|
/**
|
|
246
|
-
* Shutdown connection for a specific session
|
|
308
|
+
* Shutdown connection for a specific session.
|
|
309
|
+
*
|
|
310
|
+
* `opts.evict` controls whether eviction listeners fire (#167). Pass `true`
|
|
311
|
+
* when the session itself is ending (idle sweep, explicit session close) so
|
|
312
|
+
* the transport layer tears down its transport. Leave it false (default) when
|
|
313
|
+
* the pool entry is being recycled but the MCP session continues, e.g.
|
|
314
|
+
* switchBudget's slow path which shuts the entry down and immediately
|
|
315
|
+
* recreates it, or an infra-error drop where the next request re-establishes
|
|
316
|
+
* the connection: in those cases the transport must survive.
|
|
247
317
|
*/
|
|
248
|
-
async shutdownConnection(sessionId) {
|
|
318
|
+
async shutdownConnection(sessionId, opts = {}) {
|
|
249
319
|
const conn = this.connections.get(sessionId);
|
|
250
320
|
if (!conn || !conn.initialized) {
|
|
251
321
|
return;
|
|
@@ -256,17 +326,23 @@ class ActualConnectionPool {
|
|
|
256
326
|
if (typeof maybeApi.shutdown === 'function') {
|
|
257
327
|
await maybeApi.shutdown();
|
|
258
328
|
}
|
|
259
|
-
conn.initialized = false;
|
|
260
|
-
this.connections.delete(sessionId);
|
|
261
|
-
setApiInitialized(false);
|
|
262
|
-
// NOTE: We do NOT delete the data directory because it's shared across all sessions
|
|
263
|
-
// Deleting it would cause data loss for other active sessions
|
|
264
329
|
logger.info(`[ConnectionPool] Connection shutdown complete for session: ${sessionId}`);
|
|
265
330
|
}
|
|
266
331
|
catch (err) {
|
|
267
332
|
logger.error(`[ConnectionPool] Error shutting down connection for session ${sessionId}:`, err);
|
|
268
|
-
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
// Remove the entry in all cases so liveness can never report a session
|
|
336
|
+
// alive after its shutdown was attempted. On error the singleton is in
|
|
337
|
+
// an unknown state, so we must not leave it reusable either.
|
|
338
|
+
conn.initialized = false;
|
|
339
|
+
this.connections.delete(sessionId);
|
|
269
340
|
setApiInitialized(false);
|
|
341
|
+
// NOTE: We do NOT delete the data directory because it's shared across all sessions
|
|
342
|
+
// Deleting it would cause data loss for other active sessions
|
|
343
|
+
if (opts.evict) {
|
|
344
|
+
this.fireEviction(sessionId);
|
|
345
|
+
}
|
|
270
346
|
}
|
|
271
347
|
}
|
|
272
348
|
/**
|
|
@@ -335,17 +411,20 @@ class ActualConnectionPool {
|
|
|
335
411
|
* Clean up idle connections that haven't been used recently
|
|
336
412
|
*/
|
|
337
413
|
async cleanupIdleConnections() {
|
|
338
|
-
const now = Date.now();
|
|
339
414
|
const connectionsToRemove = [];
|
|
340
415
|
for (const [sessionId, conn] of this.connections.entries()) {
|
|
341
|
-
if (
|
|
416
|
+
if (this.isExpired(conn)) {
|
|
342
417
|
connectionsToRemove.push(sessionId);
|
|
343
418
|
}
|
|
344
419
|
}
|
|
345
420
|
if (connectionsToRemove.length > 0) {
|
|
346
421
|
logger.info(`[ConnectionPool] Cleaning up ${connectionsToRemove.length} idle connections`);
|
|
347
422
|
for (const sessionId of connectionsToRemove) {
|
|
348
|
-
|
|
423
|
+
// evict: true so the transport layer tears down the matching transport
|
|
424
|
+
// in the same window (#167). This is the path that previously drifted:
|
|
425
|
+
// the pool's autonomous timer removed an entry httpServer's separate
|
|
426
|
+
// timer knew nothing about.
|
|
427
|
+
await this.shutdownConnection(sessionId, { evict: true });
|
|
349
428
|
}
|
|
350
429
|
}
|
|
351
430
|
}
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @actual-app/api v26.3.0 introduced `navigator.platform` usage at the module
|
|
5
5
|
* top level (inside the Electron/browser bundle) which crashes on Node.js with
|
|
6
|
-
* `ReferenceError: navigator is not defined`.
|
|
6
|
+
* `ReferenceError: navigator is not defined`. v26.6.0 additionally reads
|
|
7
|
+
* `navigator.userAgent` at the top level (`navigator.userAgent.includes(...)`
|
|
8
|
+
* plus a UAParser call), so the polyfill must define `userAgent` too or the
|
|
9
|
+
* bundle throws `Cannot read properties of undefined (reading 'includes')`.
|
|
10
|
+
*
|
|
11
|
+
* Node 21+ ships a native `navigator` that already carries both fields, so this
|
|
12
|
+
* polyfill only fires on Node 20 (still inside the documented "Node.js 20+"
|
|
13
|
+
* support range, and the version several CI workflows run on).
|
|
7
14
|
*
|
|
8
15
|
* This file must be imported BEFORE any `@actual-app/api` import so that the
|
|
9
16
|
* global is defined when the bundle is first evaluated.
|
|
@@ -12,6 +19,7 @@ if (typeof globalThis.navigator === 'undefined') {
|
|
|
12
19
|
Object.defineProperty(globalThis, 'navigator', {
|
|
13
20
|
value: {
|
|
14
21
|
platform: process.platform === 'win32' ? 'Win32' : 'Linux',
|
|
22
|
+
userAgent: `Node.js/${process.version}`,
|
|
15
23
|
},
|
|
16
24
|
writable: true,
|
|
17
25
|
configurable: true,
|
|
@@ -7,6 +7,7 @@ import logger from '../logger.js';
|
|
|
7
7
|
import { getLocalIp } from '../utils.js';
|
|
8
8
|
import actualToolsManager from '../actualToolsManager.js';
|
|
9
9
|
import { getConnectionState, connectToActualForSession, shutdownActualForSession, shutdownActual, canAcceptNewSession } from '../actualConnection.js';
|
|
10
|
+
import { connectionPool } from '../lib/ActualConnectionPool.js';
|
|
10
11
|
import observability from '../observability.js';
|
|
11
12
|
import config from '../config.js';
|
|
12
13
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
@@ -75,31 +76,30 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
const transports = new Map();
|
|
78
|
-
const sessionLastActivity = new Map();
|
|
79
79
|
const sessionInitPromises = new Map(); // Track session init completion
|
|
80
|
-
// Use same timeout as ConnectionPool (SESSION_IDLE_TIMEOUT_MINUTES env var, default: 2 minutes)
|
|
81
|
-
const idleTimeoutMinutes = parseInt(process.env.SESSION_IDLE_TIMEOUT_MINUTES || '2', 10);
|
|
82
|
-
const SESSION_TIMEOUT_MS = idleTimeoutMinutes * 60 * 1000;
|
|
83
|
-
const SESSION_CLEANUP_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
|
|
84
80
|
// safe fallback if index didn't provide implementedTools
|
|
85
81
|
const toolsList = Array.isArray(implementedTools) ? implementedTools : [];
|
|
86
|
-
// Session
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
// Session liveness and idle timing are owned solely by the connection pool
|
|
83
|
+
// (#167). httpServer no longer runs its own idle timer or activity map; it
|
|
84
|
+
// owns only the transport objects. When the pool removes a session (idle
|
|
85
|
+
// sweep or explicit close) it fires this eviction listener, which tears down
|
|
86
|
+
// the matching transport in the same window. This is what keeps the two
|
|
87
|
+
// tables from drifting: no "alive in httpServer, dead in the pool" state, and
|
|
88
|
+
// no transport object left behind for a session the client abandoned.
|
|
89
|
+
connectionPool.onSessionEvicted((sessionId) => {
|
|
90
|
+
const transport = transports.get(sessionId);
|
|
91
|
+
if (transport) {
|
|
92
|
+
try {
|
|
93
|
+
transport.close?.();
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger.debug(`[SESSION] Error closing transport for evicted session ${sessionId} (ignoring): ${err}`);
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
sessionInitPromises.delete(sessionId);
|
|
100
|
-
await shutdownActualForSession(sessionId);
|
|
101
|
-
}
|
|
102
|
-
}, SESSION_CLEANUP_INTERVAL_MS);
|
|
99
|
+
transports.delete(sessionId);
|
|
100
|
+
sessionInitPromises.delete(sessionId);
|
|
101
|
+
logger.info(`[SESSION] Transport torn down for evicted session: ${sessionId}`);
|
|
102
|
+
});
|
|
103
103
|
// Authentication middleware
|
|
104
104
|
const authenticateRequest = (req, res) => {
|
|
105
105
|
// OIDC mode: mcp-auth middleware has already validated the JWT and populated req.auth.
|
|
@@ -333,9 +333,10 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
333
333
|
// Initialize connection pool for this session
|
|
334
334
|
try {
|
|
335
335
|
await connectToActualForSession(sid);
|
|
336
|
-
// Only
|
|
336
|
+
// Only register the transport if the pool connection succeeded.
|
|
337
|
+
// The pool stamped lastActivity when it created the entry, so it
|
|
338
|
+
// is already the source of truth for this session's idle clock.
|
|
337
339
|
transports.set(sid, transport);
|
|
338
|
-
sessionLastActivity.set(sid, Date.now());
|
|
339
340
|
logger.info(`[SESSION] Actual connection initialized for session: ${sid}`);
|
|
340
341
|
resolveInit?.();
|
|
341
342
|
}
|
|
@@ -370,7 +371,18 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
370
371
|
}
|
|
371
372
|
return;
|
|
372
373
|
}
|
|
373
|
-
// sessionId present -> reuse
|
|
374
|
+
// sessionId present -> reuse. Transport presence is the liveness signal:
|
|
375
|
+
// the pool's eviction listener (#167) removes the transport the moment a
|
|
376
|
+
// session is genuinely evicted (idle sweep or explicit close), so a
|
|
377
|
+
// missing transport means "expired" and a present one means "serve it".
|
|
378
|
+
//
|
|
379
|
+
// We deliberately do NOT additionally gate on connectionPool.isLive() here.
|
|
380
|
+
// A pool entry can be absent while the MCP session is still perfectly
|
|
381
|
+
// usable: after a transient infra error the adapter drops the pool entry
|
|
382
|
+
// (without evicting the transport) so the next call re-establishes it via
|
|
383
|
+
// the legacy fallback. Rejecting those requests as "expired" would force a
|
|
384
|
+
// needless client re-initialize on every transient blip, re-introducing
|
|
385
|
+
// the session churn this server works to avoid.
|
|
374
386
|
let transport = transports.get(sessionId);
|
|
375
387
|
if (!transport) {
|
|
376
388
|
// Check if session is currently being initialized
|
|
@@ -431,8 +443,8 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
431
443
|
return;
|
|
432
444
|
}
|
|
433
445
|
}
|
|
434
|
-
//
|
|
435
|
-
|
|
446
|
+
// Refresh the pool's idle clock for this session (single source of truth, #167).
|
|
447
|
+
connectionPool.touch(sessionId);
|
|
436
448
|
// Run in AsyncLocalStorage context so tools and the adapter can access
|
|
437
449
|
// sessionId (pool branch, #134) and allowedBudgets (ACL enforcement, #156).
|
|
438
450
|
const allowedBudgets = req.allowedBudgets;
|
|
@@ -457,7 +469,7 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
457
469
|
res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'No session id' }, id: null });
|
|
458
470
|
return;
|
|
459
471
|
}
|
|
460
|
-
|
|
472
|
+
connectionPool.touch(sessionId); // Refresh the pool's idle clock (#167)
|
|
461
473
|
const transport = transports.get(sessionId);
|
|
462
474
|
if (!transport) {
|
|
463
475
|
res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Transport not ready' }, id: null });
|
|
@@ -538,12 +550,13 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
538
550
|
// Cleanup on server shutdown
|
|
539
551
|
const cleanup = async () => {
|
|
540
552
|
logger.info('[SERVER] Shutting down, cleaning up sessions...');
|
|
541
|
-
|
|
542
|
-
|
|
553
|
+
// Snapshot the keys: shutdownActualForSession evicts, and the eviction
|
|
554
|
+
// listener deletes from `transports`, so iterating the live map would
|
|
555
|
+
// mutate it mid-iteration.
|
|
556
|
+
for (const sessionId of [...transports.keys()]) {
|
|
543
557
|
await shutdownActualForSession(sessionId);
|
|
544
558
|
}
|
|
545
559
|
transports.clear();
|
|
546
|
-
sessionLastActivity.clear();
|
|
547
560
|
sessionInitPromises.clear();
|
|
548
561
|
// Also shut down the shared/pooled connections
|
|
549
562
|
await shutdownActual();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.15",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
31
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
32
|
"direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
|
|
33
|
-
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js",
|
|
34
34
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
35
|
"test:e2e": "npx playwright test",
|
|
36
36
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|