quadwork 1.5.1 → 1.5.2

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.
Files changed (76) hide show
  1. package/out/404.html +1 -1
  2. package/out/__next.__PAGE__.txt +1 -1
  3. package/out/__next._full.txt +1 -1
  4. package/out/__next._head.txt +1 -1
  5. package/out/__next._index.txt +1 -1
  6. package/out/__next._tree.txt +1 -1
  7. package/out/_not-found/__next._full.txt +1 -1
  8. package/out/_not-found/__next._head.txt +1 -1
  9. package/out/_not-found/__next._index.txt +1 -1
  10. package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
  11. package/out/_not-found/__next._not-found.txt +1 -1
  12. package/out/_not-found/__next._tree.txt +1 -1
  13. package/out/_not-found.html +1 -1
  14. package/out/_not-found.txt +1 -1
  15. package/out/app-shell/__next._full.txt +1 -1
  16. package/out/app-shell/__next._head.txt +1 -1
  17. package/out/app-shell/__next._index.txt +1 -1
  18. package/out/app-shell/__next._tree.txt +1 -1
  19. package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
  20. package/out/app-shell/__next.app-shell.txt +1 -1
  21. package/out/app-shell.html +1 -1
  22. package/out/app-shell.txt +1 -1
  23. package/out/index.html +1 -1
  24. package/out/index.txt +1 -1
  25. package/out/project/_/__next._full.txt +1 -1
  26. package/out/project/_/__next._head.txt +1 -1
  27. package/out/project/_/__next._index.txt +1 -1
  28. package/out/project/_/__next._tree.txt +1 -1
  29. package/out/project/_/__next.project.$d$id.__PAGE__.txt +1 -1
  30. package/out/project/_/__next.project.$d$id.txt +1 -1
  31. package/out/project/_/__next.project.txt +1 -1
  32. package/out/project/_/memory/__next._full.txt +1 -1
  33. package/out/project/_/memory/__next._head.txt +1 -1
  34. package/out/project/_/memory/__next._index.txt +1 -1
  35. package/out/project/_/memory/__next._tree.txt +1 -1
  36. package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +1 -1
  37. package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
  38. package/out/project/_/memory/__next.project.$d$id.txt +1 -1
  39. package/out/project/_/memory/__next.project.txt +1 -1
  40. package/out/project/_/memory.html +1 -1
  41. package/out/project/_/memory.txt +1 -1
  42. package/out/project/_/queue/__next._full.txt +1 -1
  43. package/out/project/_/queue/__next._head.txt +1 -1
  44. package/out/project/_/queue/__next._index.txt +1 -1
  45. package/out/project/_/queue/__next._tree.txt +1 -1
  46. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
  47. package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
  48. package/out/project/_/queue/__next.project.$d$id.txt +1 -1
  49. package/out/project/_/queue/__next.project.txt +1 -1
  50. package/out/project/_/queue.html +1 -1
  51. package/out/project/_/queue.txt +1 -1
  52. package/out/project/_.html +1 -1
  53. package/out/project/_.txt +1 -1
  54. package/out/settings/__next._full.txt +1 -1
  55. package/out/settings/__next._head.txt +1 -1
  56. package/out/settings/__next._index.txt +1 -1
  57. package/out/settings/__next._tree.txt +1 -1
  58. package/out/settings/__next.settings.__PAGE__.txt +1 -1
  59. package/out/settings/__next.settings.txt +1 -1
  60. package/out/settings.html +1 -1
  61. package/out/settings.txt +1 -1
  62. package/out/setup/__next._full.txt +1 -1
  63. package/out/setup/__next._head.txt +1 -1
  64. package/out/setup/__next._index.txt +1 -1
  65. package/out/setup/__next._tree.txt +1 -1
  66. package/out/setup/__next.setup.__PAGE__.txt +1 -1
  67. package/out/setup/__next.setup.txt +1 -1
  68. package/out/setup.html +1 -1
  69. package/out/setup.txt +1 -1
  70. package/package.json +1 -1
  71. package/server/routes.chatWsSend.test.js +161 -0
  72. package/server/routes.js +150 -29
  73. package/server/routes.telegramBridge.test.js +50 -2
  74. /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → yMYfZ4LAn8Fy22suFUnOy}/_buildManifest.js +0 -0
  75. /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → yMYfZ4LAn8Fy22suFUnOy}/_clientMiddlewareManifest.js +0 -0
  76. /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → yMYfZ4LAn8Fy22suFUnOy}/_ssgManifest.js +0 -0
