@straiffi/archon 1.0.12 → 1.1.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.
@@ -40,6 +40,7 @@ import { JiraPlanningReferenceError, resolveJiraPlanningDescription } from './li
40
40
  import { getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, upsertBundlePullRequest, } from './lib/bundlePullRequests.js';
41
41
  import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, validateRepoPath, } from './lib/projects.js';
42
42
  import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runProject, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopProjectRun, stopTicket } from './lib/run.js';
43
+ import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
43
44
  import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
44
45
  import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
45
46
  import { countBundleTickets, createTicketRecord, createBundle, deleteBundle, ensureProjectRootBundle, autoParkTicketIfStale, clearAutoParkDismissalIfNeeded, assignTicketDoneOrder, assignTicketLaneOrder, getBundle, getBundleByName, getBundleByBranch, getBundleRepresentativeTicketContext, isProjectRootBundle, getTicket, isBundledTicket, listTicketsInRunContext, listBundles, listBoardTicketEnrichment, listTickets, resolveTicketBranch, resolveTicketTool, } from './lib/tickets.js';
@@ -697,6 +698,121 @@ const resolveProjectTargetGitWorkspace = (project) => {
697
698
  }),
698
699
  };
699
700
  };
701
+ const stripRepeatedSelfSuffix = (label) => {
702
+ let nextLabel = label.trim();
703
+ while (nextLabel !== '') {
704
+ const match = nextLabel.match(/^(.*?) \(([^()]+)\)$/);
705
+ if (!match) {
706
+ return nextLabel;
707
+ }
708
+ const prefix = match[1]?.trim() ?? '';
709
+ const suffix = match[2]?.trim() ?? '';
710
+ const normalizedPrefix = prefix.toLowerCase();
711
+ const normalizedSuffix = suffix.toLowerCase();
712
+ if (normalizedPrefix === normalizedSuffix) {
713
+ nextLabel = prefix;
714
+ continue;
715
+ }
716
+ if (normalizedPrefix.endsWith(` (${normalizedSuffix})`)) {
717
+ nextLabel = prefix;
718
+ continue;
719
+ }
720
+ return nextLabel;
721
+ }
722
+ return label.trim();
723
+ };
724
+ const stripRepeatedBranchSuffix = (label, branch) => {
725
+ const trimmedLabel = stripRepeatedSelfSuffix(label);
726
+ const trimmedBranch = branch.trim();
727
+ if (trimmedLabel === '' || trimmedBranch === '') {
728
+ return trimmedLabel;
729
+ }
730
+ const suffix = ` (${trimmedBranch})`;
731
+ let nextLabel = trimmedLabel;
732
+ while (nextLabel.toLowerCase().endsWith(suffix.toLowerCase())) {
733
+ nextLabel = nextLabel.slice(0, -suffix.length).trimEnd();
734
+ }
735
+ return nextLabel || trimmedLabel;
736
+ };
737
+ const getBundleTestTargetLabel = (bundle) => {
738
+ const displayName = stripRepeatedSelfSuffix(bundle.name || bundle.branch || '');
739
+ return bundle.branch ? stripRepeatedBranchSuffix(displayName, bundle.branch) : displayName;
740
+ };
741
+ const resolveExplicitProjectTestWorkspace = (project, targetKind, targetBundleId) => {
742
+ if (targetKind === 'bundle') {
743
+ if (!targetBundleId) {
744
+ return { error: 'target_bundle_id is required for bundle test runs', status: 400 };
745
+ }
746
+ const bundle = getBundle(targetBundleId, project.id);
747
+ if (!bundle || isProjectRootBundle(bundle)) {
748
+ return { error: 'Selected bundle target no longer exists', status: 409 };
749
+ }
750
+ const cwd = resolveExistingWorktreePath(bundle.branch, project);
751
+ if (!cwd) {
752
+ return { error: 'Selected bundle target does not have a prepared worktree', status: 409 };
753
+ }
754
+ return {
755
+ project,
756
+ cwd,
757
+ target_kind: 'bundle',
758
+ target_bundle_id: bundle.id,
759
+ target_label: getBundleTestTargetLabel(bundle),
760
+ };
761
+ }
762
+ const workspace = resolveProjectRootGitWorkspace(project);
763
+ if ('error' in workspace) {
764
+ return workspace;
765
+ }
766
+ return {
767
+ project,
768
+ cwd: workspace.cwd,
769
+ target_kind: 'repo_root',
770
+ target_bundle_id: null,
771
+ target_label: `Repo root (${workspace.branch})`,
772
+ };
773
+ };
774
+ const resolveTicketTestWorkspace = (ticket) => {
775
+ const workspace = resolveTicketGitWorkspace(ticket);
776
+ if ('error' in workspace) {
777
+ return workspace;
778
+ }
779
+ const project = workspace.project;
780
+ if (ticket.worktree_bundle_id) {
781
+ const bundle = getBundle(ticket.worktree_bundle_id, project.id);
782
+ if (bundle && !isProjectRootBundle(bundle)) {
783
+ return {
784
+ project,
785
+ cwd: workspace.cwd,
786
+ target_kind: 'bundle',
787
+ target_bundle_id: bundle.id,
788
+ target_label: getBundleTestTargetLabel(bundle),
789
+ };
790
+ }
791
+ }
792
+ return {
793
+ project,
794
+ cwd: workspace.cwd,
795
+ target_kind: 'repo_root',
796
+ target_bundle_id: null,
797
+ target_label: `Repo root (${workspace.branch})`,
798
+ };
799
+ };
800
+ const parseTestTargetSelection = (value) => {
801
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !hasOwn(value, 'kind')) {
802
+ return null;
803
+ }
804
+ const selection = value;
805
+ if (selection.kind === 'all') {
806
+ return { kind: 'all' };
807
+ }
808
+ if (selection.kind === 'file' && typeof selection.path === 'string') {
809
+ return { kind: 'file', path: selection.path };
810
+ }
811
+ if (selection.kind === 'case' && typeof selection.path === 'string' && typeof selection.name === 'string') {
812
+ return { kind: 'case', path: selection.path, name: selection.name };
813
+ }
814
+ return null;
815
+ };
700
816
  const resolveProjectFileSuggestionWorkspace = (project, options = {}) => {
701
817
  const ticketId = options.ticketId?.trim() || null;
702
818
  if (ticketId) {
@@ -878,6 +994,16 @@ const bundlePullRequestDiscoveryErrorTimestamps = new Map();
878
994
  const bundlePullRequestSyncInFlight = new Map();
879
995
  const bundlePullRequestSyncErrorTimestamps = new Map();
880
996
  const getBundlePullRequestDiscoveryKey = (projectId, bundleId) => `${projectId}:${bundleId}`;
997
+ const startPassiveGitHubWork = (label, callback) => {
998
+ setImmediate(() => {
999
+ try {
1000
+ callback();
1001
+ }
1002
+ catch (error) {
1003
+ console.warn(`[integrations] github passive work failed ${label}`, error);
1004
+ }
1005
+ });
1006
+ };
881
1007
  const isBundlePullRequestDiscoveryCoolingDown = (timestamps, key, cooldownMs) => {
882
1008
  const lastAttemptAt = timestamps.get(key);
883
1009
  if (lastAttemptAt === undefined) {
@@ -2102,15 +2228,23 @@ app.get('/tickets/:id', async (req, res) => {
2102
2228
  if (!matchesTicketProjectContext(ticket, req)) {
2103
2229
  return res.status(404).json({ error: 'Not found' });
2104
2230
  }
2231
+ let passivePullRequestContext = null;
2105
2232
  if (ticket.project_id && ticket.worktree_bundle_id) {
2106
2233
  const project = getProjectById(ticket.project_id);
2107
2234
  const bundle = project ? getBundle(ticket.worktree_bundle_id, project.id) : null;
2108
2235
  if (project && bundle) {
2236
+ passivePullRequestContext = { project, bundle };
2237
+ }
2238
+ }
2239
+ res.json(ticket);
2240
+ if (passivePullRequestContext) {
2241
+ const { project, bundle } = passivePullRequestContext;
2242
+ startPassiveGitHubWork('ticket-open-pr-sync', () => {
2109
2243
  startTrackedBundlePullRequestSyncIfNeeded(project, bundle);
2110
2244
  startBundlePullRequestDiscoveryIfNeeded(project, bundle);
2111
- }
2245
+ });
2112
2246
  }
2113
- return res.json(ticket);
2247
+ return;
2114
2248
  });
2115
2249
  app.get('/tickets/:id/review-findings', (req, res) => {
2116
2250
  const ticket = getTicketRouteContext(req.params.id);
@@ -2186,12 +2320,13 @@ app.get('/bundles', async (req, res) => {
2186
2320
  timing.mark('ensure_project_root_bundle');
2187
2321
  const bundles = listBundles(result.project.id);
2188
2322
  timing.mark('list_bundles');
2189
- startProjectBundlePullRequestSyncIfNeeded(result.project, bundles);
2190
- timing.mark('start_pr_sync');
2191
2323
  const serializedBundles = bundles.map(bundle => serializeBundleRow(result.project.id, bundle));
2192
2324
  timing.mark('serialize_bundles');
2193
2325
  timing.end({ count: serializedBundles.length });
2194
2326
  res.json(serializedBundles);
2327
+ startPassiveGitHubWork('project-bundle-pr-sync', () => {
2328
+ startProjectBundlePullRequestSyncIfNeeded(result.project, bundles);
2329
+ });
2195
2330
  });
2196
2331
  app.get('/bundles/:id/details', async (req, res) => {
2197
2332
  const result = getRequiredProject(req);
@@ -2215,9 +2350,11 @@ app.get('/bundles/:id/conversation', (req, res) => {
2215
2350
  if (!bundle) {
2216
2351
  return res.status(404).json({ error: 'Bundle not found' });
2217
2352
  }
2218
- startBundlePullRequestDiscoveryIfNeeded(result.project, bundle);
2219
2353
  const payload = serializeBundleConversationDetails(result.project, bundle);
2220
2354
  res.json(payload);
2355
+ startPassiveGitHubWork('bundle-conversation-pr-discovery', () => {
2356
+ startBundlePullRequestDiscoveryIfNeeded(result.project, bundle);
2357
+ });
2221
2358
  });
2222
2359
  app.get('/bundles/:id/review-findings', (req, res) => {
2223
2360
  const result = getRequiredProject(req);
@@ -3203,7 +3340,7 @@ app.post('/projects', (req, res) => {
3203
3340
  return res.status(400).json({ error: validation.error });
3204
3341
  }
3205
3342
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
3206
- const { name, repo_path, linked_project_ids, run_setup, run_services, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
3343
+ const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
3207
3344
  const id = randomUUID();
3208
3345
  db.prepare(`
3209
3346
  INSERT INTO projects (
@@ -3212,6 +3349,7 @@ app.post('/projects', (req, res) => {
3212
3349
  repo_path,
3213
3350
  run_setup,
3214
3351
  run_services,
3352
+ test_commands,
3215
3353
  run_ide,
3216
3354
  preview_service_name,
3217
3355
  preview_path,
@@ -3223,8 +3361,8 @@ app.post('/projects', (req, res) => {
3223
3361
  memory_enabled,
3224
3362
  worktree_sync
3225
3363
  )
3226
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3227
- `).run(id, name, repo_path, run_setup, run_services, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, worktree_sync ?? null);
3364
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3365
+ `).run(id, name, repo_path, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, worktree_sync ?? null);
3228
3366
  replaceProjectLinks(id, linked_project_ids ?? []);
3229
3367
  ensureProjectRootBundle(id);
3230
3368
  if (projectCount === 0) {
@@ -4101,13 +4239,14 @@ app.patch('/projects/:id', (req, res) => {
4101
4239
  });
4102
4240
  }
4103
4241
  }
4104
- const { name, repo_path, linked_project_ids, run_setup, run_services, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
4242
+ const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
4105
4243
  db.prepare(`
4106
4244
  UPDATE projects SET
4107
4245
  name = ?,
4108
4246
  repo_path = ?,
4109
4247
  run_setup = ?,
4110
4248
  run_services = ?,
4249
+ test_commands = ?,
4111
4250
  run_ide = ?,
4112
4251
  preview_service_name = ?,
4113
4252
  preview_path = ?,
@@ -4120,7 +4259,7 @@ app.patch('/projects/:id', (req, res) => {
4120
4259
  worktree_sync = ?,
4121
4260
  updated_at = CURRENT_TIMESTAMP
4122
4261
  WHERE id = ?
4123
- `).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, req.params.id);
4262
+ `).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, req.params.id);
4124
4263
  if (hasOwn(validation.values, 'linked_project_ids')) {
4125
4264
  replaceProjectLinks(project.id, linked_project_ids ?? []);
4126
4265
  }
@@ -4881,6 +5020,141 @@ app.post('/projects/:id/run', (req, res) => {
4881
5020
  targetBundleId: workspace.target_bundle_id,
4882
5021
  });
4883
5022
  });
5023
+ app.post('/projects/:id/test-sessions', (req, res) => {
5024
+ const project = getProjectById(req.params.id);
5025
+ if (!project) {
5026
+ return res.status(404).json({ error: 'Not found' });
5027
+ }
5028
+ const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : null;
5029
+ const targetKind = body?.target_kind === 'bundle'
5030
+ ? 'bundle'
5031
+ : body?.target_kind === 'repo_root'
5032
+ ? 'repo_root'
5033
+ : null;
5034
+ if (!targetKind) {
5035
+ return res.status(400).json({ error: 'target_kind must be repo_root or bundle' });
5036
+ }
5037
+ const targetBundleId = typeof body?.target_bundle_id === 'string' ? body.target_bundle_id : null;
5038
+ const workspace = resolveExplicitProjectTestWorkspace(project, targetKind, targetBundleId);
5039
+ if ('error' in workspace) {
5040
+ return res.status(Number(workspace.status)).json({ error: workspace.error });
5041
+ }
5042
+ return res.status(201).json(createTestSession({
5043
+ projectId: project.id,
5044
+ targetKind: workspace.target_kind,
5045
+ targetBundleId: workspace.target_bundle_id,
5046
+ cwd: workspace.cwd,
5047
+ targetLabel: workspace.target_label,
5048
+ }));
5049
+ });
5050
+ app.post('/tickets/:id/test-sessions', (req, res) => {
5051
+ const ticket = getTicket(req.params.id);
5052
+ if (!ticket) {
5053
+ return res.status(404).json({ error: 'Not found' });
5054
+ }
5055
+ if (!matchesTicketProjectContext(ticket, req)) {
5056
+ return res.status(404).json({ error: 'Not found' });
5057
+ }
5058
+ const workspace = resolveTicketTestWorkspace(ticket);
5059
+ if ('error' in workspace) {
5060
+ return res.status(workspace.status).json({ error: workspace.error });
5061
+ }
5062
+ return res.status(201).json(createTestSession({
5063
+ projectId: workspace.project.id,
5064
+ ticketId: ticket.id,
5065
+ targetKind: workspace.target_kind,
5066
+ targetBundleId: workspace.target_bundle_id,
5067
+ cwd: workspace.cwd,
5068
+ targetLabel: workspace.target_label,
5069
+ }));
5070
+ });
5071
+ app.get('/test-sessions/:id/discovery', (req, res) => {
5072
+ const session = getTestSession(req.params.id);
5073
+ if (!session) {
5074
+ return res.status(404).json({ error: 'Not found' });
5075
+ }
5076
+ const commandId = typeof req.query.command_id === 'string' ? req.query.command_id.trim() : '';
5077
+ let command = null;
5078
+ if (commandId !== '') {
5079
+ const project = getProjectById(session.project_id);
5080
+ if (!project) {
5081
+ return res.status(404).json({ error: 'Project not found' });
5082
+ }
5083
+ command = findProjectTestCommand(project, commandId);
5084
+ if (!command) {
5085
+ return res.status(404).json({ error: 'Test command not found' });
5086
+ }
5087
+ }
5088
+ try {
5089
+ const supportsGranularSelection = command
5090
+ ? isTestSelectionSupported(command, { kind: 'file', path: '' }, session.cwd)
5091
+ : false;
5092
+ return res.json({
5093
+ files: discoverTestsForSession(session.id, command),
5094
+ supports_granular_selection: supportsGranularSelection,
5095
+ });
5096
+ }
5097
+ catch (error) {
5098
+ return res.status(500).json({ error: getErrorMessage(error) });
5099
+ }
5100
+ });
5101
+ app.post('/test-sessions/:id/run', async (req, res) => {
5102
+ const session = getTestSession(req.params.id);
5103
+ if (!session) {
5104
+ return res.status(404).json({ error: 'Not found' });
5105
+ }
5106
+ const project = getProjectById(session.project_id);
5107
+ if (!project) {
5108
+ return res.status(404).json({ error: 'Project not found' });
5109
+ }
5110
+ const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : null;
5111
+ const commandId = typeof body?.command_id === 'string' ? body.command_id.trim() : '';
5112
+ if (commandId === '') {
5113
+ return res.status(400).json({ error: 'command_id is required' });
5114
+ }
5115
+ const selection = parseTestTargetSelection(body?.selection);
5116
+ if (!selection) {
5117
+ return res.status(400).json({ error: 'selection must be all, file, or case' });
5118
+ }
5119
+ const command = findProjectTestCommand(project, commandId);
5120
+ if (!command) {
5121
+ return res.status(404).json({ error: 'Test command not found' });
5122
+ }
5123
+ if (!isTestSelectionSupported(command, selection, session.cwd)) {
5124
+ return res.status(409).json({ error: 'The selected test command only supports running the full suite.' });
5125
+ }
5126
+ if (selection.kind !== 'all' && !doesTestFileExistForSession(session.id, selection.path, command)) {
5127
+ return res.status(404).json({ error: 'Selected test file no longer exists in this target workspace.' });
5128
+ }
5129
+ try {
5130
+ return res.json(await startTestSessionRun(session.id, project, {
5131
+ command,
5132
+ selection,
5133
+ }, io));
5134
+ }
5135
+ catch (error) {
5136
+ return res.status(409).json({ error: getErrorMessage(error) });
5137
+ }
5138
+ });
5139
+ app.post('/test-sessions/:id/stop', async (req, res) => {
5140
+ const session = getTestSession(req.params.id);
5141
+ if (!session) {
5142
+ return res.status(404).json({ error: 'Not found' });
5143
+ }
5144
+ try {
5145
+ return res.json(await stopTestSessionRun(session.id, io));
5146
+ }
5147
+ catch (error) {
5148
+ return res.status(500).json({ error: getErrorMessage(error) });
5149
+ }
5150
+ });
5151
+ app.delete('/test-sessions/:id', async (req, res) => {
5152
+ const deleted = await deleteTestSession(req.params.id, io);
5153
+ if (!deleted) {
5154
+ return res.status(404).json({ error: 'Not found' });
5155
+ }
5156
+ return res.status(204).send();
5157
+ });
4884
5158
  app.post('/tickets/:id/terminal', (req, res) => {
4885
5159
  const ticket = getTicket(req.params.id);
4886
5160
  if (!ticket) {
@@ -5198,6 +5472,7 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
5198
5472
  });
5199
5473
  };
5200
5474
  export const shutdownServer = async (signal) => {
5475
+ await clearAllTestSessions(io);
5201
5476
  await shutdownRealtimeServer({
5202
5477
  signal,
5203
5478
  io,