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.
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +1 -1
- package/out/__next._full.txt +1 -1
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +1 -1
- package/out/__next._tree.txt +1 -1
- package/out/_not-found/__next._full.txt +1 -1
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +1 -1
- package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +1 -1
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +1 -1
- package/out/app-shell/__next._full.txt +1 -1
- package/out/app-shell/__next._head.txt +1 -1
- package/out/app-shell/__next._index.txt +1 -1
- package/out/app-shell/__next._tree.txt +1 -1
- package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
- package/out/app-shell/__next.app-shell.txt +1 -1
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +1 -1
- package/out/index.html +1 -1
- package/out/index.txt +1 -1
- package/out/project/_/__next._full.txt +1 -1
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +1 -1
- package/out/project/_/__next._tree.txt +1 -1
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +1 -1
- package/out/project/_/__next.project.$d$id.txt +1 -1
- package/out/project/_/__next.project.txt +1 -1
- package/out/project/_/memory/__next._full.txt +1 -1
- package/out/project/_/memory/__next._head.txt +1 -1
- package/out/project/_/memory/__next._index.txt +1 -1
- package/out/project/_/memory/__next._tree.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.txt +1 -1
- package/out/project/_/memory/__next.project.txt +1 -1
- package/out/project/_/memory.html +1 -1
- package/out/project/_/memory.txt +1 -1
- package/out/project/_/queue/__next._full.txt +1 -1
- package/out/project/_/queue/__next._head.txt +1 -1
- package/out/project/_/queue/__next._index.txt +1 -1
- package/out/project/_/queue/__next._tree.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.txt +1 -1
- package/out/project/_/queue/__next.project.txt +1 -1
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +1 -1
- package/out/project/_.html +1 -1
- package/out/project/_.txt +1 -1
- package/out/settings/__next._full.txt +1 -1
- package/out/settings/__next._head.txt +1 -1
- package/out/settings/__next._index.txt +1 -1
- package/out/settings/__next._tree.txt +1 -1
- package/out/settings/__next.settings.__PAGE__.txt +1 -1
- package/out/settings/__next.settings.txt +1 -1
- package/out/settings.html +1 -1
- package/out/settings.txt +1 -1
- package/out/setup/__next._full.txt +1 -1
- package/out/setup/__next._head.txt +1 -1
- package/out/setup/__next._index.txt +1 -1
- package/out/setup/__next._tree.txt +1 -1
- package/out/setup/__next.setup.__PAGE__.txt +1 -1
- package/out/setup/__next.setup.txt +1 -1
- package/out/setup.html +1 -1
- package/out/setup.txt +1 -1
- package/package.json +1 -1
- package/server/routes.chatWsSend.test.js +161 -0
- package/server/routes.js +150 -29
- package/server/routes.telegramBridge.test.js +50 -2
- /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → yMYfZ4LAn8Fy22suFUnOy}/_buildManifest.js +0 -0
- /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → yMYfZ4LAn8Fy22suFUnOy}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
|
|
193
|
+
const doSend = () => {
|
|
194
|
+
if (sent || settled) return;
|
|
164
195
|
try {
|
|
196
|
+
maxIdAtSend = maxHistoryId;
|
|
165
197
|
ws.send(JSON.stringify({ type: "message", ...message }));
|
|
166
|
-
|
|
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
|
-
|
|
865
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
// #
|
|
2507
|
-
//
|
|
2508
|
-
//
|
|
2509
|
-
//
|
|
2510
|
-
//
|
|
2511
|
-
//
|
|
2512
|
-
//
|
|
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
|
-
|
|
2621
|
+
venvPip,
|
|
2520
2622
|
["install", "-r", path.join(BRIDGE_DIR, "requirements.txt")],
|
|
2521
|
-
{ encoding: "utf-8", timeout:
|
|
2623
|
+
{ encoding: "utf-8", timeout: 120000 },
|
|
2522
2624
|
);
|
|
2523
2625
|
} catch (err) {
|
|
2524
|
-
|
|
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
|
-
"
|
|
2532
|
-
"This
|
|
2533
|
-
|
|
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
|
|
2566
|
-
"
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|