actual-mcp-server 0.22.40 → 0.22.42

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
@@ -836,7 +836,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
836
836
 
837
837
  ---
838
838
 
839
- **Version:** 0.22.40 | **Tool Count:** 84 (verified LibreChat-compatible)
839
+ **Version:** 0.22.42 | **Tool Count:** 84 (verified LibreChat-compatible)
840
840
 
841
841
  ## Sponsor
842
842
 
@@ -52,11 +52,35 @@ logger.info(`[ADAPTER] Budget registry: ${budgetRegistry.size} budget(s) — ` +
52
52
  * stdio mode, startup health checks) fall back to the env-default budget
53
53
  * (first entry in budgetRegistry).
54
54
  *
55
- * Lifecycle: entries are removed on session close via session_close.ts and
56
- * implicitly when connectionPool.shutdownConnection runs (switchBudget calls
57
- * it to drop the stale pool entry bound to the previous syncId).
55
+ * Lifecycle (#564): entries are released on EVERY session end. The HTTP
56
+ * transport's teardownSession (idle eviction, the #530 orphan sweep, a client
57
+ * DELETE) calls clearSessionBudgetState, and actual_session_close does too (it
58
+ * also covers stdio). connectionPool.shutdownConnection does NOT release them:
59
+ * switchBudget calls it to drop the stale pool entry and must keep the selection.
60
+ *
61
+ * Every write goes through setSessionBudget, which skips a session the transport
62
+ * layer no longer holds, so a switch or a #189 restore that was in flight across
63
+ * the teardown cannot re-create the entry after it was released.
58
64
  */
59
65
  const sessionBudgetState = new Map();
66
+ /**
67
+ * #564: is this session still held by its transport? The HTTP server registers
68
+ * `transports.has`; the default (stdio, unit tests, scripts) treats every session
69
+ * as live, which is the behaviour before #564.
70
+ */
71
+ let isSessionLive = () => true;
72
+ /** #563: why a queued write whose session ended before it ran was refused. */
73
+ const SESSION_ENDED_BEFORE_WRITE = 'The session ended before this write ran, so it was not applied. Start a new session and retry.';
74
+ export function setSessionLivenessCheck(check) {
75
+ isSessionLive = check;
76
+ }
77
+ function setSessionBudget(sessionId, key) {
78
+ if (!isSessionLive(sessionId)) {
79
+ logger.debug(`[ADAPTER] Session ${sessionId} ended before its budget selection was committed; not recording it`);
80
+ return;
81
+ }
82
+ sessionBudgetState.set(sessionId, key);
83
+ }
60
84
  function getActiveBudgetConfig() {
61
85
  // _resolveSessionId is declared below; function declarations hoist so this
62
86
  // call works at runtime. If we're not in any requestContext.run scope (stdio,
@@ -78,7 +102,7 @@ function getActiveBudgetConfig() {
78
102
  // Memoize into the session slot so subsequent calls take the fast path above.
79
103
  const restored = pickAllowedPreferredBudget(getPreferredBudgetSyncId(store?.principal), store?.allowedBudgets, [...budgetRegistry.values()]);
80
104
  if (restored) {
81
- sessionBudgetState.set(sessionId, restored.name.toLowerCase());
105
+ setSessionBudget(sessionId, restored.name.toLowerCase());
82
106
  return restored;
83
107
  }
84
108
  }
@@ -1136,6 +1160,17 @@ async function processWriteQueue() {
1136
1160
  'so it was failed immediately rather than waiting for the same bound. Retry it.', { cause: stuckLoadError }));
1137
1161
  continue;
1138
1162
  }