package/server/routes.js CHANGED
@@ -148,11 +148,41 @@ router.get("/api/chat", async (req, res) => {
148
148
  const { WebSocket: NodeWebSocket } = require("ws");
149
149
  const { syncChattrToken } = require("./config");
150
150
 
151
+ // #236: wait for AgentChattr to echo our message back over the same ws
152
+ // connection before resolving, instead of fire-and-forgetting. AC's
153
+ // /ws handler does this on every connect:
154
+ // 1. Replays history as N `{type:"message", data: msg}` frames.
155
+ // 2. Sends one `{type:"status", data: …}` frame (broadcast_status).
156
+ // 3. Enters the receive loop and accepts our outgoing frame.
157
+ // After our `type:"message"` is processed, AC calls `store.add()`
158
+ // which broadcasts the stored record back to all clients (including
159
+ // us) as another `{type:"message", data: msg}`.
160
+ //
161
+ // To get a race-free ack we therefore:
162
+ // A. Wait for the first `type:"status"` frame to confirm the
163
+ // history replay is done — any `type:"message"` frame seen
164
+ // BEFORE that is historical and must be ignored.
165
+ // B. Only then send our message and record the highest message
166
+ // id observed so far as a correlation baseline.
167
+ // C. Accept the first post-send `type:"message"` whose payload
168
+ // matches (sender, text, channel, reply_to) AND whose id is
169
+ // strictly greater than the baseline (AC ids are monotonically
170
+ // increasing from store.add). This eliminates the risk a
171
+ // reviewer flagged on #382 round 1: a historical identical
172
+ // message from <1.5s ago could have satisfied the old
173
+ // heuristic matcher.
174
+ // On timeout / early close / 4003, we surface a proper error so the
175
+ // /api/chat handler can return a 5xx (or 401) instead of a silent
176
+ // {ok:true}.
151
177
  function sendViaWebSocket(baseUrl, sessionToken, message) {
152
178
  return new Promise((resolve, reject) => {
153
179
  const wsUrl = `${baseUrl.replace(/^http/, "ws")}/ws?token=${encodeURIComponent(sessionToken || "")}`;
154
180
  const ws = new NodeWebSocket(wsUrl);
155
181
  let settled = false;
182
+ let historyFlushed = false;
183
+ let sent = false;
184
+ let maxIdAtSend = -Infinity;
185
+ let maxHistoryId = -Infinity;
156
186
  const finish = (err, value) => {
157
187
  if (settled) return;
158
188
  settled = true;
@@ -160,14 +190,56 @@ function sendViaWebSocket(baseUrl, sessionToken, message) {
160
190
  if (err) reject(err); else resolve(value);
161
191
  };
162
192
  const giveUp = setTimeout(() => finish(new Error("websocket send timeout")), 4000);
163
- ws.on("open", () => {
193
+ const doSend = () => {
194
+ if (sent || settled) return;
164
195
  try {
196
+ maxIdAtSend = maxHistoryId;
165
197
  ws.send(JSON.stringify({ type: "message", ...message }));
166
- // Server acks via broadcast, but the dashboard's POST /api/chat
167
- // contract only needs to know the message was accepted. Wait
168
- // ~250ms for the server to enqueue + close cleanly.
169
- setTimeout(() => { clearTimeout(giveUp); finish(null, { ok: true }); }, 250);
198
+ sent = true;
170
199
  } catch (err) { clearTimeout(giveUp); finish(err); }
200
+ };
201
+ ws.on("open", () => {
202
+ // Do NOT send yet. Wait for the status frame that marks the
203
+ // end of history replay so we have a clean correlation
204
+ // baseline. A safety timer covers the (unlikely) case of an
205
+ // AC build that doesn't emit status on connect — after 750ms
206
+ // we fall back to sending anyway, using whatever max id we
207
+ // collected from history so far as the baseline.
208
+ setTimeout(() => {
209
+ if (!historyFlushed) {
210
+ historyFlushed = true;
211
+ doSend();
212
+ }
213
+ }, 750);
214
+ });
215
+ ws.on("message", (raw) => {
216
+ if (settled) return;
217
+ let frame;
218
+ try { frame = JSON.parse(raw.toString()); } catch { return; }
219
+ if (!frame || !frame.type) return;
220
+ if (frame.type === "status" && !historyFlushed) {
221
+ historyFlushed = true;
222
+ doSend();
223
+ return;
224
+ }
225
+ if (frame.type !== "message" || !frame.data) return;
226
+ const d = frame.data;
227
+ // Track the highest message id we have observed, whether from
228
+ // history replay or from other live broadcasts. Used as the
229
+ // baseline for the post-send correlation check.
230
+ if (typeof d.id === "number" && d.id > maxHistoryId) {
231
+ maxHistoryId = d.id;
232
+ }
233
+ if (!sent) return; // anything before our send is history
234
+ if (typeof d.id !== "number" || d.id <= maxIdAtSend) return;
235
+ if (d.sender !== message.sender) return;
236
+ if (d.text !== message.text) return;
237
+ if ((d.channel || "general") !== (message.channel || "general")) return;
238
+ const wantReply = message.reply_to ?? null;
239
+ const gotReply = d.reply_to ?? null;
240
+ if (wantReply !== gotReply) return;
241
+ clearTimeout(giveUp);
242
+ finish(null, { ok: true, message: d });
171
243
  });
172
244
  ws.on("error", (err) => { clearTimeout(giveUp); finish(err); });
173
245
  ws.on("close", (code, reason) => {
@@ -179,6 +251,15 @@ function sendViaWebSocket(baseUrl, sessionToken, message) {
179
251
  const e = new Error(msg);
180
252
  e.code = "EAGENTCHATTR_401";
181
253
  finish(e);
254
+ return;
255
+ }
256
+ // Any other premature close after we sent but before we saw
257
+ // the echo is an error — the old code path would have claimed
258
+ // success, silently swallowing a server-side reject.
259
+ if (!settled) {
260
+ clearTimeout(giveUp);
261
+ const r = (reason && reason.toString()) || "";
262
+ finish(new Error(`websocket closed before ack (code=${code}${r ? ", reason=" + r : ""})`));
182
263
  }
183
264
  });
184
265
  });
@@ -861,8 +942,12 @@ router.post("/api/chat", async (req, res) => {
861
942
  const attemptSend = () => sendViaWebSocket(base, sessionToken, message);
862
943
 
863
944
  try {
864
- await attemptSend();
865
- return res.json({ ok: true });
945
+ // #236: sendViaWebSocket now waits for AC's broadcast echo and
946
+ // returns `{ok, message}` where `message` is the stored record
947
+ // (with server-assigned id/timestamp). Pass it through so
948
+ // callers regain parity with the old /api/send response body.
949
+ const result = await attemptSend();
950
+ return res.json({ ok: true, message: result.message });
866
951
  } catch (err) {
867
952
  // If the cached session_token is stale (AgentChattr regenerates
868
953
  // one on every restart) the ws closes with code 4003 — re-sync
@@ -876,8 +961,8 @@ router.post("/api/chat", async (req, res) => {
876
961
  const { token: refreshed } = getChattrConfig(projectId);
877
962
  if (refreshed && refreshed !== sessionToken) {
878
963
  try {
879
- await sendViaWebSocket(base, refreshed, message);
880
- return res.json({ ok: true, resynced: true });
964
+ const retry = await sendViaWebSocket(base, refreshed, message);
965
+ return res.json({ ok: true, resynced: true, message: retry.message });
881
966
  } catch (retryErr) {
882
967
  console.warn(`[chat] retry after token resync failed: ${retryErr.message}`);
883
968
  return res.status(401).json({ error: "AgentChattr auth failed (token resync did not help)", detail: retryErr.message });
@@ -2339,7 +2424,12 @@ function readLastLines(filePath, n) {
2339
2424
  // otherwise. Keep the import list small and close to what the
2340
2425
  // bridge actually needs; add modules here if the bridge gains new
2341
2426
  // hard deps.
2342
- function checkTelegramBridgePythonDeps() {
2427
+ // #380: `pythonPath` defaults to bare `python3` for backward-compat,
2428
+ // but the production call sites (install, start) MUST pass the
2429
+ // dedicated bridge venv's interpreter (`<BRIDGE_DIR>/.venv/bin/python3`)
2430
+ // so the import check runs against the same interpreter the spawn will
2431
+ // use. See #379 research ticket for root cause.
2432
+ function checkTelegramBridgePythonDeps(pythonPath = "python3") {
2343
2433
  try {
2344
2434
  // Only check the third-party module the bridge actually needs
2345
2435
  // at import time — `requests`. Toml parsing differs between
@@ -2347,7 +2437,7 @@ function checkTelegramBridgePythonDeps() {
2347
2437
  // genuine toml import failure will now be captured in the
2348
2438
  // bridge log file on spawn, so this pre-flight stays narrow
2349
2439
  // and avoids false negatives on older Python installs.
2350
- execFileSync("python3", ["-c", "import requests"], {
2440
+ execFileSync(pythonPath, ["-c", "import requests"], {
2351
2441
  encoding: "utf-8",
2352
2442
  timeout: 10000,
2353
2443
  stdio: ["ignore", "pipe", "pipe"],
@@ -2503,34 +2593,47 @@ router.post("/api/telegram", async (req, res) => {
2503
2593
  }
2504
2594
  }
2505
2595
  case "install": {
2506
- // #353: pip3 can exit 0 on some systems (PEP 668 externally-
2507
- // managed environments, non-writable site-packages) even when
2508
- // the subsequent import still fails. After the pip step, run
2509
- // a post-install import check and surface both the pip output
2510
- // and the import error together if the check fails that's
2511
- // the signal the operator needs to know whether to pick a
2512
- // virtualenv, use --user, or --break-system-packages.
2596
+ // #380: create a dedicated bridge venv at
2597
+ // `<BRIDGE_DIR>/.venv` and install requirements into it using
2598
+ // that venv's pip. All bridge subprocesses then spawn with
2599
+ // `<BRIDGE_DIR>/.venv/bin/python3` by absolute path. See #379
2600
+ // research ticket for the root cause bare `python3` / `pip3`
2601
+ // resolve to Homebrew Python on modern macOS where `requests`
2602
+ // is not available, producing a ModuleNotFoundError on Start.
2603
+ // Idempotent: existing installs missing a `.venv` get the venv
2604
+ // created on top of the existing clone without re-cloning.
2605
+ const venvDir = path.join(BRIDGE_DIR, ".venv");
2606
+ const venvPython = path.join(venvDir, "bin", "python3");
2607
+ const venvPip = path.join(venvDir, "bin", "pip");
2513
2608
  let pipOutput = "";
2514
2609
  try {
2515
2610
  if (!fs.existsSync(BRIDGE_DIR)) {
2516
2611
  execFileSync("gh", ["repo", "clone", "realproject7/agentchattr-telegram", BRIDGE_DIR], { encoding: "utf-8", timeout: 30000 });
2517
2612
  }
2613
+ // #380: create the dedicated venv if missing. `python3 -m venv`
2614
+ // builds a fresh isolated environment that bypasses PEP 668
2615
+ // externally-managed markers, so this works even on Homebrew
2616
+ // Python where bare `pip3 install` would be blocked.
2617
+ if (!fs.existsSync(venvPython)) {
2618
+ execFileSync("python3", ["-m", "venv", venvDir], { encoding: "utf-8", timeout: 60000 });
2619
+ }
2518
2620
  pipOutput = execFileSync(
2519
- "pip3",
2621
+ venvPip,
2520
2622
  ["install", "-r", path.join(BRIDGE_DIR, "requirements.txt")],
2521
- { encoding: "utf-8", timeout: 60000 },
2623
+ { encoding: "utf-8", timeout: 120000 },
2522
2624
  );
2523
2625
  } catch (err) {
2524
- return res.json({ ok: false, error: err.message || "Install failed" });
2626
+ const stderr = (err && err.stderr && err.stderr.toString && err.stderr.toString()) || "";
2627
+ return res.json({ ok: false, error: (stderr.trim() || err.message || "Install failed") });
2525
2628
  }
2526
- const depCheck = checkTelegramBridgePythonDeps();
2629
+ const depCheck = checkTelegramBridgePythonDeps(venvPython);
2527
2630
  if (!depCheck.ok) {
2528
2631
  return res.json({
2529
2632
  ok: false,
2530
2633
  error:
2531
- "pip3 reported success but the bridge's Python deps still fail to import. " +
2532
- "This usually means pip installed into a location python3 cannot see " +
2533
- "(externally-managed environment / PEP 668 / mismatched interpreter).\n\n" +
2634
+ "pip reported success but the bridge venv's Python deps still fail to import. " +
2635
+ "This is unexpected for a freshly-created venv check disk space and permissions " +
2636
+ `on ${venvDir}.\n\n` +
2534
2637
  `Import error: ${depCheck.error}\n\n` +
2535
2638
  `pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
2536
2639
  });
@@ -2543,6 +2646,18 @@ router.post("/api/telegram", async (req, res) => {
2543
2646
  if (isTelegramRunning(projectId)) return res.json({ ok: true, running: true, message: "Already running" });
2544
2647
  const bridgeScript = path.join(BRIDGE_DIR, "telegram_bridge.py");
2545
2648
  if (!fs.existsSync(bridgeScript)) return res.json({ ok: false, error: "Bridge not installed. Click Install Bridge first." });
2649
+ // #380: resolve the dedicated venv's python3 by absolute path.
2650
+ // Do NOT activate the venv or set VIRTUAL_ENV in the parent —
2651
+ // calling the venv's python3 directly is sufficient because
2652
+ // Python's sys.executable bootstrap resolves the venv
2653
+ // automatically. See #379 research ticket.
2654
+ const venvPython = path.join(BRIDGE_DIR, ".venv", "bin", "python3");
2655
+ if (!fs.existsSync(venvPython)) {
2656
+ return res.json({
2657
+ ok: false,
2658
+ error: "Bridge venv missing. Click \"Install Bridge\" to create it.",
2659
+ });
2660
+ }
2546
2661
  const tg = getProjectTelegram(projectId);
2547
2662
  if (!tg || !tg.bot_token || !tg.chat_id) return res.json({ ok: false, error: "Save bot_token and chat_id in project settings first." });
2548
2663
  const tomlPath = telegramConfigToml(projectId);
@@ -2553,7 +2668,7 @@ router.post("/api/telegram", async (req, res) => {
2553
2668
  // `requests` module produces a readable error instead of the
2554
2669
  // Start → Running → Stopped flicker that the v1 code path
2555
2670
  // produced with `stdio: "ignore"`.
2556
- const depCheck = checkTelegramBridgePythonDeps();
2671
+ const depCheck = checkTelegramBridgePythonDeps(venvPython);
2557
2672
  if (!depCheck.ok) {
2558
2673
  // #372: persist the pre-flight failure to the bridge log
2559
2674
  // file so the GET /api/telegram `last_error` tail picks it
@@ -2562,8 +2677,8 @@ router.post("/api/telegram", async (req, res) => {
2562
2677
  // local error state, producing the "silent fail" symptom
2563
2678
  // (pill flips back to Stopped with no trace of why).
2564
2679
  const msg =
2565
- "Bridge Python dependencies not installed. Click \"Install Bridge\" to install them, " +
2566
- "or run: pip3 install -r " + path.join(BRIDGE_DIR, "requirements.txt") + "\n\n" +
2680
+ "Bridge Python dependencies not installed in the dedicated venv. " +
2681
+ "Click \"Install Bridge\" to (re)create the venv and install them.\n\n" +
2567
2682
  `Import error: ${depCheck.error}`;
2568
2683
  try {
2569
2684
  fs.writeFileSync(
@@ -2595,7 +2710,7 @@ router.post("/api/telegram", async (req, res) => {
2595
2710
  }
2596
2711
  let child;
2597
2712
  try {
2598
- child = spawn("python3", [bridgeScript, "--config", tomlPath], {
2713
+ child = spawn(venvPython, [bridgeScript, "--config", tomlPath], {
2599
2714
  detached: true,
2600
2715
  stdio: ["ignore", outFd, errFd],
2601
2716
  });
@@ -2780,3 +2895,9 @@ module.exports.buildNoPrRow = buildNoPrRow;
2780
2895
  module.exports.summarizeItems = summarizeItems;
2781
2896
  // #353: expose readLastLines for the telegram-bridge test.
2782
2897
  module.exports.readLastLines = readLastLines;
2898
+ // #380: expose checkTelegramBridgePythonDeps so the bridge test can
2899
+ // exercise the venv-path interpreter argument round trip.
2900
+ module.exports.checkTelegramBridgePythonDeps = checkTelegramBridgePythonDeps;
2901
+ // #236: expose sendViaWebSocket so the chat-ws-send regression test
2902
+ // can verify the ack/body/error paths against a fake AC ws server.
2903
+ module.exports.sendViaWebSocket = sendViaWebSocket;
@@ -10,7 +10,8 @@ const assert = require("node:assert/strict");
10
10
  const fs = require("node:fs");
11
11
  const os = require("node:os");
12
12
  const path = require("node:path");
13
- const { readLastLines } = require("./routes");
13
+ const { execFileSync } = require("node:child_process");
14
+ const { readLastLines, checkTelegramBridgePythonDeps } = require("./routes");
14
15
 
15
16
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "qw-bridge-log-"));
16
17
  function write(name, content) {
@@ -89,7 +90,54 @@ try {
89
90
  assert.match(tail, /ModuleNotFoundError/);
90
91
  assert.match(tail, /Install Bridge/);
91
92
 
92
- console.log("routes.telegramBridge.test.js: all assertions passed (8 cases)");
93
+ // 9) #380: checkTelegramBridgePythonDeps accepts an explicit
94
+ // interpreter path. Passing a guaranteed-broken path must
95
+ // return { ok: false, error } without throwing.
96
+ const broken = checkTelegramBridgePythonDeps(path.join(tmp, "nope", "python3"));
97
+ assert.equal(broken.ok, false);
98
+ assert.ok(broken.error && broken.error.length > 0);
99
+
100
+ // 10) #380: start handler's missing-venv branch — we don't boot
101
+ // the server here, but the branch reduces to a plain
102
+ // fs.existsSync check on `<BRIDGE_DIR>/.venv/bin/python3`,
103
+ // so we verify the check returns false for a fixture dir
104
+ // that has no `.venv` subdir at all.
105
+ const fixtureBridgeDir = fs.mkdtempSync(path.join(tmp, "bridge-no-venv-"));
106
+ const missingVenvPython = path.join(fixtureBridgeDir, ".venv", "bin", "python3");
107
+ assert.equal(fs.existsSync(missingVenvPython), false);
108
+
109
+ // 11) #380: round-trip — build a real venv in a tmp dir, install
110
+ // a stdlib-only sentinel is trivially importable, and confirm
111
+ // checkTelegramBridgePythonDeps reports ok when `requests`
112
+ // is installed into that venv. Skipped gracefully on CI if
113
+ // `python3 -m venv` or network-backed pip install fails.
114
+ const venvDir = path.join(tmp, "case11-venv");
115
+ let venvSkipped = false;
116
+ try {
117
+ execFileSync("python3", ["-m", "venv", venvDir], { timeout: 30000, stdio: "pipe" });
118
+ const venvPython = path.join(venvDir, "bin", "python3");
119
+ const venvPip = path.join(venvDir, "bin", "pip");
120
+ // Without `requests` installed yet, the check must fail.
121
+ const before = checkTelegramBridgePythonDeps(venvPython);
122
+ assert.equal(before.ok, false);
123
+ try {
124
+ execFileSync(venvPip, ["install", "--quiet", "requests"], { timeout: 120000, stdio: "pipe" });
125
+ } catch {
126
+ venvSkipped = true;
127
+ }
128
+ if (!venvSkipped) {
129
+ const after = checkTelegramBridgePythonDeps(venvPython);
130
+ assert.equal(after.ok, true);
131
+ }
132
+ } catch {
133
+ venvSkipped = true;
134
+ }
135
+
136
+ console.log(
137
+ "routes.telegramBridge.test.js: all assertions passed (11 cases" +
138
+ (venvSkipped ? ", case 11 pip step skipped" : "") +
139
+ ")",
140
+ );
93
141
  } finally {
94
142
  try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
95
143
  }