polygram 0.3.6 → 0.4.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -19
- package/config.example.json +2 -2
- package/lib/abort-detector.js +63 -0
- package/lib/db.js +16 -1
- package/lib/net-errors.js +94 -0
- package/lib/process-manager.js +77 -23
- package/lib/status-reactions.js +168 -0
- package/lib/stream-reply.js +5 -1
- package/lib/telegram-format.js +36 -0
- package/lib/telegram.js +98 -7
- package/lib/typing-indicator.js +143 -0
- package/migrations/005-polling-state.sql +14 -0
- package/package.json +5 -4
- package/polygram.js +151 -48
- package/scripts/doctor.js +324 -0
- package/scripts/smoke.js +0 -122
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* polygram-doctor — operational health check for a polygram bot.
|
|
4
|
+
*
|
|
5
|
+
* polygram-doctor --bot <name>
|
|
6
|
+
* run default static checks (config / DB / IPC / Telegram)
|
|
7
|
+
*
|
|
8
|
+
* polygram-doctor --bot <name> --roundtrip --to <chat_id>
|
|
9
|
+
* also do an outbound round-trip: IPC send → Telegram → DB read-back
|
|
10
|
+
*
|
|
11
|
+
* polygram-doctor --bot <name> --json
|
|
12
|
+
* machine-readable output for monitoring pipelines
|
|
13
|
+
*
|
|
14
|
+
* Exit codes:
|
|
15
|
+
* 0 all checks passed (warnings allowed unless --strict)
|
|
16
|
+
* 1 at least one check failed
|
|
17
|
+
* 2 bad invocation / missing args
|
|
18
|
+
*
|
|
19
|
+
* Extended from OpenClaw's doctor.ts pattern: config, token, reachability,
|
|
20
|
+
* membership checks, recent-error trail. Safe to run against a live bot
|
|
21
|
+
* (no state changes) unless --roundtrip is passed, which posts a single
|
|
22
|
+
* disable_notification=true message.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const Database = require('better-sqlite3');
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
call, tell, socketPathFor, readSecret,
|
|
31
|
+
} = require('../lib/ipc-client');
|
|
32
|
+
|
|
33
|
+
// ─── Arg parsing ─────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function parseArg(argv, flag, { required = false } = {}) {
|
|
36
|
+
const i = argv.indexOf(flag);
|
|
37
|
+
if (i === -1) {
|
|
38
|
+
if (required) die(`missing required flag: ${flag}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const v = argv[i + 1];
|
|
42
|
+
if (!v || v.startsWith('--')) die(`${flag} requires a value`);
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasFlag(argv, flag) {
|
|
47
|
+
return argv.includes(flag);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function die(msg) {
|
|
51
|
+
process.stderr.write(`polygram-doctor: ${msg}\n`);
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const argv = process.argv;
|
|
56
|
+
const botName = parseArg(argv, '--bot', { required: true });
|
|
57
|
+
const configPath = parseArg(argv, '--config')
|
|
58
|
+
|| process.env.POLYGRAM_CONFIG
|
|
59
|
+
|| path.join(process.cwd(), 'config.json');
|
|
60
|
+
const dbPath = parseArg(argv, '--db')
|
|
61
|
+
|| process.env.POLYGRAM_DB
|
|
62
|
+
|| path.join(process.cwd(), `${botName}.db`);
|
|
63
|
+
const roundtripTo = parseArg(argv, '--to');
|
|
64
|
+
const doRoundtrip = hasFlag(argv, '--roundtrip');
|
|
65
|
+
const asJson = hasFlag(argv, '--json');
|
|
66
|
+
const strict = hasFlag(argv, '--strict');
|
|
67
|
+
const timeoutMs = parseInt(parseArg(argv, '--timeout-ms') || '8000', 10);
|
|
68
|
+
|
|
69
|
+
if (doRoundtrip && !roundtripTo) die('--roundtrip requires --to <chat_id>');
|
|
70
|
+
|
|
71
|
+
// ─── Check accumulator ───────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const checks = [];
|
|
74
|
+
function push(name, status, detail, extra) {
|
|
75
|
+
checks.push({ name, status, detail, extra });
|
|
76
|
+
if (!asJson) {
|
|
77
|
+
const icon = status === 'ok' ? '✅' : status === 'warn' ? '⚠️ ' : '❌';
|
|
78
|
+
let line = `${icon} ${name}`;
|
|
79
|
+
if (detail) line += ` — ${detail}`;
|
|
80
|
+
console.log(line);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Checks ──────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function checkConfig() {
|
|
87
|
+
let raw;
|
|
88
|
+
try { raw = fs.readFileSync(configPath, 'utf8'); }
|
|
89
|
+
catch (e) { push('config', 'fail', `cannot read ${configPath}: ${e.message}`); return null; }
|
|
90
|
+
let cfg;
|
|
91
|
+
try { cfg = JSON.parse(raw); }
|
|
92
|
+
catch (e) { push('config', 'fail', `invalid JSON: ${e.message}`); return null; }
|
|
93
|
+
if (!cfg.bots || !cfg.bots[botName]) {
|
|
94
|
+
push('config', 'fail', `bot "${botName}" not in config.bots`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const bot = cfg.bots[botName];
|
|
98
|
+
if (!bot.token) {
|
|
99
|
+
push('config', 'fail', `bot.${botName}.token is empty`);
|
|
100
|
+
return cfg;
|
|
101
|
+
}
|
|
102
|
+
const chatCount = Object.values(cfg.chats || {}).filter(c => c.bot === botName).length;
|
|
103
|
+
if (chatCount === 0) {
|
|
104
|
+
push('config', 'warn', `bot owns 0 chats in config.chats`);
|
|
105
|
+
} else {
|
|
106
|
+
push('config', 'ok', `bot found, ${chatCount} chat(s), admin=${bot.adminChatId || 'none'}`);
|
|
107
|
+
}
|
|
108
|
+
return cfg;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkDb() {
|
|
112
|
+
if (!fs.existsSync(dbPath)) {
|
|
113
|
+
push('db', 'fail', `no file at ${dbPath}`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let db;
|
|
117
|
+
try { db = new Database(dbPath, { readonly: true, fileMustExist: true }); }
|
|
118
|
+
catch (e) { push('db', 'fail', `cannot open: ${e.message}`); return null; }
|
|
119
|
+
try {
|
|
120
|
+
const version = db.pragma('user_version', { simple: true });
|
|
121
|
+
const migrationsDir = path.join(__dirname, '..', 'migrations');
|
|
122
|
+
const files = fs.readdirSync(migrationsDir).filter(f => /^\d+.*\.sql$/.test(f));
|
|
123
|
+
const latest = files.length
|
|
124
|
+
? Math.max(...files.map(f => parseInt(f.match(/^(\d+)/)[1], 10)))
|
|
125
|
+
: 0;
|
|
126
|
+
if (version === latest) {
|
|
127
|
+
push('db', 'ok', `schema v${version}`, { version, latest });
|
|
128
|
+
} else if (version < latest) {
|
|
129
|
+
push('db', 'warn', `schema v${version}, migrations dir has v${latest}; restart bot to apply`, { version, latest });
|
|
130
|
+
} else {
|
|
131
|
+
push('db', 'warn', `schema v${version} ahead of migrations dir v${latest}`, { version, latest });
|
|
132
|
+
}
|
|
133
|
+
return db;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
push('db', 'fail', e.message);
|
|
136
|
+
db.close();
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function checkIpc() {
|
|
142
|
+
try {
|
|
143
|
+
const res = await call({
|
|
144
|
+
path: socketPathFor(botName),
|
|
145
|
+
op: 'ping',
|
|
146
|
+
callTimeoutMs: timeoutMs,
|
|
147
|
+
});
|
|
148
|
+
if (res?.ok && res.pong) {
|
|
149
|
+
push('ipc', 'ok', `socket responsive, bot=${res.bot}`);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
push('ipc', 'fail', JSON.stringify(res));
|
|
153
|
+
return false;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// Distinguish "no socket" (bot not running) from "socket dead"
|
|
156
|
+
if (/ENOENT|ECONNREFUSED/.test(err.message)) {
|
|
157
|
+
push('ipc', 'warn', `bot not running — IPC socket absent at ${socketPathFor(botName)}`);
|
|
158
|
+
} else {
|
|
159
|
+
push('ipc', 'fail', err.message);
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function checkTelegram(cfg) {
|
|
166
|
+
if (!cfg || !cfg.bots?.[botName]?.token) {
|
|
167
|
+
push('telegram', 'warn', 'skipped — no token in config');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const token = cfg.bots[botName].token;
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
173
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
174
|
+
});
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
if (!data.ok) {
|
|
177
|
+
push('telegram', 'fail', `getMe returned: ${data.description || data.error_code}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
push('telegram', 'ok', `@${data.result.username} (${data.result.first_name})`,
|
|
181
|
+
{ username: data.result.username, id: data.result.id });
|
|
182
|
+
} catch (err) {
|
|
183
|
+
push('telegram', 'fail', err.message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function checkRecentErrors(db) {
|
|
188
|
+
if (!db) { push('recent-errors', 'warn', 'skipped — no db'); return; }
|
|
189
|
+
try {
|
|
190
|
+
const since = Date.now() - 24 * 3600 * 1000;
|
|
191
|
+
const rows = db.prepare(`
|
|
192
|
+
SELECT kind, COUNT(*) AS n
|
|
193
|
+
FROM events
|
|
194
|
+
WHERE ts >= ?
|
|
195
|
+
AND kind IN (
|
|
196
|
+
'handler-error', 'telegram-api-error', 'telegram-edit-failed',
|
|
197
|
+
'poll-stalled', 'resume-fail', 'turn-timeout',
|
|
198
|
+
'typing-suspended', 'approval-sweep-failed', 'malformed-update',
|
|
199
|
+
'telegram-retry', 'telegram-thread-fallback'
|
|
200
|
+
)
|
|
201
|
+
GROUP BY kind
|
|
202
|
+
ORDER BY n DESC
|
|
203
|
+
`).all(since);
|
|
204
|
+
if (rows.length === 0) {
|
|
205
|
+
push('recent-errors', 'ok', 'no failure events in last 24h');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const summary = rows.map(r => `${r.kind}=${r.n}`).join(', ');
|
|
209
|
+
// Hard-fail on real errors, warn on retry/fallback (those are recoveries).
|
|
210
|
+
const hasHardError = rows.some(r =>
|
|
211
|
+
r.kind === 'handler-error' || r.kind === 'resume-fail' ||
|
|
212
|
+
r.kind === 'turn-timeout' || r.kind === 'approval-sweep-failed'
|
|
213
|
+
);
|
|
214
|
+
push('recent-errors', hasHardError ? 'warn' : 'ok', summary, { rows });
|
|
215
|
+
} catch (e) {
|
|
216
|
+
push('recent-errors', 'fail', e.message);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function checkPendingOutbound(db) {
|
|
221
|
+
if (!db) return;
|
|
222
|
+
try {
|
|
223
|
+
const cutoff = Date.now() - 5 * 60 * 1000;
|
|
224
|
+
const stale = db.prepare(`
|
|
225
|
+
SELECT COUNT(*) AS n FROM messages
|
|
226
|
+
WHERE status = 'pending' AND bot_name = ? AND ts < ?
|
|
227
|
+
`).get(botName, cutoff).n;
|
|
228
|
+
if (stale > 0) {
|
|
229
|
+
push('pending-outbound', 'warn', `${stale} pending rows older than 5min — bot may have crashed mid-send`);
|
|
230
|
+
} else {
|
|
231
|
+
push('pending-outbound', 'ok', 'no stale pending outbound rows');
|
|
232
|
+
}
|
|
233
|
+
} catch (e) {
|
|
234
|
+
push('pending-outbound', 'fail', e.message);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function checkApprovals(db) {
|
|
239
|
+
if (!db) return;
|
|
240
|
+
try {
|
|
241
|
+
const row = db.prepare(`
|
|
242
|
+
SELECT COUNT(*) AS n FROM approvals
|
|
243
|
+
WHERE status = 'pending' AND bot_name = ?
|
|
244
|
+
`).get(botName);
|
|
245
|
+
if (row.n > 0) {
|
|
246
|
+
push('approvals', 'warn', `${row.n} pending approval(s) — operator may need to act`);
|
|
247
|
+
} else {
|
|
248
|
+
push('approvals', 'ok', 'no pending approvals');
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// Table may not exist pre-migration — not fatal.
|
|
252
|
+
push('approvals', 'warn', `skipped: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function checkRoundtrip() {
|
|
257
|
+
if (!doRoundtrip) return;
|
|
258
|
+
const marker = `polygram-doctor:${Date.now()}`;
|
|
259
|
+
try {
|
|
260
|
+
const res = await tell(botName, 'sendMessage', {
|
|
261
|
+
chat_id: roundtripTo,
|
|
262
|
+
text: marker,
|
|
263
|
+
disable_notification: true,
|
|
264
|
+
}, { source: 'polygram-doctor', callTimeoutMs: timeoutMs });
|
|
265
|
+
const msgId = res?.message_id;
|
|
266
|
+
if (!msgId) {
|
|
267
|
+
push('roundtrip', 'fail', `no message_id: ${JSON.stringify(res)}`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// DB read-back
|
|
271
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
272
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
273
|
+
const row = db.prepare(`
|
|
274
|
+
SELECT direction, status, text FROM messages
|
|
275
|
+
WHERE chat_id = ? AND msg_id = ?
|
|
276
|
+
`).get(String(roundtripTo), msgId);
|
|
277
|
+
db.close();
|
|
278
|
+
if (!row) { push('roundtrip', 'fail', `no DB row for msg_id=${msgId}`); return; }
|
|
279
|
+
if (row.status !== 'sent' || row.direction !== 'out') {
|
|
280
|
+
push('roundtrip', 'fail', `row status=${row.status} direction=${row.direction}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!String(row.text).includes(marker)) {
|
|
284
|
+
push('roundtrip', 'fail', 'marker not in DB row');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
push('roundtrip', 'ok', `msg_id=${msgId} delivered + recorded`);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
push('roundtrip', 'fail', err.message);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Main ────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
async function main() {
|
|
296
|
+
const cfg = checkConfig();
|
|
297
|
+
const db = checkDb();
|
|
298
|
+
await checkIpc();
|
|
299
|
+
await checkTelegram(cfg);
|
|
300
|
+
checkRecentErrors(db);
|
|
301
|
+
checkPendingOutbound(db);
|
|
302
|
+
checkApprovals(db);
|
|
303
|
+
await checkRoundtrip();
|
|
304
|
+
if (db) try { db.close(); } catch {}
|
|
305
|
+
|
|
306
|
+
const fails = checks.filter(c => c.status === 'fail').length;
|
|
307
|
+
const warns = checks.filter(c => c.status === 'warn').length;
|
|
308
|
+
|
|
309
|
+
if (asJson) {
|
|
310
|
+
console.log(JSON.stringify({ bot: botName, checks, fails, warns }, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
const passed = checks.length - fails - warns;
|
|
313
|
+
console.log(`\n${passed} ok / ${warns} warn / ${fails} fail (bot=${botName})`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (fails > 0) process.exit(1);
|
|
317
|
+
if (strict && warns > 0) process.exit(1);
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main().catch((err) => {
|
|
322
|
+
process.stderr.write(`polygram-doctor: unexpected error: ${err.message}\n${err.stack}\n`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|
package/scripts/smoke.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* polygram-smoke — round-trip health check for a polygram bot.
|
|
4
|
-
*
|
|
5
|
-
* polygram-smoke --bot <name> --to <chat_id>
|
|
6
|
-
*
|
|
7
|
-
* Exits 0 if all three round-trips pass, 1 otherwise.
|
|
8
|
-
*
|
|
9
|
-
* Intended for cron (heartbeat) or ad-hoc operator verification.
|
|
10
|
-
*
|
|
11
|
-
* Round-trips:
|
|
12
|
-
* 1. IPC ping — socket is alive and handshakes
|
|
13
|
-
* 2. Outbound — IPC 'send' op → Telegram API → returns msg_id
|
|
14
|
-
* 3. DB read-back — that msg_id appears in messages table with
|
|
15
|
-
* direction='out', status='sent', matching text
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const Database = require('better-sqlite3');
|
|
20
|
-
const { call, tell, socketPathFor, readSecret } = require('../lib/ipc-client');
|
|
21
|
-
|
|
22
|
-
function parseArg(argv, flag, required = false) {
|
|
23
|
-
const i = argv.indexOf(flag);
|
|
24
|
-
if (i === -1) {
|
|
25
|
-
if (required) die(`missing required flag: ${flag}`);
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
const v = argv[i + 1];
|
|
29
|
-
if (!v || v.startsWith('--')) die(`${flag} requires a value`);
|
|
30
|
-
return v;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function die(msg) {
|
|
34
|
-
process.stderr.write(`polygram-smoke: ${msg}\n`);
|
|
35
|
-
process.exit(2);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const bot = parseArg(process.argv, '--bot', true);
|
|
39
|
-
const to = parseArg(process.argv, '--to', true);
|
|
40
|
-
const dbPath = parseArg(process.argv, '--db')
|
|
41
|
-
|| process.env.POLYGRAM_DB
|
|
42
|
-
|| path.join(process.cwd(), `${bot}.db`);
|
|
43
|
-
const timeoutMs = parseInt(parseArg(process.argv, '--timeout-ms') || '8000', 10);
|
|
44
|
-
|
|
45
|
-
const stamp = Date.now();
|
|
46
|
-
const marker = `polygram-smoke:${stamp}`;
|
|
47
|
-
const results = [];
|
|
48
|
-
|
|
49
|
-
function report(step, ok, detail) {
|
|
50
|
-
const icon = ok ? '✅' : '❌';
|
|
51
|
-
console.log(`${icon} ${step}${detail ? ` — ${detail}` : ''}`);
|
|
52
|
-
results.push({ step, ok, detail });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function main() {
|
|
56
|
-
// Step 1 — IPC ping
|
|
57
|
-
try {
|
|
58
|
-
const res = await call({
|
|
59
|
-
path: socketPathFor(bot),
|
|
60
|
-
op: 'ping',
|
|
61
|
-
callTimeoutMs: timeoutMs,
|
|
62
|
-
});
|
|
63
|
-
if (res?.ok && res.pong) report('ipc-ping', true, `bot=${res.bot}`);
|
|
64
|
-
else { report('ipc-ping', false, JSON.stringify(res)); exitNow(); }
|
|
65
|
-
} catch (err) {
|
|
66
|
-
report('ipc-ping', false, err.message);
|
|
67
|
-
exitNow();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Step 2 — outbound round-trip
|
|
71
|
-
let msgId = null;
|
|
72
|
-
try {
|
|
73
|
-
const res = await tell(bot, 'sendMessage', {
|
|
74
|
-
chat_id: to,
|
|
75
|
-
text: marker,
|
|
76
|
-
disable_notification: true,
|
|
77
|
-
}, { source: 'polygram-smoke', callTimeoutMs: timeoutMs });
|
|
78
|
-
msgId = res?.message_id;
|
|
79
|
-
if (msgId) report('outbound-send', true, `msg_id=${msgId}`);
|
|
80
|
-
else { report('outbound-send', false, JSON.stringify(res)); exitNow(); }
|
|
81
|
-
} catch (err) {
|
|
82
|
-
report('outbound-send', false, err.message);
|
|
83
|
-
exitNow();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Step 3 — DB read-back. The sender writes pending→sent in two steps;
|
|
87
|
-
// give it a tick, then query by msg_id.
|
|
88
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
89
|
-
try {
|
|
90
|
-
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
91
|
-
const row = db.prepare(`
|
|
92
|
-
SELECT direction, status, text, source, msg_id
|
|
93
|
-
FROM messages
|
|
94
|
-
WHERE chat_id = ? AND msg_id = ?
|
|
95
|
-
`).get(String(to), msgId);
|
|
96
|
-
db.close();
|
|
97
|
-
|
|
98
|
-
if (!row) { report('db-readback', false, `no row for msg_id=${msgId}`); exitNow(); }
|
|
99
|
-
if (row.direction !== 'out') { report('db-readback', false, `direction=${row.direction}`); exitNow(); }
|
|
100
|
-
if (row.status !== 'sent') { report('db-readback', false, `status=${row.status}`); exitNow(); }
|
|
101
|
-
if (!String(row.text).includes(marker)) { report('db-readback', false, `text mismatch: ${row.text?.slice(0, 60)}`); exitNow(); }
|
|
102
|
-
report('db-readback', true, `sent row confirmed (source=${row.source})`);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
report('db-readback', false, err.message);
|
|
105
|
-
exitNow();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
console.log(`\npolygram-smoke: PASS ${bot} ${new Date().toISOString()}`);
|
|
109
|
-
process.exit(0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function exitNow() {
|
|
113
|
-
const passed = results.filter(r => r.ok).length;
|
|
114
|
-
const total = results.length;
|
|
115
|
-
console.log(`\npolygram-smoke: FAIL ${passed}/${total} steps bot=${bot}`);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
main().catch((err) => {
|
|
120
|
-
console.error('polygram-smoke: unexpected error:', err.message);
|
|
121
|
-
process.exit(1);
|
|
122
|
-
});
|