1163
+ // #563: the session ended (a client DELETE, an eviction) after this write was queued.
1164
+ // Teardown released its budget selection (#564), so its store would now resolve to the
1165
+ // default budget, one the session never chose and its principal may not be allowed.
1166
+ // Refuse instead. Outside the try on purpose, like the fail-fast above: the write never
1167
+ // ran, so it must not reach the pool-drop classification. A plain Error, not a typed
1168
+ // refusal: those mean the caller's input was wrong, and this one is not (gate round 2,
1169
+ // tool-author lens). The check is repeated just before the operation runs, below.
1170
+ if (entry.sessionId && !isSessionLive(entry.sessionId)) {
1171
+ reject(new Error(SESSION_ENDED_BEFORE_WRITE));
1172
+ continue;
1173
+ }
1139
1174
  try {
1140
1175
  // #390 round 2: run each op in the context it was ENQUEUED in.
1141
1176
  //
@@ -1181,6 +1216,17 @@ async function processWriteQueue() {
1181
1216
  throw waitErr;
1182
1217
  }
1183
1218
  await ensureLoadedBudgetMatchesSession();
1219
+ // #563 round 3: the check above ran before the awaits in this block. Holding the
1220
+ // api lock does not hold teardown off: with the pool disabled nothing in teardown
1221
+ // takes the lock, so the session can end (and its budget selection be released)
1222
+ // while this op waits on an abandoned load, and the precondition would then have
1223
+ // loaded the default budget. Re-check last, immediately before the write, with nothing
1224
+ // awaited in between. Accepted: a session that ended during the wait may cost one
1225
+ // wasted default-budget load, which is harmless because the write is still refused.
1226
+ // The error matches no pool-drop pattern, so the catch below only logs and rejects it.
1227
+ if (entry.sessionId && !isSessionLive(entry.sessionId)) {
1228
+ throw new Error(SESSION_ENDED_BEFORE_WRITE);
1229
+ }
1184
1230
  // #407: an operation may carry its OWN bound. Only a budget import does today, and
1185
1231
  // it needs one: the import is legitimately long, and bounding it at the general
1186
1232
  // operation timeout abandons it while every other session is already waiting on it
@@ -3859,7 +3905,7 @@ export async function switchBudget(name) {
3859
3905
  (currentEntry.encryptionPassword ?? '') === (found.encryptionPassword ?? '');
3860
3906
  if (sameAuth && currentEntry.syncId === found.syncId) {
3861
3907
  // No-op: already on this exact budget. Keep session map consistent and return.
3862
- sessionBudgetState.set(sessionId, key);
3908
+ setSessionBudget(sessionId, key);
3863
3909
  setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
3864
3910
  logger.info(`[ADAPTER] switchBudget no-op for session ${sessionId}: already on "${found.name}" (${found.syncId})`);
3865
3911
  return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
@@ -3879,7 +3925,7 @@ export async function switchBudget(name) {
3879
3925
  });
3880
3926
  }
3881
3927
  connectionPool.updateLoadedSyncId(sessionId, found.syncId);
3882
- sessionBudgetState.set(sessionId, key);
3928
+ setSessionBudget(sessionId, key);
3883
3929
  setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
3884
3930
  logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${found.serverUrl}`);
3885
3931
  return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
@@ -3895,7 +3941,7 @@ export async function switchBudget(name) {
3895
3941
  }
3896
3942
  // Update the per-session active-budget slot. Subsequent getActiveBudgetConfig
3897
3943
  // calls for this session now return the new budget.
3898
- sessionBudgetState.set(sessionId, key);
3944
+ setSessionBudget(sessionId, key);
3899
3945
  setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
3900
3946
  // Materialise a fresh pool entry bound to the new budget. Without this, the
3901
3947
  // next withActualApi call would find no pool entry and fall back to the
@@ -3938,9 +3984,10 @@ export async function switchBudget(name) {
3938
3984
  return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
3939
3985
  }
3940
3986
  /**
3941
- * Clear the per-session budget state for a session. Called from
3942
- * session_close so the per-session map does not accumulate stale entries
3943
- * after a session ends. See #156.
3987
+ * Clear the per-session budget state for a session, so the map does not
3988
+ * accumulate entries for ended sessions (#156). Called from the HTTP
3989
+ * transport's teardownSession on every session end (#564) and from
3990
+ * session_close, which also covers stdio.
3944
3991
  */
3945
3992
  export function clearSessionBudgetState(sessionId) {
3946
3993
  sessionBudgetState.delete(sessionId);
@@ -9,6 +9,8 @@ import { getLocalIp } from '../utils.js';
9
9
  import actualToolsManager from '../actualToolsManager.js';
10
10
  import { getConnectionState, connectToActualForSession, shutdownActualForSession, shutdownActual, canAcceptNewSession, isConnectionPoolEnabled } from '../actualConnection.js';
11
11
  import { connectionPool } from '../lib/ActualConnectionPool.js';
12
+ import { clearSessionBudgetState, setSessionLivenessCheck } from '../lib/actual-adapter.js';
13
+ import { errorLogFields } from '../lib/errors.js';
12
14
  import observability from '../observability.js';
13
15
  import config from '../config.js';
14
16
  import { createRemoteJWKSet, jwtVerify } from 'jose';
@@ -299,6 +301,9 @@ advertisedUrl) {
299
301
  // window without a served request, with reason `orphaned`; same teardown. The
300
302
  // body is synchronous on purpose: the pool walks its registrations in one pass.
301
303
  connectionPool.onSessionEvicted(teardownSession);
304
+ // #564: a session's selected budget lives as long as its transport. A selection
305
+ // committed after teardown (a switch in flight across it) is dropped by the adapter.
306
+ setSessionLivenessCheck((id) => transports.has(id));
302
307
  function teardownSession(sessionId, reason) {
303
308
  const transport = transports.get(sessionId);
304
309
  if (transport) {
@@ -312,6 +317,7 @@ advertisedUrl) {
312
317
  transports.delete(sessionId);
313
318
  sessionInitPromises.delete(sessionId);
314
319
  forgetSessionOwner(sessionId);
320
+ clearSessionBudgetState(sessionId);
315
321
  connectionPool.forgetSession(sessionId);
316
322
  logger.info(`[SESSION] Transport torn down for ${reason} session: ${sessionId}`);
317
323
  }
@@ -521,6 +527,14 @@ advertisedUrl) {
521
527
  const transport = new StreamableHTTPServerTransport({
522
528
  sessionIdGenerator: () => randomUUID(),
523
529
  enableJsonResponse: true,
530
+ // #563: the SDK awaits this on a valid DELETE, BEFORE it answers 200, so the pool
531
+ // slot is free by the time the client hears the session ended. It waits for the api
532
+ // mutex (#392), so another session's call in flight finishes first. Without evict:
533
+ // the DELETE route tears the session down itself after the 200.
534
+ onsessionclosed: async (sid) => {
535
+ if (isConnectionPoolEnabled())
536
+ await connectionPool.shutdownConnection(sid);
537
+ },
524
538
  onsessioninitialized: async (sid) => {
525
539
  logger.debug(`Session initialized: ${sid}`);
526
540
  // Store the promise before starting initialization
@@ -826,6 +840,69 @@ advertisedUrl) {
826
840
  connectionPool.touch(sessionId); // Refresh the pool's idle clock (#167)
827
841
  await transport.handleRequest(req, res);
828
842
  });
843
+ // #563: DELETE ends the session (MCP spec 2025-11-25, Session Management rule 5).
844
+ // Same gate, missing-id answer and ownership rule as POST; the SDK then validates
845
+ // MCP-Protocol-Version, runs onsessionclosed (which frees the pool slot, see the
846
+ // transport options), answers 200 and closes its transport. Only after a 200 do we
847
+ // tear the session down ourselves: a 400 for an unsupported version leaves the
848
+ // session open, as the SDK does. teardownSession is called directly rather than
849
+ // through the pool's eviction listener because the listener never fires with the
850
+ // pool disabled or with the entry already dropped (#530), which would leave the
851
+ // transport, owner and budget selection behind. The pool entry is shut down
852
+ // WITHOUT evict so the teardown runs once, logged as `closed`.
853
+ const sessionNotFound = { jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Session not found. Please re-initialize by calling initialize without mcp-session-id header.' } };
854
+ app.delete(httpPath, async (req, res) => {
855
+ if (!authenticateRequest(req, res)) {
856
+ return;
857
+ }
858
+ const sessionId = req.headers['mcp-session-id'];
859
+ if (!sessionId) {
860
+ res.status(400).json({ jsonrpc: '2.0', id: null, error: { code: -32000, message: 'Bad Request: Mcp-Session-Id header is required' } });
861
+ return;
862
+ }
863
+ try {
864
+ let transport = transports.get(sessionId);
865
+ const initPromise = transport ? undefined : sessionInitPromises.get(sessionId);
866
+ if (initPromise) {
867
+ // A failed init is answered like an unknown id; its #438 record is peek-only and stays.
868
+ await initPromise.catch(() => undefined);
869
+ transport = transports.get(sessionId);
870
+ }
871
+ // A non-owner gets the unknown-id answer byte for byte (#529), so the reply never
872
+ // confirms that the id exists.
873
+ if (!transport || !sessionOwnerMatches(sessionId, resolvePrincipal(req))) {
874
+ if (transport)
875
+ logger.warn(`[SESSION] Refused a DELETE for session ${sessionId} from a principal that does not own it`);
876
+ res.status(404).json(sessionNotFound);
877
+ return;
878
+ }
879
+ const allowedBudgets = req.allowedBudgets;
880
+ const requestId = resolveRequestId(req.get('x-correlation-id'));
881
+ await requestContext.run({ sessionId, requestId, allowedBudgets, principal: resolvePrincipal(req) }, async () => {
882
+ await transport.handleRequest(req, res);
883
+ });
884
+ if (res.statusCode !== 200)
885
+ return;
886
+ teardownSession(sessionId, 'closed');
887
+ }
888
+ catch (err) {
889
+ logger.error(`[SESSION] DELETE for session ${sessionId} failed`, errorLogFields(err));
890
+ if (!res.headersSent) {
891
+ res.status(500).json({ jsonrpc: '2.0', id: null, error: { code: -32603, message: 'Internal error. The server logged the cause.' } });
892
+ }
893
+ }
894
+ });
895
+ // #563: every other method on the MCP path gets a JSON-RPC 405 with the Allow header
896
+ // RFC 9110 15.5.6 requires, instead of Express's HTML 404. HEAD is served by the GET
897
+ // route; OPTIONS answers 204 with the same Allow list.
898
+ app.all(httpPath, (req, res) => {
899
+ res.setHeader('Allow', 'GET, POST, DELETE');
900
+ if (req.method === 'OPTIONS') {
901
+ res.status(204).end();
902
+ return;
903
+ }
904
+ res.status(405).json({ jsonrpc: '2.0', id: null, error: { code: -32000, message: 'Method not allowed.' } });
905
+ });
829
906
  // Quick GET server-info probe used by our test harness (testMcpClient.ts, the
830
907
  // Playwright E2E) to check the server is up and advertises tools before the full
831
908
  // JSON-RPC handshake. #286: this used to live on /.well-known/oauth-protected-resource
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.22.40",
4
+ "version": "0.22.42",
5
5
  "engines": {
6
6
  "node": ">=22.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/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.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/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.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/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.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/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.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/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.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/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/budget_acl_parse.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.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/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.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/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.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/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.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/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.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/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.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",