/** * @file Tests for the Remote Data Sources feature: input validation + command * builders in server/lib/remote-sync.js, the /api/remote-sources route CRUD, and * the source-scoped data filter threaded through the sessions/events/agents/ * stats/analytics endpoints. The actual SSH/rsync transfer is not exercised * (that needs a live remote); everything up to and around it is. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const path = require("path"); const fs = require("fs"); const os = require("os"); const http = require("http"); // Isolate the DB and disable background pollers/probes before loading server. const TEST_DB = path.join(os.tmpdir(), `dashboard-remote-test-${Date.now()}-${process.pid}.db`); process.env.DASHBOARD_DB_PATH = TEST_DB; process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); const { db, stmts } = require("../db"); const remoteSync = require("../lib/remote-sync"); const sourceFilter = require("../lib/source-filter"); let server; let BASE; function fetchJson(urlPath, options = {}) { return new Promise((resolve, reject) => { const url = new URL(urlPath, BASE); const opts = { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: options.method || "GET", headers: { "Content-Type": "application/json", ...options.headers }, }; const req = http.request(opts, (res) => { let body = ""; res.on("data", (c) => (body += c)); res.on("end", () => { let parsed; try { parsed = JSON.parse(body); } catch { parsed = body; } resolve({ status: res.statusCode, body: parsed }); }); }); req.on("error", reject); if (options.body) req.write(JSON.stringify(options.body)); req.end(); }); } const get = (p) => fetchJson(p); const post = (p, body) => fetchJson(p, { method: "POST", body }); const patch = (p, body) => fetchJson(p, { method: "PATCH", body }); const del = (p) => fetchJson(p, { method: "DELETE" }); before(async () => { const app = createApp(); server = await startServer(app, 0); BASE = `http://127.0.0.1:${server.address().port}`; }); after(() => { if (server) server.close(); if (db) db.close(); for (const suffix of ["", "-wal", "-shm"]) { try { fs.unlinkSync(TEST_DB + suffix); } catch { /* ignore */ } } }); // ── Validation ──────────────────────────────────────────────────────────────── describe("remote-sync validateSourceInput", () => { it("accepts a valid full config and expands ~ in identity_file", () => { const v = remoteSync.validateSourceInput({ label: "Dev Box", host: "son@dev.local", ssh_port: 22, identity_file: "~/.ssh/id_ed25519", remote_home: "~/.claude", }); assert.equal(v.label, "Dev Box"); assert.equal(v.host, "son@dev.local"); assert.equal(v.sshPort, 22); assert.ok(path.isAbsolute(v.identityFile)); assert.equal(v.remoteHome, "~/.claude"); }); it("accepts a config-alias host with no user", () => { const v = remoteSync.validateSourceInput({ label: "x", host: "mybox" }); assert.equal(v.host, "mybox"); }); const rejects = [ [ "leading-dash host (ssh option injection)", { label: "x", host: "-oProxyCommand=evil" }, "INVALID_HOST", ], ["host with space", { label: "x", host: "a b" }, "INVALID_HOST"], ["host with ;", { label: "x", host: "a;rm -rf /" }, "INVALID_HOST"], ["host with : (breaks scp spec)", { label: "x", host: "a:b" }, "INVALID_HOST"], ["missing label", { host: "a" }, "INVALID_LABEL"], ["port out of range", { label: "x", host: "a", ssh_port: 99999 }, "INVALID_PORT"], [ "remote_home with ..", { label: "x", host: "a", remote_home: "~/../etc" }, "INVALID_REMOTE_HOME", ], [ "relative remote_home", { label: "x", host: "a", remote_home: "rel/path" }, "INVALID_REMOTE_HOME", ], [ "identity_file with newline", { label: "x", host: "a", identity_file: "/a\nb" }, "INVALID_IDENTITY_FILE", ], ]; for (const [name, input, code] of rejects) { it(`rejects ${name}`, () => { assert.throws( () => remoteSync.validateSourceInput(input), (err) => err.code === code ); }); } it("allows hyphens inside an identity_file path", () => { const v = remoteSync.validateSourceInput({ label: "x", host: "a", identity_file: "/home/u/.ssh/id-ed25519", }); assert.equal(v.identityFile, "/home/u/.ssh/id-ed25519"); }); }); describe("remote-sync command builders", () => { it("builds ssh option args with port + identity", async () => { const args = await remoteSync.sshOptionArgs({ ssh_port: 2222, identity_file: "/k" }); assert.ok(args.includes("-p")); assert.equal(args[args.indexOf("-p") + 1], "2222"); assert.ok(args.includes("-i")); assert.equal(args[args.indexOf("-i") + 1], "/k"); assert.ok(args.includes("IdentitiesOnly=yes")); }); it("builds scp option args with capital -P for port", async () => { const args = await remoteSync.scpOptionArgs({ ssh_port: 2222, identity_file: "/k" }); assert.ok(args.includes("-P")); assert.equal(args[args.indexOf("-P") + 1], "2222"); }); it("parses ssh -G output for identity agent discovery", () => { const cfg = remoteSync.parseSshGOutput( "hostname example.com\nidentityagent /tmp/agent.sock\nport 22\n" ); assert.equal(cfg.identityagent, "/tmp/agent.sock"); assert.equal(cfg.port, "22"); }); it("buildSshChildEnv sets HOME and does not override SSH_AUTH_SOCK", () => { const env = remoteSync.buildSshChildEnv(); assert.ok(env.HOME); if (process.env.SSH_AUTH_SOCK) { assert.equal(env.SSH_AUTH_SOCK, process.env.SSH_AUTH_SOCK); } }); it("identityAgentArgsFromConfig follows ssh -G only for concrete agent paths", () => { assert.deepEqual(remoteSync.identityAgentArgsFromConfig("none"), []); assert.deepEqual(remoteSync.identityAgentArgsFromConfig("SSH_AUTH_SOCK"), []); assert.deepEqual(remoteSync.identityAgentArgsFromConfig("/tmp/custom-agent.sock"), [ "-o", "IdentityAgent=/tmp/custom-agent.sock", ]); const home = os.homedir(); assert.deepEqual(remoteSync.identityAgentArgsFromConfig("~/Library/agent.sock"), [ "-o", `IdentityAgent=${path.join(home, "Library/agent.sock")}`, ]); }); it("adds PowerShell and WSL probes for ~-rooted remote homes (Windows remotes)", () => { const probes = remoteSync.connectionProbeCommands({ remote_home: "~/.claude" }); assert.equal(probes.length, 3); assert.match(probes[0], /sh -c/); assert.match(probes[0], /~\/\.claude\/projects/); assert.match(probes[1], /powershell\.exe/); assert.match(probes[1], /\.claude\\projects/); assert.match(probes[2], /wsl\.exe/); assert.match(probes[2], /~\/\.claude\/projects/); }); it("uses only wsl.exe for wsl: remote homes", () => { const probes = remoteSync.connectionProbeCommands({ remote_home: "wsl:~/.claude" }); assert.equal(probes.length, 1); assert.match(probes[0], /wsl\.exe/); }); it("connectionSuccessMessage reflects explicit vs auto-detected WSL", () => { const wslProbe = "wsl.exe -e sh -c 'test -d ~/.claude/projects && echo CCAM_OK'"; assert.match( remoteSync.connectionSuccessMessage({ remote_home: "wsl:~/.claude" }, wslProbe), /wsl:~\/\.claude/ ); assert.doesNotMatch( remoteSync.connectionSuccessMessage({ remote_home: "wsl:~/.claude" }, wslProbe), /auto-detected/i ); assert.match( remoteSync.connectionSuccessMessage({ remote_home: null }, wslProbe), /auto-detected/i ); assert.match( remoteSync.connectionSuccessMessage({ remote_home: null }, "sh -c 'echo CCAM_OK'"), /Remote Claude Code history found/ ); }); it("accepts wsl: and UNC remote_home values", () => { const wsl = remoteSync.validateSourceInput({ label: "WSL", host: "u@win", remote_home: "wsl:/home/hoang/.claude", }); assert.equal(wsl.remoteHome, "wsl:/home/hoang/.claude"); assert.equal( remoteSync.remoteProjectsPath({ remote_home: wsl.remoteHome }), "wsl:/home/hoang/.claude/projects" ); const unc = remoteSync.validateSourceInput({ label: "UNC", host: "u@win", remote_home: "//wsl.localhost/Ubuntu/home/hoang/.claude", }); assert.equal(unc.remoteHome, "//wsl.localhost/Ubuntu/home/hoang/.claude"); assert.equal( remoteSync.remoteProjectsPath({ remote_home: unc.remoteHome }), "//wsl.localhost/Ubuntu/home/hoang/.claude/projects" ); }); it("builds a wsl tar command for WSL-hosted Claude homes", () => { assert.equal( remoteSync.wslTarRemoteCmd("~/.claude"), "wsl.exe -e sh -c 'tar -cC ~/.claude/projects .'" ); }); it("adds cmd.exe probe only for Windows drive-letter remote homes", () => { const probes = remoteSync.connectionProbeCommands({ remote_home: "C:/Users/hoang/.claude", }); assert.equal(probes.length, 1); assert.match(probes[0], /cmd \/c/); assert.match(probes[0], /C:\\Users\\hoang\\.claude\\projects/); }); it("uses sh probe for POSIX absolute remote homes", () => { const probes = remoteSync.connectionProbeCommands({ remote_home: "/opt/cc" }); assert.deepEqual(probes, [ "sh -c 'test -d /opt/cc/projects && echo CCAM_OK || echo CCAM_NO_DIR'", ]); }); it("accepts Windows-style remote_home with forward slashes", () => { const v = remoteSync.validateSourceInput({ label: "Win", host: "u@win", remote_home: "C:/Users/hoang/.claude", }); assert.equal(v.remoteHome, "C:/Users/hoang/.claude"); assert.equal( remoteSync.remoteProjectsPath({ remote_home: v.remoteHome }), "C:/Users/hoang/.claude/projects" ); assert.equal( remoteSync.scpRemoteSpec({ host: "u@win", remote_home: v.remoteHome }), "u@win:C:/Users/hoang/.claude/projects/." ); }); it("expands tilde in ssh -G IdentityAgent paths and strips quotes", () => { const home = os.homedir(); assert.equal( remoteSync.expandSshConfigPath("~/Library/agent.sock"), path.join(home, "Library/agent.sock") ); assert.equal(remoteSync.expandSshConfigPath('"/tmp/quoted.sock"'), "/tmp/quoted.sock"); assert.equal(remoteSync.expandSshConfigPath("/tmp/a"), "/tmp/a"); }); it("sshConfigFileArgs points at user config when present", () => { const args = remoteSync.sshConfigFileArgs(); const cfg = path.join(os.homedir(), ".ssh", "config"); if (fs.existsSync(cfg)) { assert.deepEqual(args, ["-F", cfg]); } else { assert.deepEqual(args, []); } }); it("treats blank identity_file as null", () => { const v = remoteSync.validateSourceInput({ label: "x", host: "a", identity_file: " ", }); assert.equal(v.identityFile, null); }); it("detects legacy scp protocol errors for -O retry", () => { assert.equal( remoteSync.isLegacyScpProtocolError("subsystem request failed on channel 0"), true ); assert.equal(remoteSync.isLegacyScpProtocolError("Permission denied"), false); }); it("strips ANSI escapes from command output", () => { assert.equal(remoteSync.stripAnsi("\u001b[31;1mscp: not found\u001b[0m"), "scp: not found"); }); it("resolves ssh/scp binaries on Windows when OpenSSH is in System32", () => { const prev = process.platform; const prevWin = process.env.WINDIR; try { Object.defineProperty(process, "platform", { value: "win32" }); process.env.WINDIR = process.env.WINDIR || "C:\\Windows"; const ssh = remoteSync.resolveSshBinary("ssh"); assert.match(ssh, /ssh\.exe$/i); } finally { Object.defineProperty(process, "platform", { value: prev }); if (prevWin === undefined) delete process.env.WINDIR; else process.env.WINDIR = prevWin; } }); it("defaults the remote projects path to ~/.claude/projects", () => { assert.equal(remoteSync.remoteProjectsPath({}), "~/.claude/projects"); assert.equal(remoteSync.remoteProjectsPath({ remote_home: "/opt/cc" }), "/opt/cc/projects"); }); it("identifies top-level session ids in a mirrored tree (skips subagents)", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-staged-")); const proj = path.join(dir, "-Users-x-proj"); fs.mkdirSync(path.join(proj, "sess-1", "subagents"), { recursive: true }); fs.writeFileSync(path.join(proj, "sess-1.jsonl"), "{}\n"); fs.writeFileSync(path.join(proj, "sess-2.jsonl"), "{}\n"); fs.writeFileSync(path.join(proj, "sess-1", "subagents", "agent-abc.jsonl"), "{}\n"); const ids = remoteSync.stagedSessionIds(dir).sort(); assert.deepEqual(ids, ["sess-1", "sess-2"]); fs.rmSync(dir, { recursive: true, force: true }); }); }); // ── source-filter helper ──────────────────────────────────────────────────── describe("source-filter helper", () => { it("parses the sources csv, deduped; empty/absent → null", () => { assert.deepEqual(sourceFilter.parseSources({ query: { sources: "local, a ,a,," } }), [ "local", "a", ]); assert.equal(sourceFilter.parseSources({ query: {} }), null); assert.equal(sourceFilter.parseSources({ query: { sources: " ,, " } }), null); }); it("builds a column clause and a subquery clause", () => { assert.deepEqual(sourceFilter.sourceColumnClause(["local", "a"]), { clause: "s.source IN (?,?)", params: ["local", "a"], }); assert.deepEqual(sourceFilter.sessionIdInSourcesClause(["local"], "e.session_id"), { clause: "e.session_id IN (SELECT id FROM sessions WHERE source IN (?))", params: ["local"], }); assert.deepEqual(sourceFilter.sourceColumnClause(null), { clause: "", params: [] }); }); }); // ── Route CRUD ────────────────────────────────────────────────────────────── describe("/api/remote-sources CRUD", () => { let createdId; it("starts empty", async () => { const res = await get("/api/remote-sources"); assert.equal(res.status, 200); assert.deepEqual(res.body.sources, []); }); it("creates a source", async () => { const res = await post("/api/remote-sources", { label: "Dev", host: "son@dev", ssh_port: 22 }); assert.equal(res.status, 201); assert.equal(res.body.source.label, "Dev"); assert.equal(res.body.source.host, "son@dev"); assert.equal(res.body.source.enabled, true); assert.equal(res.body.source.status, "idle"); assert.ok(res.body.source.id.startsWith("src_")); createdId = res.body.source.id; }); it("rejects an invalid host with a 400 + structured error", async () => { const res = await post("/api/remote-sources", { label: "Bad", host: "-oProxyCommand=x" }); assert.equal(res.status, 400); assert.equal(res.body.error.code, "INVALID_HOST"); }); it("patches label + enabled, leaving other fields intact", async () => { const res = await patch(`/api/remote-sources/${createdId}`, { label: "Renamed", enabled: false, }); assert.equal(res.status, 200); assert.equal(res.body.source.label, "Renamed"); assert.equal(res.body.source.enabled, false); assert.equal(res.body.source.ssh_port, 22); // unchanged }); it("404s for an unknown id", async () => { const res = await patch("/api/remote-sources/src_nope", { label: "x" }); assert.equal(res.status, 404); }); it("delete without purge detaches its sessions back to local", async () => { // Attach a session to the source, then delete without purge. stmts.insertSession.run("rs-detach-1", "s", "active", "/x", "claude-opus-4-8", null); stmts.setSessionSource.run(createdId, "rs-detach-1"); const res = await del(`/api/remote-sources/${createdId}`); assert.equal(res.status, 200); assert.equal(res.body.purged, 0); assert.equal(stmts.getSession.get("rs-detach-1").source, "local"); assert.equal(stmts.getRemoteSource.get(createdId), undefined); }); it("delete with purge removes the source's sessions", async () => { const c = await post("/api/remote-sources", { label: "P", host: "p@h" }); const id = c.body.source.id; stmts.insertSession.run("rs-purge-1", "s", "active", "/x", "claude-opus-4-8", null); stmts.setSessionSource.run(id, "rs-purge-1"); const res = await del(`/api/remote-sources/${id}?purge=true`); assert.equal(res.status, 200); assert.equal(res.body.purged, 1); assert.equal(stmts.getSession.get("rs-purge-1"), undefined); }); }); // ── Source-scoped data endpoints ────────────────────────────────────────────── describe("source scoping across data endpoints", () => { before(async () => { // A local session and a remote-tagged session, each with one event. await post("/api/hooks/event", { hook_type: "SessionStart", data: { session_id: "scope-local", cwd: "/local" }, }); await post("/api/hooks/event", { hook_type: "SessionStart", data: { session_id: "scope-remote", cwd: "/remote" }, }); stmts.setSessionSource.run("src_scope", "scope-remote"); }); it("facets lists distinct sources (local + tagged)", async () => { const res = await get("/api/sessions/facets"); assert.ok(res.body.sources.includes("local")); assert.ok(res.body.sources.includes("src_scope")); }); it("sessions?sources=local excludes the remote session", async () => { const res = await get("/api/sessions?sources=local&limit=1000"); const ids = res.body.sessions.map((s) => s.id); assert.ok(ids.includes("scope-local")); assert.ok(!ids.includes("scope-remote")); }); it("sessions?sources=src_scope returns only the remote session", async () => { const res = await get("/api/sessions?sources=src_scope&limit=1000"); const ids = res.body.sessions.map((s) => s.id); assert.deepEqual(ids, ["scope-remote"]); assert.equal(res.body.sessions[0].source, "src_scope"); }); it("sessions with no sources param returns both", async () => { const res = await get("/api/sessions?limit=1000"); const ids = res.body.sessions.map((s) => s.id); assert.ok(ids.includes("scope-local") && ids.includes("scope-remote")); }); it("stats respects the source scope", async () => { const all = await get("/api/stats"); const local = await get("/api/stats?sources=local"); const remote = await get("/api/stats?sources=src_scope"); assert.ok(all.body.total_sessions >= 2); assert.equal( local.body.total_sessions + remote.body.total_sessions <= all.body.total_sessions, true ); // The remote scope sees exactly its one tagged session. assert.equal(remote.body.total_sessions, 1); }); it("analytics respects the source scope", async () => { const remote = await get("/api/analytics?sources=src_scope"); assert.equal(remote.body.overview.total_sessions, 1); }); it("events?sources=src_scope only returns the remote session's events", async () => { const res = await get("/api/events?sources=src_scope&limit=1000"); assert.ok(res.body.events.every((e) => e.session_id === "scope-remote")); }); it("agents?sources=local excludes the remote session's agents", async () => { const res = await get("/api/agents?sources=local"); assert.ok(res.body.agents.every((a) => a.session_id !== "scope-remote")); }); it("pricing cost respects the source scope (regression: total cost was global)", async () => { // Equal usage on the local + the remote session; the default claude-opus-4-8 // pricing rule prices it. Before the fix, /pricing/cost ignored `sources`, so // every scope returned the same global total and the Dashboard cost never // moved when the scope changed. stmts.upsertTokenUsage.run("scope-local", "claude-opus-4-8", 1_000_000, 0, 0, 0); stmts.upsertTokenUsage.run("scope-remote", "claude-opus-4-8", 1_000_000, 0, 0, 0); const all = await get("/api/pricing/cost"); const local = await get("/api/pricing/cost?sources=local"); const remote = await get("/api/pricing/cost?sources=src_scope"); assert.ok(all.body.total_cost > 0, "some cost recorded across all sources"); assert.ok(local.body.total_cost > 0, "local scope has cost"); assert.ok(remote.body.total_cost > 0, "remote scope has cost"); // The fix: each scope is strictly less than the combined total. assert.ok(local.body.total_cost < all.body.total_cost, "local scope excludes remote cost"); assert.ok(remote.body.total_cost < all.body.total_cost, "remote scope excludes local cost"); // The two disjoint scopes partition the whole (no other opus-4-8 usage here). assert.ok( Math.abs(local.body.total_cost + remote.body.total_cost - all.body.total_cost) < 1e-6, "local + remote cost sums to the unscoped total" ); }); }); describe("/api/remote-sources session_count", () => { it("reports the live number of sessions attributed to each source", async () => { const c = await post("/api/remote-sources", { label: "Counted", host: "c@h" }); const id = c.body.source.id; // A freshly-added source has no sessions yet. assert.equal(c.body.source.session_count ?? 0, 0); // Tag two sessions to it, then confirm the list reflects the count. stmts.insertSession.run("rs-count-1", "s", "active", "/x", "claude-opus-4-8", null); stmts.insertSession.run("rs-count-2", "s", "active", "/y", "claude-opus-4-8", null); stmts.setSessionSource.run(id, "rs-count-1"); stmts.setSessionSource.run(id, "rs-count-2"); const list = await get("/api/remote-sources"); const row = list.body.sources.find((s) => s.id === id); assert.equal(row.session_count, 2); }); }); // ── Remote session status reconciliation ───────────────────────────────────── // A remote session gets NO live hooks and is excluded from every local liveness/ // staleness sweep, so its active/completed state is driven solely by whether its // mirrored transcript is still advancing. These exercise that reconciliation // directly against a staged tree with controlled mtimes. describe("reconcileRemoteSessionStatus", () => { const dbModule = require("../db"); const SRC = { id: "src_recon" }; let stageRoot; before(() => { stageRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-recon-")); }); after(() => { try { fs.rmSync(stageRoot, { recursive: true, force: true }); } catch { /* ignore */ } }); function stageSession(id, ageMs, contentLine = "{}") { const proj = path.join(stageRoot, "-Users-x-proj"); fs.mkdirSync(proj, { recursive: true }); const f = path.join(proj, `${id}.jsonl`); fs.writeFileSync(f, `${contentLine}\n`); const t = new Date(Date.now() - ageMs); fs.utimesSync(f, t, t); return f; } it("heals a wrongly-completed remote session whose mirror is still fresh", () => { stmts.insertSession.run( "recon-fresh", "s", "completed", "/home/ubuntu/matroid", "claude-opus-4-8", null ); stmts.setSessionSource.run(SRC.id, "recon-fresh"); stmts.insertAgent.run( "recon-fresh-main", "recon-fresh", "Main", "main", null, "completed", null, null, null ); stageSession("recon-fresh", 1_000); // 1s old → still running remoteSync.reconcileRemoteSessionStatus(dbModule, SRC, stageRoot); assert.equal(stmts.getSession.get("recon-fresh").status, "active"); assert.equal(stmts.getSession.get("recon-fresh").ended_at, null, "ended_at cleared on heal"); assert.equal(stmts.getAgent.get("recon-fresh-main").status, "waiting"); }); it("completes an active remote session whose mirror has gone stale", () => { stmts.insertSession.run( "recon-stale", "s", "active", "/home/ubuntu/other", "claude-opus-4-8", null ); stmts.setSessionSource.run(SRC.id, "recon-stale"); stmts.insertAgent.run( "recon-stale-main", "recon-stale", "Main", "main", null, "waiting", null, null, null ); stageSession("recon-stale", 20 * 60 * 1000); // 20 min idle → ended remoteSync.reconcileRemoteSessionStatus(dbModule, SRC, stageRoot); assert.equal(stmts.getSession.get("recon-stale").status, "completed"); assert.ok(stmts.getSession.get("recon-stale").ended_at, "ended_at stamped"); assert.equal(stmts.getAgent.get("recon-stale-main").status, "completed"); }); it("completes an active session when mtime is fresh but transcript content is stale", () => { stmts.insertSession.run( "recon-touch", "s", "active", "/home/ubuntu/touched", "claude-opus-4-8", null ); stmts.setSessionSource.run(SRC.id, "recon-touch"); stmts.insertAgent.run( "recon-touch-main", "recon-touch", "Main", "main", null, "waiting", null, null, null ); const stale = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); stageSession("recon-touch", 1_000, `{"timestamp":"${stale}"}`); remoteSync.reconcileRemoteSessionStatus(dbModule, SRC, stageRoot); assert.equal(stmts.getSession.get("recon-touch").status, "completed"); assert.equal(stmts.getAgent.get("recon-touch-main").status, "completed"); }); it("never touches a session owned by a different source", () => { stmts.insertSession.run( "recon-other", "s", "active", "/home/ubuntu/z", "claude-opus-4-8", null ); stmts.setSessionSource.run("src_different", "recon-other"); stageSession("recon-other", 20 * 60 * 1000); // stale, but not this source's remoteSync.reconcileRemoteSessionStatus(dbModule, SRC, stageRoot); assert.equal(stmts.getSession.get("recon-other").status, "active"); }); }); describe("POST /api/remote-sources/sync-all", () => { it("syncs only enabled sources and isolates per-source outcomes", async () => { // Disable every existing source so this exercises the wiring without any // real SSH/rsync shell-out (nothing enabled → nothing to pull). const { body } = await get("/api/remote-sources"); for (const s of body.sources) { if (s.enabled) await patch(`/api/remote-sources/${s.id}`, { enabled: false }); } const res = await post("/api/remote-sources/sync-all"); assert.equal(res.status, 200); assert.equal(res.body.ok, true); assert.equal(res.body.synced, 0); assert.deepEqual(res.body.results, []); }); it("does not collide with the /:id/sync route", async () => { // "sync-all" is a single path segment, so it must not be treated as an :id. const res = await post("/api/remote-sources/sync-all"); assert.equal(res.status, 200); assert.ok(Array.isArray(res.body.results)); }); });