@rubytech/create-maxy-code 0.1.358 → 0.1.359
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/dist/__tests__/neo4j-pidfile-scrub.test.js +103 -0
- package/dist/index.js +42 -7
- package/dist/neo4j-pidfile-scrub.js +90 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.js +58 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-probe.test.js +109 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-probe.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts +13 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +35 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +18 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +79 -20
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.js +33 -0
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +11 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-status.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-status.js +24 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-status.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +9 -2
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +11 -4
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +22 -7
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-Ro_RUTDh.js → AdminLoginScreens-tHyAPNnQ.js} +1 -1
- package/payload/server/public/assets/AdminShell-Ce9izNVZ.js +1 -0
- package/payload/server/public/assets/{Checkbox-dYLaMEdh.js → Checkbox-Cq6SJJlX.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-BERW_hUa.css +1 -0
- package/payload/server/public/assets/OperatorConversations-m2lz6Ss7.js +9 -0
- package/payload/server/public/assets/admin-D9iMgdbN.js +1 -0
- package/payload/server/public/assets/{browser-DrXZ4wpy.js → browser-CZWi0EAP.js} +1 -1
- package/payload/server/public/assets/calendar-DUAh-_kx.js +1 -0
- package/payload/server/public/assets/chat-DHuibPfc.js +1 -0
- package/payload/server/public/assets/data-CoOcAR_2.js +1 -0
- package/payload/server/public/assets/{graph-l_3_j3I9.js → graph-Dc_votDd.js} +3 -3
- package/payload/server/public/assets/{graph-labels-CJj55nHk.js → graph-labels-jqagxwhW.js} +1 -1
- package/payload/server/public/assets/operator-KYXcuQ4Y.js +1 -0
- package/payload/server/public/assets/page-Cfsv97nq.js +32 -0
- package/payload/server/public/assets/{public-smrhkkxD.js → public-49guEjCh.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-Dd8aAvU7.js → rotate-ccw-CyR5o3Nq.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +6 -6
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +7 -7
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +8 -8
- package/payload/server/public/public.html +6 -6
- package/payload/server/public/assets/AdminShell-CmqhpAPU.js +0 -1
- package/payload/server/public/assets/OperatorConversations-2zJh7qnO.css +0 -1
- package/payload/server/public/assets/OperatorConversations-Cei12sSK.js +0 -9
- package/payload/server/public/assets/admin-BldZQrJF.js +0 -1
- package/payload/server/public/assets/calendar-CQ4TCbO7.js +0 -1
- package/payload/server/public/assets/chat-D7w6_NE1.js +0 -1
- package/payload/server/public/assets/data-Dc5XNWgF.js +0 -1
- package/payload/server/public/assets/operator-CKmOgXNa.js +0 -1
- package/payload/server/public/assets/page-hG7xTrcO.js +0 -32
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Contract grid for resolveNeo4jPidfileScrub (the pure decision behind the
|
|
2
|
+
// install-time per-boot scrub provisioning) plus a behaviour grid for the
|
|
3
|
+
// shipped SCRUB_SCRIPT run against crafted PIDs. The wrapper in index.ts owns
|
|
4
|
+
// the privileged fs/systemctl; this suite is decision + script only, no sudo.
|
|
5
|
+
//
|
|
6
|
+
// The load-bearing guarantee: the scrub NEVER removes a pidfile that names a
|
|
7
|
+
// live Neo4j process, and ALWAYS removes one whose PID is dead or reused by an
|
|
8
|
+
// unrelated process. Runs via Node's built-in test runner (codebase convention,
|
|
9
|
+
// see neo4j-teardown.test.ts).
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { spawnSync, spawn } from "node:child_process";
|
|
16
|
+
import { resolveNeo4jPidfileScrub, SCRUB_SCRIPT, SCRUB_SCRIPT_PATH } from "../neo4j-pidfile-scrub.js";
|
|
17
|
+
// ---- resolve grid (pure) ----
|
|
18
|
+
test("default brand (port 7687) → apt neo4j unit + /var/lib/neo4j/run pidfile", () => {
|
|
19
|
+
const plan = resolveNeo4jPidfileScrub({ brandHostname: "maxy-code", neo4jPort: 7687 });
|
|
20
|
+
assert.notEqual(plan, null);
|
|
21
|
+
assert.equal(plan.unit, "neo4j");
|
|
22
|
+
assert.equal(plan.pidfile, "/var/lib/neo4j/run/neo4j.pid");
|
|
23
|
+
assert.equal(plan.dropinPath, "/etc/systemd/system/neo4j.service.d/10-maxy-pidfile-scrub.conf");
|
|
24
|
+
assert.match(plan.dropinContent, /ExecStartPre=\+\/usr\/local\/lib\/maxy-neo4j-pidfile-scrub\.sh \/var\/lib\/neo4j\/run\/neo4j\.pid/);
|
|
25
|
+
assert.equal(plan.scriptPath, SCRUB_SCRIPT_PATH);
|
|
26
|
+
});
|
|
27
|
+
test("dedicated brand (custom port) → neo4j-<host> unit + per-brand run pidfile", () => {
|
|
28
|
+
const plan = resolveNeo4jPidfileScrub({ brandHostname: "sitedesk-code", neo4jPort: 7689 });
|
|
29
|
+
assert.notEqual(plan, null);
|
|
30
|
+
assert.equal(plan.unit, "neo4j-sitedesk-code");
|
|
31
|
+
assert.equal(plan.pidfile, "/var/lib/neo4j-sitedesk-code/run/neo4j.pid");
|
|
32
|
+
assert.equal(plan.dropinPath, "/etc/systemd/system/neo4j-sitedesk-code.service.d/10-maxy-pidfile-scrub.conf");
|
|
33
|
+
// dedicated drop-in never names the shared run-dir pidfile
|
|
34
|
+
assert.doesNotMatch(plan.dropinContent, /\/var\/lib\/neo4j\/run\//);
|
|
35
|
+
});
|
|
36
|
+
test("unreadable port (undefined) → null (never guess the unit)", () => {
|
|
37
|
+
assert.equal(resolveNeo4jPidfileScrub({ brandHostname: "sitedesk-code", neo4jPort: undefined }), null);
|
|
38
|
+
});
|
|
39
|
+
test("dedicated port with empty hostname → null (cannot form a unit)", () => {
|
|
40
|
+
assert.equal(resolveNeo4jPidfileScrub({ brandHostname: "", neo4jPort: 7689 }), null);
|
|
41
|
+
});
|
|
42
|
+
// ---- script behaviour (writes SCRUB_SCRIPT to disk, runs against crafted PIDs) ----
|
|
43
|
+
function runScrub(pidfile) {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "scrub-"));
|
|
45
|
+
const script = join(dir, "scrub.sh");
|
|
46
|
+
writeFileSync(script, SCRUB_SCRIPT, { mode: 0o755 });
|
|
47
|
+
const r = spawnSync("sh", [script, pidfile], { encoding: "utf8" });
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
return { stdout: r.stdout ?? "", status: r.status ?? -1 };
|
|
50
|
+
}
|
|
51
|
+
test("absent pidfile → no-op, exit 0", () => {
|
|
52
|
+
const r = runScrub("/no/such/pidfile");
|
|
53
|
+
assert.equal(r.status, 0);
|
|
54
|
+
assert.match(r.stdout, /no-op .*reason=absent/);
|
|
55
|
+
});
|
|
56
|
+
test("unparseable pidfile → removed, exit 0", () => {
|
|
57
|
+
const dir = mkdtempSync(join(tmpdir(), "scrub-pf-"));
|
|
58
|
+
const pf = join(dir, "neo4j.pid");
|
|
59
|
+
writeFileSync(pf, "not-a-number\n");
|
|
60
|
+
const r = runScrub(pf);
|
|
61
|
+
assert.equal(r.status, 0);
|
|
62
|
+
assert.match(r.stdout, /removed .*reason=unparseable/);
|
|
63
|
+
assert.equal(existsSync(pf), false);
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
test("dead PID → removed, exit 0", () => {
|
|
67
|
+
const dir = mkdtempSync(join(tmpdir(), "scrub-pf-"));
|
|
68
|
+
const pf = join(dir, "neo4j.pid");
|
|
69
|
+
// 999999 is above the default pid_max on Linux and unused on darwin — dead.
|
|
70
|
+
writeFileSync(pf, "999999\n");
|
|
71
|
+
const r = runScrub(pf);
|
|
72
|
+
assert.equal(r.status, 0);
|
|
73
|
+
assert.match(r.stdout, /removed .*reason=dead/);
|
|
74
|
+
assert.equal(existsSync(pf), false);
|
|
75
|
+
rmSync(dir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
test("alive non-neo4j PID → removed, exit 0", async () => {
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), "scrub-pf-"));
|
|
79
|
+
const pf = join(dir, "neo4j.pid");
|
|
80
|
+
const proc = spawn("sleep", ["30"]); // argv has no 'neo4j'
|
|
81
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
82
|
+
writeFileSync(pf, String(proc.pid) + "\n");
|
|
83
|
+
const r = runScrub(pf);
|
|
84
|
+
proc.kill("SIGKILL");
|
|
85
|
+
assert.equal(r.status, 0);
|
|
86
|
+
assert.match(r.stdout, /removed .*reason=alive-other/);
|
|
87
|
+
assert.equal(existsSync(pf), false);
|
|
88
|
+
rmSync(dir, { recursive: true, force: true });
|
|
89
|
+
});
|
|
90
|
+
test("alive Neo4j-cmdline PID → no-op, exit 0 (Linux only — needs /proc)", { skip: process.platform !== "linux" }, async () => {
|
|
91
|
+
const dir = mkdtempSync(join(tmpdir(), "scrub-pf-"));
|
|
92
|
+
const pf = join(dir, "neo4j.pid");
|
|
93
|
+
// a long-lived process whose argv contains 'neo4j' ($0 = the stub name)
|
|
94
|
+
const proc = spawn("sh", ["-c", "sleep 30", "neo4j-jvm-stub"]);
|
|
95
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
96
|
+
writeFileSync(pf, String(proc.pid) + "\n");
|
|
97
|
+
const r = runScrub(pf);
|
|
98
|
+
proc.kill("SIGKILL");
|
|
99
|
+
assert.equal(r.status, 0);
|
|
100
|
+
assert.match(r.stdout, /no-op .*reason=live-neo4j/);
|
|
101
|
+
assert.equal(existsSync(pf), true); // never removes a live neo4j's pidfile
|
|
102
|
+
rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerU
|
|
|
9
9
|
import { validateTierFlag, validateEntitlementBase64, applyTierToAccountConfig, entitlementPath } from "./tier-flag.js";
|
|
10
10
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
11
11
|
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
12
|
+
import { resolveNeo4jPidfileScrub } from "./neo4j-pidfile-scrub.js";
|
|
12
13
|
import { runInitLogging } from "./init-logging.js";
|
|
13
14
|
import { requireSupportedPlatform, detectPlatform } from "./platform-detect.js";
|
|
14
15
|
import { protectCommands, unprotectCommands, lsattrShowsImmutable } from "./install-immutability.js";
|
|
@@ -1515,13 +1516,16 @@ WantedBy=multi-user.target
|
|
|
1515
1516
|
// ============================================================================
|
|
1516
1517
|
// unified state remediation + start + verify.
|
|
1517
1518
|
//
|
|
1518
|
-
// Runs on both fresh and recovery paths. The
|
|
1519
|
-
//
|
|
1520
|
-
// NEO4J_HOME
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1519
|
+
// Runs on both fresh and recovery paths. The apt system unit leaves
|
|
1520
|
+
// NEO4J_HOME unset so its run dir is /var/lib/neo4j/run; the dedicated unit
|
|
1521
|
+
// sets NEO4J_HOME=<per-brand data dir> so server.directories.run resolves
|
|
1522
|
+
// per-brand to <data dir>/run — no collision with the system unit's pidfile.
|
|
1523
|
+
// Either way the launcher refuses with "Neo4j is already running (pid:N)" if
|
|
1524
|
+
// a stale run-dir pidfile names a still-live PID. Stopping the system unit
|
|
1525
|
+
// first frees its run-state; disabling prevents it returning at next boot.
|
|
1526
|
+
// reset-failed clears any prior start-limit-hit from a half-installed Pi.
|
|
1527
|
+
// (The per-boot scrub from provisionNeo4jPidfileScrub auto-clears the
|
|
1528
|
+
// reused-PID case on every subsequent boot.)
|
|
1525
1529
|
// ============================================================================
|
|
1526
1530
|
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
1527
1531
|
console.log(" [privileged] systemctl enable");
|
|
@@ -1599,6 +1603,36 @@ WantedBy=multi-user.target
|
|
|
1599
1603
|
logFile(` Neo4j dedicated: config=${confDir} data=${dataDir} service=${serviceName} bolt=:${NEO4J_PORT} http=:${httpPort}`);
|
|
1600
1604
|
console.log(` Dedicated Neo4j instance ready on bolt://localhost:${NEO4J_PORT}`);
|
|
1601
1605
|
}
|
|
1606
|
+
// Provision the per-boot stale-pidfile scrub drop-in for whichever Neo4j unit
|
|
1607
|
+
// this brand uses. The drop-in's ExecStartPre runs the host-shared scrub script
|
|
1608
|
+
// before each unit start, removing a stale run-dir pidfile whose PID is not a
|
|
1609
|
+
// live Neo4j (the reused-PID case that browns-out a Pi into a non-starting
|
|
1610
|
+
// Neo4j after an unclean reboot). Idempotent: fixed paths, overwritten each
|
|
1611
|
+
// install, never appended. Linux-only — a no-op on darwin/dev hosts.
|
|
1612
|
+
function provisionNeo4jPidfileScrub() {
|
|
1613
|
+
if (process.platform !== "linux")
|
|
1614
|
+
return;
|
|
1615
|
+
const plan = resolveNeo4jPidfileScrub({ brandHostname: BRAND.hostname, neo4jPort: NEO4J_PORT });
|
|
1616
|
+
if (plan === null) {
|
|
1617
|
+
logFile(` [neo4j] pidfile-scrub: no plan (port=${NEO4J_PORT} hostname=${BRAND.hostname}) — skipped`);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
// pid-namespaced staging paths so two co-resident brand installs never race
|
|
1621
|
+
// on a shared /tmp file (matches the file's other tmp-staging idioms).
|
|
1622
|
+
const tmpScript = `/tmp/maxy-neo4j-pidfile-scrub.${process.pid}.sh`;
|
|
1623
|
+
writeFileSync(tmpScript, plan.scriptContent);
|
|
1624
|
+
shell("cp", [tmpScript, plan.scriptPath], { sudo: true });
|
|
1625
|
+
shell("chmod", ["0755", plan.scriptPath], { sudo: true });
|
|
1626
|
+
spawnSync("rm", ["-f", tmpScript]);
|
|
1627
|
+
shell("mkdir", ["-p", plan.dropinDir], { sudo: true });
|
|
1628
|
+
const tmpDropin = `/tmp/10-maxy-pidfile-scrub.${process.pid}.conf`;
|
|
1629
|
+
writeFileSync(tmpDropin, plan.dropinContent);
|
|
1630
|
+
shell("cp", [tmpDropin, plan.dropinPath], { sudo: true });
|
|
1631
|
+
spawnSync("rm", ["-f", tmpDropin]);
|
|
1632
|
+
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
1633
|
+
console.log(` [neo4j] pidfile-scrub armed for ${plan.unit} (ExecStartPre scrubs ${plan.pidfile})`);
|
|
1634
|
+
logFile(` [neo4j] pidfile-scrub armed unit=${plan.unit} pidfile=${plan.pidfile} dropin=${plan.dropinPath} script=${plan.scriptPath}`);
|
|
1635
|
+
}
|
|
1602
1636
|
function ensureOllamaServing() {
|
|
1603
1637
|
const check = () => spawnSync("curl", ["-sf", "http://localhost:11434/api/tags"], {
|
|
1604
1638
|
stdio: "pipe", timeout: 5_000,
|
|
@@ -4347,6 +4381,7 @@ try {
|
|
|
4347
4381
|
installClaudeCode();
|
|
4348
4382
|
installNeo4j();
|
|
4349
4383
|
setupDedicatedNeo4j();
|
|
4384
|
+
provisionNeo4jPidfileScrub();
|
|
4350
4385
|
installOllama(EMBED_MODEL);
|
|
4351
4386
|
installUv();
|
|
4352
4387
|
installCloudflared();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Pure per-boot Neo4j stale-pidfile scrub resolution + the scrub script body.
|
|
2
|
+
// Mirrors neo4j-teardown.ts: inputs in, plan out, no fs/systemctl. The installer
|
|
3
|
+
// wrapper (provisionNeo4jPidfileScrub in index.ts) owns the privileged writes.
|
|
4
|
+
//
|
|
5
|
+
// Why: after an unclean reboot the neo4j launcher refuses to start with
|
|
6
|
+
// "Neo4j is already running (pid:N)" when the run-dir pidfile names a PID that
|
|
7
|
+
// is alive but reused by an unrelated process (the launcher's kill -0 liveness
|
|
8
|
+
// check false-positives). The scrub runs as the unit's ExecStartPre and removes
|
|
9
|
+
// the pidfile IFF the PID it names is not a live Neo4j process, so the next boot
|
|
10
|
+
// recovers with no human action. It never removes a pidfile naming a live Neo4j.
|
|
11
|
+
import { SHARED_NEO4J_PORT, resolveDedicatedNeo4jTeardown } from "./neo4j-teardown.js";
|
|
12
|
+
/** Host-shared scrub script path. Stateless and parameterised by the pidfile
|
|
13
|
+
* path, so one copy serves the apt unit and any dedicated unit. */
|
|
14
|
+
export const SCRUB_SCRIPT_PATH = "/usr/local/lib/maxy-neo4j-pidfile-scrub.sh";
|
|
15
|
+
const DROPIN_BASENAME = "10-maxy-pidfile-scrub.conf";
|
|
16
|
+
/** The scrub script body. POSIX sh. $1 = the run-dir pidfile path. Always exits
|
|
17
|
+
* 0 — an ExecStartPre that fails would block the unit from starting. Emits one
|
|
18
|
+
* line per run naming the action, pidfile, PID, and what the PID was. */
|
|
19
|
+
export const SCRUB_SCRIPT = `#!/bin/sh
|
|
20
|
+
# Maxy per-boot Neo4j stale-pidfile scrub. ExecStartPre for the brand's Neo4j
|
|
21
|
+
# unit. Removes the run-dir pidfile IFF the PID it names is not a live Neo4j
|
|
22
|
+
# process, so an unclean reboot that left a stale pidfile (whose PID may have
|
|
23
|
+
# been reused by an unrelated process) does not make the launcher false-positive
|
|
24
|
+
# "Neo4j is already running". Never removes a pidfile naming a live Neo4j.
|
|
25
|
+
# Always exits 0 so it cannot block unit start.
|
|
26
|
+
set -u
|
|
27
|
+
PIDFILE="\${1:-}"
|
|
28
|
+
emit() { echo "neo4j-pidfile-scrub $1 pidfile=\${PIDFILE:-<none>} pid=$2 reason=$3"; }
|
|
29
|
+
|
|
30
|
+
if [ -z "$PIDFILE" ]; then emit no-op '<none>' no-pidfile-arg; exit 0; fi
|
|
31
|
+
if [ ! -e "$PIDFILE" ]; then emit no-op '<none>' absent; exit 0; fi
|
|
32
|
+
|
|
33
|
+
PID="$(tr -d '[:space:]' < "$PIDFILE" 2>/dev/null)"
|
|
34
|
+
case "$PID" in
|
|
35
|
+
''|*[!0-9]*) emit removed "\${PID:-<empty>}" unparseable; rm -f "$PIDFILE"; exit 0 ;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
if ! kill -0 "$PID" 2>/dev/null; then
|
|
39
|
+
emit removed "$PID" dead
|
|
40
|
+
rm -f "$PIDFILE"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
CMD="$(tr '\\0' ' ' < "/proc/$PID/cmdline" 2>/dev/null)"
|
|
45
|
+
case "$CMD" in
|
|
46
|
+
*neo4j*) emit no-op "$PID" live-neo4j; exit 0 ;;
|
|
47
|
+
*) emit removed "$PID" alive-other; rm -f "$PIDFILE"; exit 0 ;;
|
|
48
|
+
esac
|
|
49
|
+
`;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the per-boot scrub plan for a brand's Neo4j unit, or `null` when no
|
|
52
|
+
* unit can be safely targeted:
|
|
53
|
+
* - `neo4jPort === undefined`: unreadable port — never guess the unit.
|
|
54
|
+
* - dedicated port with empty `brandHostname`: no `neo4j-<host>` unit exists.
|
|
55
|
+
* A default-brand port (7687) targets the shared apt `neo4j` unit; any other
|
|
56
|
+
* port targets the brand-dedicated `neo4j-<hostname>` unit, whose run dir lives
|
|
57
|
+
* under its per-brand `NEO4J_HOME` (`<dataDir>/run`). The dedicated unit name +
|
|
58
|
+
* data dir are taken from `resolveDedicatedNeo4jTeardown` so install, uninstall,
|
|
59
|
+
* and this scrub share one derivation and never drift.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveNeo4jPidfileScrub(args) {
|
|
62
|
+
const { brandHostname, neo4jPort } = args;
|
|
63
|
+
if (neo4jPort === undefined)
|
|
64
|
+
return null;
|
|
65
|
+
let unit;
|
|
66
|
+
let pidfile;
|
|
67
|
+
if (neo4jPort === SHARED_NEO4J_PORT) {
|
|
68
|
+
unit = "neo4j";
|
|
69
|
+
pidfile = "/var/lib/neo4j/run/neo4j.pid";
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Dedicated brand: reuse the teardown resolver's unit/dataDir derivation
|
|
73
|
+
// (returns null on empty hostname) rather than re-deriving the names here.
|
|
74
|
+
const dedicated = resolveDedicatedNeo4jTeardown({ brandHostname, neo4jPort });
|
|
75
|
+
if (dedicated === null)
|
|
76
|
+
return null;
|
|
77
|
+
unit = dedicated.service;
|
|
78
|
+
pidfile = `${dedicated.dataDir}/run/neo4j.pid`;
|
|
79
|
+
}
|
|
80
|
+
const dropinDir = `/etc/systemd/system/${unit}.service.d`;
|
|
81
|
+
return {
|
|
82
|
+
unit,
|
|
83
|
+
pidfile,
|
|
84
|
+
dropinDir,
|
|
85
|
+
dropinPath: `${dropinDir}/${DROPIN_BASENAME}`,
|
|
86
|
+
dropinContent: `[Service]\nExecStartPre=+${SCRUB_SCRIPT_PATH} ${pidfile}\n`,
|
|
87
|
+
scriptPath: SCRUB_SCRIPT_PATH,
|
|
88
|
+
scriptContent: SCRUB_SCRIPT,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:6f90b00dd8971be316fca8e109765ec840a220d06f1cb4f1aabecefb9da17916
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2522,6 +2522,8 @@ either is a regression.
|
|
|
2522
2522
|
|
|
2523
2523
|
**`/chat` empty-state greeting panel.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`) above two read-only lists from `GET /api/webchat/greeting` (`server/routes/webchat-greeting.ts`, `requireAdminSession`-gated, `accountId`-scoped): the next 5 upcoming `:Event` rows (`eventStatus IN ['scheduled','due']`, future `COALESCE(nextRun, startDate)`, soonest first) and the outstanding `:Task` rows (`status IN ['pending','active','running']`, 5 shown + `+N more`). Read failure returns HTTP 200 `{ ok: false }` and the page shows the headline alone — the panel never blocks the composer. A third section, **Your specialists**, shows the account's installed roster — every `*.md` under `data/accounts/<accountId>/specialists/agents/`, core and `--`-prefixed premium files alike — shown by frontmatter `name` + `summary` (fallback `description`), empty dir → "No specialists installed". The roster read (`app/lib/claude-agent/specialist-roster.ts`) is isolated from the graph fallback: malformed files are skipped (`op=specialist-skipped file=<name> reason=<…>`), a whole-roster failure degrades to `specialists: []` inside the `ok: true` payload (`op=specialists-read-failed`). Logs: `[webchat:greeting] op=read account=<id8> events=<n> tasks=<n> specialists=<n> ms=<n>` / `op=read-failed reason=…`; client warns `[admin-ui] greeting-unavailable …`. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
|
|
2524
2524
|
|
|
2525
|
+
**`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), lists them newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
|
|
2526
|
+
|
|
2525
2527
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
2526
2528
|
|
|
2527
2529
|
**`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
|
|
@@ -67,6 +67,8 @@ either is a regression.
|
|
|
67
67
|
|
|
68
68
|
**`/chat` empty-state greeting panel.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`) above two read-only lists from `GET /api/webchat/greeting` (`server/routes/webchat-greeting.ts`, `requireAdminSession`-gated, `accountId`-scoped): the next 5 upcoming `:Event` rows (`eventStatus IN ['scheduled','due']`, future `COALESCE(nextRun, startDate)`, soonest first) and the outstanding `:Task` rows (`status IN ['pending','active','running']`, 5 shown + `+N more`). Read failure returns HTTP 200 `{ ok: false }` and the page shows the headline alone — the panel never blocks the composer. A third section, **Your specialists**, shows the account's installed roster — every `*.md` under `data/accounts/<accountId>/specialists/agents/`, core and `--`-prefixed premium files alike — shown by frontmatter `name` + `summary` (fallback `description`), empty dir → "No specialists installed". The roster read (`app/lib/claude-agent/specialist-roster.ts`) is isolated from the graph fallback: malformed files are skipped (`op=specialist-skipped file=<name> reason=<…>`), a whole-roster failure degrades to `specialists: []` inside the `ok: true` payload (`op=specialists-read-failed`). Logs: `[webchat:greeting] op=read account=<id8> events=<n> tasks=<n> specialists=<n> ms=<n>` / `op=read-failed reason=…`; client warns `[admin-ui] greeting-unavailable …`. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
|
|
69
69
|
|
|
70
|
+
**`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), lists them newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
|
|
71
|
+
|
|
70
72
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
71
73
|
|
|
72
74
|
**`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-status.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-status.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// Mock the heavy IMAP/SMTP/credentials deps so emailStatus is exercised over its
|
|
3
|
+
// reporting logic in isolation. The never-connected signal is the focus.
|
|
4
|
+
const imap = vi.hoisted(() => ({ testConnection: vi.fn(), getInboxCount: vi.fn() }));
|
|
5
|
+
const smtp = vi.hoisted(() => ({ testConnection: vi.fn() }));
|
|
6
|
+
vi.mock("../lib/imap.js", () => imap);
|
|
7
|
+
vi.mock("../lib/smtp.js", () => smtp);
|
|
8
|
+
const cred = vi.hoisted(() => ({
|
|
9
|
+
readConfig: vi.fn(),
|
|
10
|
+
loadCredentials: vi.fn(),
|
|
11
|
+
credentialStorePath: vi.fn(() => "/tmp/store.json"),
|
|
12
|
+
recordSuccessfulConnect: vi.fn(),
|
|
13
|
+
readLastSuccessfulConnect: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../lib/credentials.js", () => cred);
|
|
16
|
+
const { emailStatus } = await import("../tools/email-status.js");
|
|
17
|
+
const config = {
|
|
18
|
+
email: "dale@glsmithandsons.co.uk",
|
|
19
|
+
agentAddress: "dale@glsmithandsons.co.uk",
|
|
20
|
+
imapHost: "mail.lcn.com", imapPort: 993, imapSecurity: "tls",
|
|
21
|
+
smtpHost: "mail.lcn.com", smtpPort: 587, smtpSecurity: "starttls",
|
|
22
|
+
};
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
cred.readConfig.mockResolvedValue(config);
|
|
26
|
+
cred.loadCredentials.mockResolvedValue({ credentials: { config, password: "pw" } });
|
|
27
|
+
});
|
|
28
|
+
describe("email-status never-connected signal", () => {
|
|
29
|
+
it("reports never-connected for a configured mailbox whose IMAP probe fails and has no recorded connect", async () => {
|
|
30
|
+
imap.testConnection.mockResolvedValue("IMAP authentication failed for dale@glsmithandsons.co.uk. Check the password — most providers require an app-specific password when 2FA is enabled.");
|
|
31
|
+
smtp.testConnection.mockResolvedValue("SMTP authentication failed");
|
|
32
|
+
cred.readLastSuccessfulConnect.mockResolvedValue(null);
|
|
33
|
+
const out = await emailStatus({ accountId: "acct-1" });
|
|
34
|
+
expect(out).toMatch(/Last successful connect: never \(never-connected\)/);
|
|
35
|
+
// A failing probe must NOT stamp a success.
|
|
36
|
+
expect(cred.recordSuccessfulConnect).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
it("reports the stored timestamp when a prior connect exists but the probe now fails", async () => {
|
|
39
|
+
imap.testConnection.mockResolvedValue("IMAP connection timed out to mail.lcn.com:993. Check the host and port.");
|
|
40
|
+
smtp.testConnection.mockResolvedValue(null);
|
|
41
|
+
cred.readLastSuccessfulConnect.mockResolvedValue("2026-06-01T09:00:00.000Z");
|
|
42
|
+
const out = await emailStatus({ accountId: "acct-1" });
|
|
43
|
+
expect(out).toMatch(/Last successful connect: 2026-06-01T09:00:00\.000Z/);
|
|
44
|
+
expect(out).not.toMatch(/never-connected/);
|
|
45
|
+
});
|
|
46
|
+
it("stamps and reports a fresh timestamp when the IMAP probe succeeds", async () => {
|
|
47
|
+
imap.testConnection.mockResolvedValue(null);
|
|
48
|
+
smtp.testConnection.mockResolvedValue(null);
|
|
49
|
+
imap.getInboxCount.mockResolvedValue(12);
|
|
50
|
+
const out = await emailStatus({ accountId: "acct-1" });
|
|
51
|
+
expect(cred.recordSuccessfulConnect).toHaveBeenCalledWith("acct-1");
|
|
52
|
+
expect(out).toMatch(/Last successful connect: \d{4}-\d{2}-\d{2}T/);
|
|
53
|
+
expect(out).not.toMatch(/never-connected/);
|
|
54
|
+
// A live success never falls back to readLastSuccessfulConnect.
|
|
55
|
+
expect(cred.readLastSuccessfulConnect).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=email-status.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-status.test.js","sourceRoot":"","sources":["../../src/__tests__/email-status.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,18 +7,25 @@ import { EventEmitter } from "node:events";
|
|
|
7
7
|
const state = vi.hoisted(() => ({ clients: [] }));
|
|
8
8
|
vi.mock("imapflow", () => ({ ImapFlow: vi.fn() }));
|
|
9
9
|
const { ImapFlow } = await import("imapflow");
|
|
10
|
-
const { testConnection, getInboxCount } = await import("../lib/imap.js");
|
|
10
|
+
const { testConnection, getInboxCount, classifyImapError } = await import("../lib/imap.js");
|
|
11
11
|
function fakeClient(overrides = {}) {
|
|
12
12
|
const client = Object.assign(new EventEmitter(), {
|
|
13
13
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
14
14
|
logout: vi.fn().mockResolvedValue(undefined),
|
|
15
15
|
close: vi.fn(),
|
|
16
16
|
mailboxOpen: vi.fn().mockResolvedValue({ exists: 42 }),
|
|
17
|
+
// The underlying transport socket. ImapFlow leaves it live on the auth-reject
|
|
18
|
+
// path; containProbeSocket attaches a persistent 'error' listener to it.
|
|
19
|
+
socket: new EventEmitter(),
|
|
17
20
|
...overrides,
|
|
18
21
|
});
|
|
19
22
|
state.clients.push(client);
|
|
20
23
|
return client;
|
|
21
24
|
}
|
|
25
|
+
/** Build an ImapFlow-shaped error with arbitrary structured fields. */
|
|
26
|
+
function imapErr(message, fields = {}) {
|
|
27
|
+
return Object.assign(new Error(message), fields);
|
|
28
|
+
}
|
|
22
29
|
// new ImapFlow(...) requires a constructable mock — a regular function whose
|
|
23
30
|
// returned object overrides `this`.
|
|
24
31
|
function armImapFlow(overrides = {}) {
|
|
@@ -66,4 +73,105 @@ describe("imap.getInboxCount", () => {
|
|
|
66
73
|
expect(() => state.clients[0].emit("error", new Error("late"))).not.toThrow();
|
|
67
74
|
});
|
|
68
75
|
});
|
|
76
|
+
describe("classifyImapError — structured fields, not only message", () => {
|
|
77
|
+
it("classifies the live auth shape (message 'Command failed') as auth", () => {
|
|
78
|
+
// The exact shape reproduced from the box: the real signal is in the
|
|
79
|
+
// structured fields, never in err.message.
|
|
80
|
+
const err = imapErr("Command failed", {
|
|
81
|
+
authenticationFailed: true,
|
|
82
|
+
serverResponseCode: "AUTHENTICATIONFAILED",
|
|
83
|
+
responseText: "Authentication failed.",
|
|
84
|
+
});
|
|
85
|
+
expect(classifyImapError(err)).toBe("auth");
|
|
86
|
+
});
|
|
87
|
+
it("classifies serverResponseCode=AUTHENTICATIONFAILED alone as auth", () => {
|
|
88
|
+
expect(classifyImapError(imapErr("Command failed", { serverResponseCode: "AUTHENTICATIONFAILED" }))).toBe("auth");
|
|
89
|
+
});
|
|
90
|
+
it("classifies refused / not-found / timeout by err.code", () => {
|
|
91
|
+
expect(classifyImapError(imapErr("Command failed", { code: "ECONNREFUSED" }))).toBe("refused");
|
|
92
|
+
expect(classifyImapError(imapErr("getaddrinfo failed", { code: "ENOTFOUND" }))).toBe("not-found");
|
|
93
|
+
expect(classifyImapError(imapErr("Failed to establish connection", { code: "CONNECT_TIMEOUT" }))).toBe("timeout");
|
|
94
|
+
expect(classifyImapError(imapErr("socket", { code: "ETIMEDOUT" }))).toBe("timeout");
|
|
95
|
+
});
|
|
96
|
+
it("classifies a TLS cert error and falls back to unknown otherwise", () => {
|
|
97
|
+
expect(classifyImapError(imapErr("self signed certificate in chain"))).toBe("tls");
|
|
98
|
+
expect(classifyImapError(imapErr("something else entirely"))).toBe("unknown");
|
|
99
|
+
});
|
|
100
|
+
it("a code-bearing timeout to a host containing 'ssl' is timeout, not tls", () => {
|
|
101
|
+
// The message embeds the hostname; the TLS message regex must not steal it
|
|
102
|
+
// because the code-based timeout branch runs first.
|
|
103
|
+
expect(classifyImapError(imapErr("connect ETIMEDOUT imap.ssl-mail.com:993", { code: "ETIMEDOUT" }))).toBe("timeout");
|
|
104
|
+
});
|
|
105
|
+
it("a refused error whose text mentions authentication is refused, not auth", () => {
|
|
106
|
+
// Structured auth fields are absent; the code-based refused branch beats the
|
|
107
|
+
// message-only auth fallback.
|
|
108
|
+
expect(classifyImapError(imapErr("connect ECONNREFUSED (authentication failed downstream)", { code: "ECONNREFUSED" }))).toBe("refused");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe("testConnection — classification maps to actionable messages", () => {
|
|
112
|
+
it("the live auth shape returns the auth message, not the catch-all", async () => {
|
|
113
|
+
armImapFlow({
|
|
114
|
+
connect: vi.fn().mockRejectedValue(imapErr("Command failed", {
|
|
115
|
+
authenticationFailed: true,
|
|
116
|
+
serverResponseCode: "AUTHENTICATIONFAILED",
|
|
117
|
+
responseText: "Authentication failed.",
|
|
118
|
+
})),
|
|
119
|
+
});
|
|
120
|
+
const msg = await testConnection(config, "bad-password");
|
|
121
|
+
expect(msg).toMatch(/IMAP authentication failed for a@b\.test/);
|
|
122
|
+
expect(msg).not.toMatch(/Command failed/);
|
|
123
|
+
});
|
|
124
|
+
it("ECONNREFUSED via err.code returns the refused message", async () => {
|
|
125
|
+
armImapFlow({ connect: vi.fn().mockRejectedValue(imapErr("Command failed", { code: "ECONNREFUSED" })) });
|
|
126
|
+
expect(await testConnection(config, "pw")).toMatch(/IMAP connection refused/);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("testConnection — socket-level late-error containment", () => {
|
|
130
|
+
it("a late 'error' on the underlying socket after a failed probe does not throw", async () => {
|
|
131
|
+
armImapFlow({
|
|
132
|
+
connect: vi.fn().mockRejectedValue(imapErr("Command failed", { authenticationFailed: true, serverResponseCode: "AUTHENTICATIONFAILED" })),
|
|
133
|
+
});
|
|
134
|
+
await testConnection(config, "bad-password");
|
|
135
|
+
const client = state.clients[0];
|
|
136
|
+
// The raw socket emits 'error' after the probe returned and the census
|
|
137
|
+
// dropped to baseline — this is the path that previously crashed the process.
|
|
138
|
+
expect(() => client.socket.emit("error", new Error("late TLS reset"))).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
it("a late 'error' on the socket after a successful probe does not throw", async () => {
|
|
141
|
+
armImapFlow();
|
|
142
|
+
await testConnection(config, "good-password");
|
|
143
|
+
const client = state.clients[0];
|
|
144
|
+
expect(() => client.socket.emit("error", new Error("late FIN"))).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("testConnection — observability", () => {
|
|
148
|
+
it("emits one imap-attempt line with host/port/security/outcome/reason and no password", async () => {
|
|
149
|
+
const writes = [];
|
|
150
|
+
const spy = vi.spyOn(process.stderr, "write").mockImplementation((s) => { writes.push(String(s)); return true; });
|
|
151
|
+
armImapFlow({
|
|
152
|
+
connect: vi.fn().mockRejectedValue(imapErr("Command failed", { authenticationFailed: true, serverResponseCode: "AUTHENTICATIONFAILED" })),
|
|
153
|
+
});
|
|
154
|
+
await testConnection(config, "s3cr3t-password");
|
|
155
|
+
spy.mockRestore();
|
|
156
|
+
const attempts = writes.filter((l) => l.includes("[email] imap-attempt"));
|
|
157
|
+
expect(attempts).toHaveLength(1);
|
|
158
|
+
expect(attempts[0]).toContain("host=imap.b.test");
|
|
159
|
+
expect(attempts[0]).toContain("port=993");
|
|
160
|
+
expect(attempts[0]).toContain("security=tls");
|
|
161
|
+
expect(attempts[0]).toContain("outcome=fail");
|
|
162
|
+
expect(attempts[0]).toContain("reason=auth");
|
|
163
|
+
expect(writes.join("")).not.toContain("s3cr3t-password");
|
|
164
|
+
});
|
|
165
|
+
it("a successful probe emits outcome=ok reason=none", async () => {
|
|
166
|
+
const writes = [];
|
|
167
|
+
const spy = vi.spyOn(process.stderr, "write").mockImplementation((s) => { writes.push(String(s)); return true; });
|
|
168
|
+
armImapFlow();
|
|
169
|
+
await testConnection(config, "good-password");
|
|
170
|
+
spy.mockRestore();
|
|
171
|
+
const attempts = writes.filter((l) => l.includes("[email] imap-attempt"));
|
|
172
|
+
expect(attempts).toHaveLength(1);
|
|
173
|
+
expect(attempts[0]).toContain("outcome=ok");
|
|
174
|
+
expect(attempts[0]).toContain("reason=none");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
69
177
|
//# sourceMappingURL=imap-probe.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imap-probe.test.js","sourceRoot":"","sources":["../../src/__tests__/imap-probe.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,wEAAwE;AACxE,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,EAAW,EAAE,CAAC,CAAC,CAAC;AAC3D,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAEnD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;AAC9C,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"imap-probe.test.js","sourceRoot":"","sources":["../../src/__tests__/imap-probe.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -65,6 +65,19 @@ export declare function writeConfig(accountId: string, config: EmailConfig): Pro
|
|
|
65
65
|
* the `contact-erase` cascade, which this function does not touch.
|
|
66
66
|
*/
|
|
67
67
|
export declare function removeConfig(accountId: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Stamp the EmailAccount node with the time of a verified IMAP connection.
|
|
70
|
+
* Called from every successful IMAP probe (email-setup, email-status) so a
|
|
71
|
+
* mailbox that has authenticated at least once is distinguishable from one that
|
|
72
|
+
* never has. No-op when no EmailAccount node exists for the account.
|
|
73
|
+
*/
|
|
74
|
+
export declare function recordSuccessfulConnect(accountId: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Read the last verified-IMAP-connection timestamp for an account.
|
|
77
|
+
* Returns null when the account has no EmailAccount node, or one that has never
|
|
78
|
+
* recorded a successful connect (the never-connected state).
|
|
79
|
+
*/
|
|
80
|
+
export declare function readLastSuccessfulConnect(accountId: string): Promise<string | null>;
|
|
68
81
|
/**
|
|
69
82
|
* Read poll state from the EmailAccount node.
|
|
70
83
|
* Returns defaults if properties are not yet set (first run).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAeA,mFAAmF;AACnF,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAwBD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,KAAK,GAAG,UAAU,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,KAAK,GAAG,UAAU,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAwClD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAInE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMlD;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAwB/E;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CvF;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUnE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CA2BhF;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,WAAW,EAAE,eAAe,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/D"}
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAeA,mFAAmF;AACnF,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAwBD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,KAAK,GAAG,UAAU,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,KAAK,GAAG,UAAU,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAwClD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAInE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMlD;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAwB/E;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CvF;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUnE;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW9E;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAczF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CA2BhF;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,WAAW,EAAE,eAAe,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/D"}
|