diff --git a/src/cli/auth.ts b/src/cli/auth.ts index 0681f16..d2d7708 100644 --- a/src/cli/auth.ts +++ b/src/cli/auth.ts @@ -166,6 +166,15 @@ export function readStoredAccountAddress(): string | null { return readStoredAuthFile()?.address ?? null; } +/** + * The saved account for Personal Server ownership checks, even after expiry. + * An env token has unknown identity and overrides the saved account. This + * address is never authorization; tokens keep their own validity checks. + */ +export function loadAccountAddressForOwnership(): string | null { + return process.env.VANA_SESSION_TOKEN ? null : readStoredAccountAddress(); +} + /** * Raw peek at auth.json ignoring expiry: the previous address and the * previous personal_server block. Login uses it to detect an account switch diff --git a/src/cli/index.ts b/src/cli/index.ts index 559a71f..a616957 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -170,6 +170,7 @@ import { isNewerVersion, } from "./update-check.js"; import { + loadAccountAddressForOwnership, loadCredentials, loadPersonalServerSession, readStoredAuthFile, @@ -1832,14 +1833,14 @@ async function runConnect( const credentials = loadCredentials(); const mismatch = personalServerOwnerMismatch( target.health?.owner, - credentials?.account?.address, + loadAccountAddressForOwnership(), ); if (mismatch) { // An expired login names whoever signed in last, which may be long // gone; say that, rather than claim a current identity. renderer?.detail( - credentials && isExpired(credentials) - ? `Your vana login has expired, so the CLI cannot check that the Personal Server at ${target.url} (owner ${formatAddress(mismatch.owner)}) is yours. Run \`vana login\` to check.` + !credentials + ? `Your vana login has expired or is unavailable, so the CLI cannot check that the Personal Server at ${target.url} (owner ${formatAddress(mismatch.owner)}) is yours. Run \`vana login\` to check.` : `The Personal Server at ${target.url} belongs to ${formatAddress(mismatch.owner)}, but you are signed in as ${formatAddress(mismatch.account)}.`, ); if (cannotAskAboutForeignServer(options)) { @@ -4766,12 +4767,22 @@ async function runCollectAll(options: GlobalOptions): Promise { // may be another account's. const foreign = personalServerOwnerMismatch( target.health?.owner, - loadCredentials()?.account?.address, + loadAccountAddressForOwnership(), ); - if (target.state === "available" && foreign && !options.json) { - process.stderr.write( - `Pending data not synced: the Personal Server at ${target.url} belongs to ${formatAddress(foreign.owner)}, not to you. Start yours with \`vana server start\`.\n`, - ); + let blockedPendingCount = 0; + if (target.state === "available" && foreign) { + const after = await readCliState(); + const error = `Pending data not synced: the Personal Server at ${target.url} belongs to ${formatAddress(foreign.owner)}, not to you. Start yours with \`vana server start\`.`; + for (const [source, stored] of Object.entries(after.sources)) { + if (!shouldRetryPendingSource(stored, "automatic")) continue; + blockedPendingCount++; + const prior = results.get(source)?.outcome; + if (prior === "collect_failed" || prior === "sync_failed") continue; + results.set(source, { source, outcome: "sync_pending", error }); + } + if (blockedPendingCount > 0 && !options.json) { + process.stderr.write(`${error}\n`); + } } if (target.state === "available" && !foreign) { const synced = await syncPendingSources(target, "automatic"); @@ -4806,7 +4817,9 @@ async function runCollectAll(options: GlobalOptions): Promise { const message = syncedPendingCount > 0 ? `Synced ${syncedPendingCount} pending dataset(s).` - : "No sources are due for collection."; + : blockedPendingCount > 0 + ? `${blockedPendingCount} pending dataset(s) not synced: the Personal Server belongs to another account.` + : "No sources are due for collection."; process.stdout.write( `${JSON.stringify({ message, count: 0, syncedPendingCount, sources: sourceResults })}\n`, ); @@ -4868,7 +4881,7 @@ async function runServerSync(options: GlobalOptions): Promise { const mismatch = personalServerOwnerMismatch( target.health?.owner, - loadCredentials()?.account?.address, + loadAccountAddressForOwnership(), ); if (mismatch) { let useIt = false; diff --git a/src/cli/schedule.ts b/src/cli/schedule.ts index 216c9d9..b11c565 100644 --- a/src/cli/schedule.ts +++ b/src/cli/schedule.ts @@ -360,9 +360,10 @@ export function classifyCollectedSource( /** * The exit code for a whole `collect --all`: OK only when every source it - * touched collected and synced. When the only problem is that no Personal - * Server answered, that is {@link CliExitCode.SERVER_UNAVAILABLE}; anything - * else is {@link CliExitCode.FAILURE}. + * touched collected and synced. When the only problem is sync pending because + * no usable Personal Server answered, that is + * {@link CliExitCode.SERVER_UNAVAILABLE}; anything else is + * {@link CliExitCode.FAILURE}. */ export function collectAllExitCode( results: readonly CollectAllSourceResult[], @@ -384,13 +385,13 @@ export function describeCollectAllFailures( ok: "ok", collect_failed: "collection failed", sync_failed: "sync failed", - sync_pending: "not synced, no Personal Server answered", + sync_pending: "sync pending", }; return [ `${failures.length} of ${results.length} source(s) did not collect and sync:`, ...failures.map( (r) => - ` ${r.source}: ${label[r.outcome]}${r.error ? ` (${r.error})` : ""}`, + ` ${r.source}: ${r.outcome === "sync_pending" && !r.error ? "not synced, no Personal Server answered" : label[r.outcome]}${r.error ? ` (${r.error})` : ""}`, ), ]; } diff --git a/src/personal-server/index.ts b/src/personal-server/index.ts index 8566cf4..e308a61 100644 --- a/src/personal-server/index.ts +++ b/src/personal-server/index.ts @@ -6,7 +6,10 @@ import { resolveScopes } from "./scope-resolver.js"; import { createPersonalServerClient } from "./client.js"; import { readCachedConnectorMetadata } from "../connectors/registry.js"; import { getConnectorCacheDir } from "../core/paths.js"; -import { loadCredentials, loadPersonalServerSession } from "../cli/auth.js"; +import { + loadAccountAddressForOwnership, + loadPersonalServerSession, +} from "../cli/auth.js"; export { createPersonalServerClient } from "./client.js"; export type { @@ -96,7 +99,7 @@ export async function detectPersonalServerTarget(): Promise { owner: "0x99Bf14e94DE7edB022E08528C5Cdb627f73A988d", }, }); - mockLoadCredentials.mockReturnValue({ - account: { - address: "0xbffbd3316ef8c6d8b8046151228c09840ae08d48", - session_token: "expired", - expires_at: "2026-01-01T00:00:00.000Z", - }, - personal_server: null, - }); + mockLoadCredentials.mockReturnValue(null); + mockReadFileSync.mockReturnValue( + JSON.stringify({ + account: { + address: "0xbffbd3316ef8c6d8b8046151228c09840ae08d48", + session_token: "expired", + expires_at: "2026-01-01T00:00:00.000Z", + }, + personal_server: null, + }), + ); mockConfirm.mockResolvedValueOnce(false); const { runCli } = await import("../../src/cli/index.js"); const exitCode = await runCli(["node", "vana", "connect", "github"]); expect(exitCode).toBe(7); + // copy-assertion-ok: expired credentials must not present a past account as currently signed in. expect(stderr).toContain("Your vana login has expired"); expect(stderr).toContain("Run `vana login` to check."); expect(stderr).not.toContain("you are signed in as"); @@ -2639,20 +2643,24 @@ describe("runCli", () => { owner: "0x99Bf14e94DE7edB022E08528C5Cdb627f73A988d", }, }); - mockLoadCredentials.mockReturnValue({ - account: { - address: "0xbffbd3316ef8c6d8b8046151228c09840ae08d48", - session_token: "current", - expires_at: "2999-01-01T00:00:00.000Z", - }, - personal_server: null, - }); + mockLoadCredentials.mockReturnValue(null); + mockReadFileSync.mockReturnValue( + JSON.stringify({ + account: { + address: "0xbffbd3316ef8c6d8b8046151228c09840ae08d48", + session_token: "current", + expires_at: "2999-01-01T00:00:00.000Z", + }, + personal_server: null, + }), + ); mockConfirm.mockResolvedValueOnce(false); const { runCli } = await import("../../src/cli/index.js"); const exitCode = await runCli(["node", "vana", "connect", "github"]); expect(exitCode).toBe(7); + // copy-assertion-ok: the current-login warning identifies the live account, unlike the expired-login warning. expect(stderr).toContain("but you are signed in as"); mockLoadCredentials.mockReset(); mockLoadCredentials.mockReturnValue(null); @@ -2684,16 +2692,39 @@ describe("runCli", () => { { id: "github", name: "GitHub", authMode: "interactive" }, ]); mockDetectPersonalServerTarget.mockResolvedValue(foreignTarget); - mockLoadCredentials.mockReturnValue(signedIn as never); + mockLoadCredentials.mockReturnValue(null); }); afterEach(() => { mockLoadCredentials.mockReset(); mockLoadCredentials.mockReturnValue(null); }); - it.each([["--no-input"], ["--yes"], ["--ipc"]])( - "connect %s refuses to store data there", - async (flag) => { + function selectLogin(login: string) { + mockLoadCredentials.mockReturnValue(null); + mockReadFileSync.mockImplementation((file) => { + if (!/auth(?:\..+)?\.json$/.test(String(file))) + throw new Error("missing"); + return JSON.stringify({ + ...signedIn, + account: { + ...signedIn.account, + expires_at: + login === "expired" + ? "2000-01-01T00:00:00.000Z" + : signedIn.account.expires_at, + }, + }); + }); + } + + it.each( + ["current", "expired"].flatMap((login) => + ["--no-input", "--yes", "--ipc"].map((flag) => [login, flag]), + ), + )( + "%s login, connect %s refuses to store data there", + async (login, flag) => { + selectLogin(login); const { runCli } = await import("../../src/cli/index.js"); const exitCode = await runCli([ "node", @@ -2718,55 +2749,148 @@ describe("runCli", () => { }, ); - it("a scheduled collect --all stores nothing there and says why", async () => { - mockReadCliState.mockResolvedValue({ - version: 1, - sources: { - github: { - connectorInstalled: true, - exportFrequency: "daily", - lastCollectedAt: "2020-01-01T00:00:00.000Z", - lastResultPath: "/tmp/results/github.json", - dataState: "collected_local", + it.each(["current", "expired"])( + "a scheduled collect --all stores nothing there with %s login", + async (login) => { + selectLogin(login); + mockReadCliState.mockResolvedValue({ + version: 1, + sources: { + github: { + connectorInstalled: true, + exportFrequency: "daily", + lastCollectedAt: "2020-01-01T00:00:00.000Z", + lastResultPath: "/tmp/results/github.json", + dataState: "collected_local", + }, }, - }, - }); - const previous = process.env.VANA_SCHEDULED_RUN; - process.env.VANA_SCHEDULED_RUN = "1"; - try { + }); + const previous = process.env.VANA_SCHEDULED_RUN; + process.env.VANA_SCHEDULED_RUN = "1"; + try { + const { runCli } = await import("../../src/cli/index.js"); + const exitCode = await runCli([ + "node", + "vana", + "collect", + "--all", + "--quiet", + "--no-input", + ]); + + expect(exitCode).toBe(5); + expect(mockIngestResult).not.toHaveBeenCalled(); + expect(managedFetchCalls).toBe(0); + } finally { + if (previous === undefined) delete process.env.VANA_SCHEDULED_RUN; + else process.env.VANA_SCHEDULED_RUN = previous; + } + }, + ); + + it.each(["current", "expired"])( + "collect --all refuses pending-only uploads with %s login", + async (login) => { + selectLogin(login); + mockReadCliState.mockResolvedValue({ + version: 1, + sources: { + github: { + connectorInstalled: true, + exportFrequency: "daily", + lastCollectedAt: "2999-01-01T00:00:00.000Z", + lastResultPath: "/tmp/results/github.json", + dataState: "collected_local", + }, + }, + }); const { runCli } = await import("../../src/cli/index.js"); const exitCode = await runCli([ "node", "vana", "collect", "--all", - "--quiet", "--no-input", + "--json", ]); - expect(exitCode).toBe(5); expect(mockIngestResult).not.toHaveBeenCalled(); - expect(stderr).toContain( - "Not storing your data: the Personal Server at http://localhost:8080 belongs to 0x99Bf...88d, not to you.", - ); - expect(stderr).toContain("Pending data not synced"); - } finally { - if (previous === undefined) delete process.env.VANA_SCHEDULED_RUN; - else process.env.VANA_SCHEDULED_RUN = previous; - } - }); + expect(JSON.parse(stdout.trim())).toMatchObject({ + syncedPendingCount: 0, + sources: [ + { + source: "github", + outcome: "sync_pending", + }, + ], + }); + }, + ); - it("server sync --no-input refuses; interactively it asks first", async () => { + it.each(["current", "expired"])( + "server sync refuses or asks first with %s login", + async (login) => { + selectLogin(login); + mockReadCliState.mockResolvedValue({ + version: 1, + sources: { + github: { + connectorInstalled: true, + lastResultPath: "/tmp/results/github.json", + dataState: "collected_local", + }, + }, + }); + const { runCli } = await import("../../src/cli/index.js"); + expect( + await runCli([ + "node", + "vana", + "server", + "sync", + "--no-input", + "--json", + ]), + ).toBe(5); + expect(JSON.parse(stdout.trim())).toMatchObject({ + error: "personal_server_not_yours", + owner: theirs, + }); + expect(mockIngestResult).not.toHaveBeenCalled(); + + stdout = ""; + mockConfirm.mockResolvedValueOnce(false); + expect(await runCli(["node", "vana", "server", "sync"])).toBe(5); + expect(mockConfirm).toHaveBeenCalledTimes(1); + expect(mockIngestResult).not.toHaveBeenCalled(); + + mockConfirm.mockResolvedValueOnce(true); + mockIngestResult.mockResolvedValue([ + { type: "ingest-complete", source: "github" }, + ]); + expect(await runCli(["node", "vana", "server", "sync"])).toBe(0); + expect(mockIngestResult).toHaveBeenCalledTimes(1); + }, + ); + + it("keeps same-owner sync usable after Account expiry", async () => { + selectLogin("expired"); + mockDetectPersonalServerTarget.mockResolvedValue({ + ...foreignTarget, + health: { ...foreignTarget.health, owner: signedIn.account.address }, + }); mockReadCliState.mockResolvedValue({ version: 1, sources: { github: { - connectorInstalled: true, lastResultPath: "/tmp/results/github.json", dataState: "collected_local", }, }, }); + mockIngestResult.mockResolvedValue([ + { type: "ingest-complete", source: "github" }, + ]); const { runCli } = await import("../../src/cli/index.js"); expect( await runCli([ @@ -2777,25 +2901,76 @@ describe("runCli", () => { "--no-input", "--json", ]), - ).toBe(5); + ).toBe(0); + expect(mockIngestResult).toHaveBeenCalledTimes(1); + expect(mockConfirm).not.toHaveBeenCalled(); + }); + + it.each(["env", "accountless"])( + "preserves %s identity's unattended sync", + async (identity) => { + if (identity === "env") { + selectLogin("expired"); + vi.stubEnv("VANA_SESSION_TOKEN", "env-token"); + } + mockReadCliState.mockResolvedValue({ + version: 1, + sources: { + github: { + lastResultPath: "/tmp/results/github.json", + dataState: "collected_local", + }, + }, + }); + mockIngestResult.mockResolvedValue([ + { type: "ingest-complete", source: "github" }, + ]); + const { runCli } = await import("../../src/cli/index.js"); + expect( + await runCli([ + "node", + "vana", + "server", + "sync", + "--no-input", + "--json", + ]), + ).toBe(0); + expect(mockIngestResult).toHaveBeenCalledTimes(1); + expect(mockConfirm).not.toHaveBeenCalled(); + }, + ); + + it("collect --all succeeds when a foreign server has no automatically retryable pending work", async () => { + selectLogin("expired"); + mockReadCliState.mockResolvedValue({ + version: 1, + sources: { + github: { + lastResultPath: "/tmp/results/github.json", + dataState: "ingest_failed", + }, + shop: { dataState: "collected_local" }, + }, + }); + const { runCli } = await import("../../src/cli/index.js"); + expect( + await runCli([ + "node", + "vana", + "collect", + "--all", + "--no-input", + "--json", + ]), + ).toBe(0); expect(JSON.parse(stdout.trim())).toMatchObject({ - error: "personal_server_not_yours", - owner: theirs, + count: 0, + syncedPendingCount: 0, + sources: [], }); expect(mockIngestResult).not.toHaveBeenCalled(); - - stdout = ""; - mockConfirm.mockResolvedValueOnce(false); - expect(await runCli(["node", "vana", "server", "sync"])).toBe(5); - expect(mockConfirm).toHaveBeenCalledTimes(1); - expect(mockIngestResult).not.toHaveBeenCalled(); - - mockConfirm.mockResolvedValueOnce(true); - mockIngestResult.mockResolvedValue([ - { type: "ingest-complete", source: "github" }, - ]); - expect(await runCli(["node", "vana", "server", "sync"])).toBe(0); - expect(mockIngestResult).toHaveBeenCalledTimes(1); + expect(stderr).toBe(""); }); }); diff --git a/test/cli/schedule.test.ts b/test/cli/schedule.test.ts index 097993d..3dfd82d 100644 --- a/test/cli/schedule.test.ts +++ b/test/cli/schedule.test.ts @@ -368,14 +368,23 @@ describe("collect --all outcome", () => { }); it("names each failed source for the log", () => { + // copy-assertion-ok: distinguish a foreign-owner refusal from no server answering in the human failure log. expect( describeCollectAllFailures([ { source: "a", outcome: "ok" }, { source: "b", outcome: "sync_failed", error: "HTTP 401" }, + { + source: "c", + outcome: "sync_pending", + error: "Server belongs to another account", + }, + { source: "d", outcome: "sync_pending" }, ]), ).toEqual([ - "1 of 2 source(s) did not collect and sync:", + "3 of 4 source(s) did not collect and sync:", " b: sync failed (HTTP 401)", + " c: sync pending (Server belongs to another account)", + " d: not synced, no Personal Server answered", ]); expect( describeCollectAllFailures([{ source: "a", outcome: "ok" }]), diff --git a/test/personal-server/index.test.ts b/test/personal-server/index.test.ts index df4a13d..9ddaa87 100644 --- a/test/personal-server/index.test.ts +++ b/test/personal-server/index.test.ts @@ -5,7 +5,6 @@ import { join } from "node:path"; const mocks = vi.hoisted(() => ({ readCliConfig: vi.fn(), - loadCredentials: vi.fn(), loadPersonalServerSession: vi.fn(), actualLoadPersonalServerSession: null as null | (() => unknown), })); @@ -21,7 +20,6 @@ vi.mock("../../src/cli/auth.js", async () => { ).loadPersonalServerSession; return { ...actual, - loadCredentials: mocks.loadCredentials, loadPersonalServerSession: mocks.loadPersonalServerSession, }; }); @@ -31,6 +29,7 @@ import { personalServerOwnerMismatch, resolvePersonalServerAuthConfig, } from "../../src/personal-server/index.js"; +import { getAuthFilePath, saveCredentials } from "../../src/cli/auth.js"; const originalEnv = { ...process.env }; const fetchMock = vi.fn(); @@ -38,8 +37,6 @@ const fetchMock = vi.fn(); beforeEach(() => { mocks.readCliConfig.mockReset(); mocks.readCliConfig.mockResolvedValue({}); - mocks.loadCredentials.mockReset(); - mocks.loadCredentials.mockReturnValue(null); mocks.loadPersonalServerSession.mockReset(); mocks.loadPersonalServerSession.mockReturnValue(null); fetchMock.mockReset(); @@ -154,6 +151,14 @@ describe("resolvePersonalServerAuthConfig", () => { }); describe("detectPersonalServerTarget", () => { + beforeEach(() => { + delete process.env.VANA_SESSION_TOKEN; + }); + + afterEach(async () => { + await rm(getAuthFilePath(), { force: true }); + }); + it("falls back from an unreachable saved URL to the authenticated personal server URL", async () => { mocks.readCliConfig.mockResolvedValue({ personalServerUrl: "https://dead.example.com", @@ -189,88 +194,94 @@ describe("detectPersonalServerTarget", () => { }); }); - it("prefers a scanned server the signed-in account owns over the first to answer", async () => { - const mine = "0x1234567890abcdef1234567890abcdef12345678"; - const theirs = "0x99bf14e94de7edb022e08528c5cdb627f73a988d"; - mocks.loadCredentials.mockReturnValue({ - account: { - address: mine, - session_token: "t", - expires_at: "2026-04-22T00:00:00.000Z", - }, - personal_server: null, - }); - - const health = (owner: string) => - new Response( - JSON.stringify({ - status: "healthy", - version: "1.0.0", - uptime: 1, - owner, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - // 8080 answers first but belongs to another identity. - fetchMock - .mockResolvedValueOnce(health(theirs)) - .mockResolvedValueOnce(health(mine)); - - await expect(detectPersonalServerTarget()).resolves.toMatchObject({ - url: "http://localhost:8081", - source: "scan", - health: { owner: mine }, - }); - }); + it.each(["2099-01-01T00:00:00.000Z", "2000-01-01T00:00:00.000Z"])( + "prefers a scanned server the saved account owns with Account expiry %s", + async (expiresAt) => { + const mine = "0x1234567890abcdef1234567890abcdef12345678"; + const theirs = "0x99bf14e94de7edb022e08528c5cdb627f73a988d"; + await saveCredentials({ + account: { + address: mine, + session_token: "t", + expires_at: expiresAt, + }, + personal_server: null, + }); - it("passes over a saved server of another account for one of ours", async () => { - // Two accounts' servers on one machine: auth.json still names A's. - const mine = "0xaff7000000000000000000000000000000000001"; - const theirs = "0x99bf14e94de7edb022e08528c5cdb627f73a988d"; - mocks.loadCredentials.mockReturnValue({ - account: { - address: mine, - session_token: "t", + const health = (owner: string) => + new Response( + JSON.stringify({ + status: "healthy", + version: "1.0.0", + uptime: 1, + owner, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + // 8080 answers first but belongs to another identity. + fetchMock + .mockResolvedValueOnce(health(theirs)) + .mockResolvedValueOnce(health(mine)); + + await expect(detectPersonalServerTarget()).resolves.toMatchObject({ + url: "http://localhost:8081", + source: "scan", + health: { owner: mine }, + }); + }, + ); + + it.each(["2099-01-01T00:00:00.000Z", "2000-01-01T00:00:00.000Z"])( + "passes over a saved foreign server with Account expiry %s", + async (expiresAt) => { + // Two accounts' servers on one machine: auth.json still names A's. + const mine = "0xaff7000000000000000000000000000000000001"; + const theirs = "0x99bf14e94de7edb022e08528c5cdb627f73a988d"; + await saveCredentials({ + account: { + address: mine, + session_token: "t", + expires_at: expiresAt, + }, + personal_server: null, + }); + mocks.loadPersonalServerSession.mockReturnValue({ + url: "http://localhost:8080", + session_token: "vana_ps_token", expires_at: "2999-01-01T00:00:00.000Z", - }, - personal_server: null, - }); - mocks.loadPersonalServerSession.mockReturnValue({ - url: "http://localhost:8080", - session_token: "vana_ps_token", - expires_at: "2999-01-01T00:00:00.000Z", - }); - const owners: Record = { - "http://localhost:8080/health": theirs, - "http://localhost:8082/health": mine, - }; - fetchMock.mockImplementation(async (input) => { - const owner = owners[String(input)]; - if (!owner) throw new Error("nothing there"); - return new Response( - JSON.stringify({ status: "healthy", version: "1", uptime: 1, owner }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }); + }); + const owners: Record = { + "http://localhost:8080/health": theirs, + "http://localhost:8082/health": mine, + }; + fetchMock.mockImplementation(async (input) => { + const owner = owners[String(input)]; + if (!owner) throw new Error("nothing there"); + return new Response( + JSON.stringify({ status: "healthy", version: "1", uptime: 1, owner }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }); - await expect(detectPersonalServerTarget()).resolves.toMatchObject({ - url: "http://localhost:8082", - source: "scan", - health: { owner: mine }, - }); + await expect(detectPersonalServerTarget()).resolves.toMatchObject({ + url: "http://localhost:8082", + source: "scan", + health: { owner: mine }, + }); - // With ours down, the saved one is still reported, as someone else's. - delete owners["http://localhost:8082/health"]; - await expect(detectPersonalServerTarget()).resolves.toMatchObject({ - url: "http://localhost:8080", - source: "auth", - health: { owner: theirs }, - }); - }); + // With ours down, the saved one is still reported, as someone else's. + delete owners["http://localhost:8082/health"]; + await expect(detectPersonalServerTarget()).resolves.toMatchObject({ + url: "http://localhost:8080", + source: "auth", + health: { owner: theirs }, + }); + }, + ); it("falls back to a server owned by someone else when nothing of ours answers", async () => { const theirs = "0x99bf14e94de7edb022e08528c5cdb627f73a988d"; - mocks.loadCredentials.mockReturnValue({ + await saveCredentials({ account: { address: "0x1234567890abcdef1234567890abcdef12345678", session_token: "t",