Files
fusion/packages/dashboard/src/__tests__/routes-nodes-sync.test.ts
Fusion 05329fb059 feat(FN-3707): implement mesh auth snapshot replication across core, engine
The merge delivers two major features: mesh auth snapshot synchronization across nodes (FN-3707 steps 2/4/5, including core types, central-core plumbing, peer-exchange updates, and settings/node sync route helpers) and a bundled roadmap plugin with chat UX improvements (FN-3162, FN-3756, FN-3771, FN

Fusion-Task-Id: FN-3707
2026-05-08 18:26:14 -07:00

949 lines
31 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request, get } from "../test-request.js";
import { createServer } from "../server.js";
import { resetRuntimeLogSink, setRuntimeLogSink, type RuntimeLogContext } from "../runtime-logger.js";
// Mock node:fs for auth.json reading
vi.mock("node:fs", () => ({
default: {
readFileSync: vi.fn().mockReturnValue(JSON.stringify({
anthropic: { type: "api_key", key: "sk-ant-test123" },
openai: { type: "api_key", key: "sk-test456" },
})),
existsSync: vi.fn().mockReturnValue(true),
},
readFileSync: vi.fn().mockReturnValue(JSON.stringify({
anthropic: { type: "api_key", key: "sk-ant-test123" },
openai: { type: "api_key", key: "sk-test456" },
})),
existsSync: vi.fn().mockReturnValue(true),
}));
// ── Mock @fusion/core for node routes ─────────────────────────────────
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockClose = vi.fn().mockResolvedValue(undefined);
const mockListNodes = vi.fn();
const mockGetNode = vi.fn();
const mockGetLocalPeerInfo = vi.fn();
const mockGetSettingsSyncState = vi.fn();
const mockUpdateSettingsSyncState = vi.fn();
const mockApplyRemoteSettings = vi.fn();
const mockGetSettingsForSync = vi.fn();
const mockGetAuthMaterialSnapshot = vi.fn();
const mockApplyAuthMaterialSnapshot = vi.fn();
const mockChatStoreInit = vi.fn().mockResolvedValue(undefined);
const mockAgentStoreInit = vi.fn().mockResolvedValue(undefined);
const mockAgentStoreGetAgent = vi.fn().mockResolvedValue(null);
vi.mock("@fusion/core", () => {
return {
CentralCore: class MockCentralCore {
init = mockInit;
close = mockClose;
listNodes = mockListNodes;
getNode = mockGetNode;
getLocalPeerInfo = mockGetLocalPeerInfo;
getSettingsSyncState = mockGetSettingsSyncState;
updateSettingsSyncState = mockUpdateSettingsSyncState;
applyRemoteSettings = mockApplyRemoteSettings;
getSettingsForSync = mockGetSettingsForSync;
getAuthMaterialSnapshot = mockGetAuthMaterialSnapshot;
applyAuthMaterialSnapshot = mockApplyAuthMaterialSnapshot;
},
ChatStore: class MockChatStore {
init = mockChatStoreInit;
},
AgentStore: class MockAgentStore {
init = mockAgentStoreInit;
getAgent = mockAgentStoreGetAgent;
},
};
});
// ── Mock AuthStorage ───────────────────────────────────────────────────
const mockAuthStorageSet = vi.fn();
const mockAuthStorageGetOAuthProviders = vi.fn().mockReturnValue([]);
vi.mock("@mariozechner/pi-coding-agent", () => {
return {
AuthStorage: {
create: vi.fn(() => ({
set: mockAuthStorageSet,
get: vi.fn(),
getApiKey: vi.fn(),
getOAuthProviders: mockAuthStorageGetOAuthProviders,
reload: vi.fn(),
})),
},
};
});
// ── Mock Store ────────────────────────────────────────────────────────
class MockStore extends EventEmitter {
getRootDir(): string {
return "/tmp/fn-1821-test";
}
getFusionDir(): string {
return "/tmp/fn-1821-test/.fusion";
}
getDatabase() {
return {
exec: vi.fn(),
prepare: vi.fn().mockReturnValue({
run: vi.fn().mockReturnValue({ changes: 0 }),
get: vi.fn(),
all: vi.fn().mockReturnValue([]),
}),
};
}
async getSettingsByScope() {
return {
global: { defaultProvider: "anthropic", defaultModelId: "claude-3-5-sonnet" },
project: { maxConcurrent: 2 },
};
}
getGlobalSettingsStore() {
return {
async getSettings() {
return { defaultProvider: "anthropic", defaultModelId: "claude-3-5-sonnet" };
},
};
}
}
// ── Test helpers ──────────────────────────────────────────────────────
function createMockRemoteNode(overrides: Record<string, unknown> = {}) {
return {
id: "node-remote-001",
name: "Remote Node",
type: "remote" as const,
status: "online" as const,
url: "http://192.168.1.100:3001",
apiKey: "test-api-key-123",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
...overrides,
};
}
function createMockLocalNode(overrides: Record<string, unknown> = {}) {
return {
id: "node-local-001",
name: "Local Node",
type: "local" as const,
status: "online" as const,
url: null,
apiKey: "local-api-key-456",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
...overrides,
};
}
// ── Tests ─────────────────────────────────────────────────────────────
interface RuntimeEvent {
level: "info" | "warn" | "error";
scope: string;
message: string;
context?: RuntimeLogContext;
}
describe("Node settings sync routes", () => {
let store: MockStore;
let app: ReturnType<typeof createServer>;
let mockFetch: ReturnType<typeof vi.fn>;
let runtimeEvents: RuntimeEvent[];
beforeEach(async () => {
vi.clearAllMocks();
mockInit.mockResolvedValue(undefined);
mockClose.mockResolvedValue(undefined);
mockListNodes.mockResolvedValue([]);
mockGetNode.mockResolvedValue(null);
mockGetLocalPeerInfo.mockResolvedValue({ nodeId: "node-local-001", nodeName: "Local Node" });
mockGetSettingsSyncState.mockResolvedValue(null);
mockUpdateSettingsSyncState.mockResolvedValue({});
mockApplyRemoteSettings.mockResolvedValue({ success: true, globalCount: 1, projectCount: 1, authCount: 0 });
mockGetSettingsForSync.mockResolvedValue({});
mockGetAuthMaterialSnapshot.mockReturnValue({
version: 1,
exportedAt: "2026-04-14T10:00:00.000Z",
checksum: "auth-checksum",
payload: { providerAuth: { anthropic: { type: "api_key", key: "sk-ant-test" } } },
});
mockApplyAuthMaterialSnapshot.mockReturnValue({
success: true,
authCount: 1,
providerAuth: { anthropic: { type: "api_key", key: "sk-ant-received" } },
});
mockAuthStorageSet.mockResolvedValue(undefined);
mockAuthStorageGetOAuthProviders.mockReturnValue([]);
// Mock global fetch for remote node calls
mockFetch = vi.fn();
global.fetch = mockFetch;
runtimeEvents = [];
setRuntimeLogSink((level, scope, message, context) => {
runtimeEvents.push({ level, scope, message, context });
});
store = new MockStore();
app = createServer(store as any);
});
afterEach(() => {
resetRuntimeLogSink();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
// ── GET /api/nodes/:id/settings ──────────────────────────────────────
describe("GET /api/nodes/:id/settings", () => {
it("returns remote settings scopes for valid remote node", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ global: { test: "global" }, project: { test: "project" } }),
});
const res = await get(app, "/api/nodes/node-remote-001/settings");
expect(res.status).toBe(200);
expect(res.body).toEqual({ global: { test: "global" }, project: { test: "project" } });
expect(mockFetch).toHaveBeenCalledWith(
"http://192.168.1.100:3001/api/settings/scopes",
expect.objectContaining({
method: "GET",
headers: { Authorization: "Bearer test-api-key-123", "Content-Type": "application/json" },
}),
);
});
it("returns 404 for unknown node", async () => {
mockGetNode.mockResolvedValue(null);
const res = await get(app, "/api/nodes/unknown/settings");
expect(res.status).toBe(404);
expect(res.body.error).toContain("not found");
});
it("returns 400 for local node", async () => {
const localNode = createMockLocalNode();
mockGetNode.mockResolvedValue(localNode);
const res = await get(app, "/api/nodes/node-local-001/settings");
expect(res.status).toBe(400);
expect(res.body.error).toContain("local node");
});
it("returns 502 when remote returns non-200", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: false,
status: 500,
});
const res = await get(app, "/api/nodes/node-remote-001/settings");
expect(res.status).toBe(502);
expect(res.body.error).toContain("500");
});
it("returns 504 when remote is unreachable", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockRejectedValue(new Error("Network error"));
const res = await get(app, "/api/nodes/node-remote-001/settings");
expect(res.status).toBe(504);
expect(res.body.error).toContain("unreachable");
});
});
// ── POST /api/nodes/:id/settings/push ────────────────────────────────
describe("POST /api/nodes/:id/settings/push", () => {
it("successfully pushes local settings to remote", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/settings/push",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.syncedFields).toContain("defaultProvider");
expect(mockFetch).toHaveBeenCalledWith(
"http://192.168.1.100:3001/api/settings/sync-receive",
expect.objectContaining({
method: "POST",
headers: { Authorization: "Bearer test-api-key-123", "Content-Type": "application/json" },
}),
);
});
it("returns 404 for unknown node", async () => {
mockGetNode.mockResolvedValue(null);
const res = await request(
app,
"POST",
"/api/nodes/unknown/settings/push",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(404);
});
it("returns 400 for local node", async () => {
const localNode = createMockLocalNode();
mockGetNode.mockResolvedValue(localNode);
const res = await request(
app,
"POST",
"/api/nodes/node-local-001/settings/push",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("local node");
});
it("returns 400 for remote node without apiKey", async () => {
const remoteNode = createMockRemoteNode({ apiKey: undefined });
mockGetNode.mockResolvedValue(remoteNode);
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/settings/push",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("apiKey");
});
it("records sync state after successful push", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/settings/push",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(mockUpdateSettingsSyncState).toHaveBeenCalledWith(
"node-remote-001",
expect.objectContaining({
lastSyncedAt: expect.any(String),
}),
);
});
});
// ── POST /api/nodes/:id/settings/pull ───────────────────────────────
describe("POST /api/nodes/:id/settings/pull", () => {
it("successfully pulls and applies remote settings with default conflict resolution", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
global: { defaultProvider: "openai" },
project: { maxConcurrent: 3 },
}),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/settings/pull",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(mockApplyRemoteSettings).toHaveBeenCalled();
});
it("returns diff without applying when conflictResolution is manual", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
global: { defaultProvider: "openai" },
project: { maxConcurrent: 3 },
}),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/settings/pull",
JSON.stringify({ conflictResolution: "manual" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.diff).toBeDefined();
expect(res.body.remoteSettings).toBeDefined();
expect(res.body.localSettings).toBeDefined();
expect(mockApplyRemoteSettings).not.toHaveBeenCalled();
});
it("returns 404 for unknown node", async () => {
mockGetNode.mockResolvedValue(null);
const res = await request(
app,
"POST",
"/api/nodes/unknown/settings/pull",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(404);
});
it("records sync state after successful pull", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
global: { defaultProvider: "openai" },
project: { maxConcurrent: 3 },
}),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/settings/pull",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(mockUpdateSettingsSyncState).toHaveBeenCalledWith(
"node-remote-001",
expect.objectContaining({
lastSyncedAt: expect.any(String),
}),
);
});
});
// ── GET /api/nodes/:id/settings/sync-status ─────────────────────────
describe("GET /api/nodes/:id/settings/sync-status", () => {
it("returns sync status with diff summary when remote is reachable", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockGetSettingsSyncState.mockResolvedValue({
nodeId: "node-local-001",
remoteNodeId: "node-remote-001",
lastSyncedAt: "2026-04-14T10:00:00.000Z",
localChecksum: "abc123",
remoteChecksum: "def456",
syncCount: 5,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-04-14T10:00:00.000Z",
});
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
global: { defaultProvider: "openai" },
project: { maxConcurrent: 3 },
}),
});
const res = await get(app, "/api/nodes/node-remote-001/settings/sync-status");
expect(res.status).toBe(200);
expect(res.body.lastSyncAt).toBe("2026-04-14T10:00:00.000Z");
expect(res.body.remoteReachable).toBe(true);
expect(res.body.diff).toBeDefined();
});
it("returns remoteReachable false with empty diff when remote is down", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockGetSettingsSyncState.mockResolvedValue(null);
mockFetch.mockRejectedValue(new Error("Network error"));
const res = await get(app, "/api/nodes/node-remote-001/settings/sync-status");
expect(res.status).toBe(200);
expect(res.body.remoteReachable).toBe(false);
expect(res.body.diff.global).toEqual([]);
expect(res.body.diff.project).toEqual([]);
});
it("returns 404 for unknown node", async () => {
mockGetNode.mockResolvedValue(null);
const res = await get(app, "/api/nodes/unknown/settings/sync-status");
expect(res.status).toBe(404);
});
it("returns null timestamps when no sync has occurred", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockGetSettingsSyncState.mockResolvedValue(null);
mockFetch.mockRejectedValue(new Error("Network error"));
const res = await get(app, "/api/nodes/node-remote-001/settings/sync-status");
expect(res.status).toBe(200);
expect(res.body.lastSyncAt).toBe(null);
});
});
// ── POST /api/nodes/:id/auth/sync ───────────────────────────────────
describe("POST /api/nodes/:id/auth/sync", () => {
it("successfully pushes auth credentials to remote (push mode)", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/auth/sync",
JSON.stringify({ direction: "push" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
// The actual providers depend on what's in ~/.pi/agent/auth.json
// We just verify the sync completed successfully
expect(Array.isArray(res.body.syncedProviders)).toBe(true);
});
it("returns 404 for unknown node", async () => {
mockGetNode.mockResolvedValue(null);
const res = await request(
app,
"POST",
"/api/nodes/unknown/auth/sync",
JSON.stringify({ direction: "push" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(404);
});
it("returns 400 for remote node without apiKey", async () => {
const remoteNode = createMockRemoteNode({ apiKey: undefined });
mockGetNode.mockResolvedValue(remoteNode);
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/auth/sync",
JSON.stringify({ direction: "push" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("apiKey");
});
it("emits structured redacted diagnostics for push-mode auth sync", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/auth/sync",
JSON.stringify({ direction: "push" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
const authEvent = runtimeEvents.find((event) =>
event.message === "Auth sync diagnostic event"
&& event.context?.route === "/nodes/:id/auth/sync"
&& event.context?.direction === "push"
);
expect(authEvent).toMatchObject({
level: "info",
message: "Auth sync diagnostic event",
context: expect.objectContaining({
operation: "sync",
direction: "push",
route: "/nodes/:id/auth/sync",
sourceNodeId: "node-local-001",
targetNodeId: "node-remote-001",
providerNames: res.body.syncedProviders,
providerCount: res.body.syncedProviders.length,
}),
});
expect(authEvent?.scope.endsWith("routes:settings-sync:auth")).toBe(true);
expect(authEvent?.context).toHaveProperty("targetNodeId", "node-remote-001");
const serialized = JSON.stringify(authEvent);
expect(serialized).not.toContain("sk-");
expect(serialized).not.toContain("Bearer ");
expect(serialized).not.toContain("\"key\"");
expect(serialized).not.toContain("\"access\"");
expect(serialized).not.toContain("\"refresh\"");
});
it("emits structured redacted diagnostics for pull-mode auth sync", async () => {
const remoteNode = createMockRemoteNode();
mockGetNode.mockResolvedValue(remoteNode);
mockApplyAuthMaterialSnapshot.mockReturnValueOnce({
success: true,
authCount: 1,
providerAuth: {
google: { type: "api_key", key: "sk-pull-secret-123" },
},
});
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
authMaterial: {
version: 1,
exportedAt: "2026-04-14T10:00:00.000Z",
checksum: "auth-checksum",
payload: {
providerAuth: {
google: { type: "api_key", key: "sk-pull-secret-123" },
},
},
},
sourceNodeId: "node-other",
timestamp: "2026-04-14T10:00:00.000Z",
}),
});
const res = await request(
app,
"POST",
"/api/nodes/node-remote-001/auth/sync",
JSON.stringify({ direction: "pull" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.syncedProviders).toContain("google");
const authEvent = runtimeEvents.find((event) =>
event.message === "Auth sync diagnostic event"
&& event.context?.route === "/nodes/:id/auth/sync"
&& event.context?.direction === "pull"
);
expect(authEvent).toMatchObject({
level: "info",
message: "Auth sync diagnostic event",
context: expect.objectContaining({
operation: "sync",
direction: "pull",
route: "/nodes/:id/auth/sync",
sourceNodeId: "node-other",
targetNodeId: "node-local-001",
providerNames: ["google"],
providerCount: 1,
}),
});
expect(authEvent?.scope.endsWith("routes:settings-sync:auth")).toBe(true);
expect(authEvent?.context).toHaveProperty("sourceNodeId", "node-other");
expect(authEvent?.context).toHaveProperty("targetNodeId", "node-local-001");
const serialized = JSON.stringify(authEvent);
expect(serialized).not.toContain("sk-pull-secret-123");
expect(serialized).not.toContain("Bearer ");
expect(serialized).not.toContain("\"key\"");
expect(serialized).not.toContain("\"access\"");
expect(serialized).not.toContain("\"refresh\"");
});
});
// ── POST /api/settings/sync-receive ─────────────────────────────────
describe("POST /api/settings/sync-receive", () => {
it("successfully receives and applies settings", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
mockApplyRemoteSettings.mockResolvedValue({
success: true,
globalCount: 2,
projectCount: 1,
authCount: 0,
});
const payload = {
global: { defaultProvider: "anthropic" },
projects: {},
exportedAt: "2026-04-14T10:00:00.000Z",
checksum: "abc123",
version: 1,
};
const res = await request(
app,
"POST",
"/api/settings/sync-receive",
JSON.stringify({ ...payload, sourceNodeId: "node-remote-001" }),
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(mockApplyRemoteSettings).toHaveBeenCalled();
});
it("returns 401 when auth header is missing", async () => {
const res = await request(
app,
"POST",
"/api/settings/sync-receive",
JSON.stringify({ sourceNodeId: "node-remote-001", exportedAt: "2026-04-14T10:00:00.000Z" }),
{ "content-type": "application/json" },
);
expect(res.status).toBe(401);
});
it("returns 401 when apiKey doesn't match", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
const res = await request(
app,
"POST",
"/api/settings/sync-receive",
JSON.stringify({ sourceNodeId: "node-remote-001", exportedAt: "2026-04-14T10:00:00.000Z" }),
{ "content-type": "application/json", "Authorization": "Bearer wrong-key" },
);
expect(res.status).toBe(401);
});
it("returns 400 when payload is missing sourceNodeId", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
const res = await request(
app,
"POST",
"/api/settings/sync-receive",
JSON.stringify({ exportedAt: "2026-04-14T10:00:00.000Z" }),
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("sourceNodeId");
});
});
// ── POST /api/settings/auth-receive ──────────────────────────────────
describe("POST /api/settings/auth-receive", () => {
it("successfully receives auth credentials", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
const res = await request(
app,
"POST",
"/api/settings/auth-receive",
JSON.stringify({
authMaterial: {
version: 1,
exportedAt: "2026-04-14T10:00:00.000Z",
checksum: "auth-checksum",
payload: { providerAuth: { anthropic: { type: "api_key", key: "sk-ant-received" } } },
},
sourceNodeId: "node-remote-001",
timestamp: "2026-04-14T10:00:00.000Z",
}),
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.receivedProviders).toContain("anthropic");
});
it("returns 401 when auth header is missing", async () => {
const res = await request(
app,
"POST",
"/api/settings/auth-receive",
JSON.stringify({
authMaterial: {
version: 1,
exportedAt: "2026-04-14T10:00:00.000Z",
checksum: "auth-checksum",
payload: { providerAuth: { anthropic: { type: "api_key", key: "sk-ant" } } },
},
sourceNodeId: "node-remote-001",
timestamp: "2026-04-14T10:00:00.000Z",
}),
{ "content-type": "application/json" },
);
expect(res.status).toBe(401);
});
it("returns 400 when payload is malformed", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
const res = await request(
app,
"POST",
"/api/settings/auth-receive",
JSON.stringify({ authMaterial: "not-an-object" }),
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
);
expect(res.status).toBe(400);
});
it("emits structured redacted diagnostics for auth-receive", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
const res = await request(
app,
"POST",
"/api/settings/auth-receive",
JSON.stringify({
authMaterial: {
version: 1,
exportedAt: "2026-04-14T10:00:00.000Z",
checksum: "auth-checksum",
payload: { providerAuth: { anthropic: { type: "api_key", key: "sk-ant-secret" } } },
},
sourceNodeId: "node-remote-001",
timestamp: "2026-04-14T10:00:00.000Z",
}),
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
);
expect(res.status).toBe(200);
const authEvent = runtimeEvents.find((event) =>
event.message === "Auth sync diagnostic event"
&& event.context?.route === "/settings/auth-receive"
&& event.context?.direction === "receive"
);
expect(authEvent).toMatchObject({
level: "info",
message: "Auth sync diagnostic event",
context: {
operation: "receive",
direction: "receive",
route: "/settings/auth-receive",
sourceNodeId: "node-remote-001",
providerNames: ["anthropic"],
providerCount: 1,
},
});
expect(authEvent?.scope.endsWith("routes:settings-sync:auth")).toBe(true);
expect(authEvent?.context).not.toHaveProperty("targetNodeId");
const serialized = JSON.stringify(authEvent);
expect(serialized).not.toContain("sk-ant-secret");
expect(serialized).not.toContain("Bearer ");
expect(serialized).not.toContain("\"key\"");
expect(serialized).not.toContain("\"access\"");
expect(serialized).not.toContain("\"refresh\"");
});
});
// ── GET /api/settings/auth-export ────────────────────────────────────
describe("GET /api/settings/auth-export", () => {
it("returns auth credentials for authenticated request", async () => {
const localNode = createMockLocalNode();
mockListNodes.mockResolvedValue([localNode]);
mockGetLocalPeerInfo.mockResolvedValue({ nodeId: "node-local-001", nodeName: "Local Node" });
const res = await request(
app,
"GET",
"/api/settings/auth-export",
undefined,
{ "Authorization": `Bearer ${localNode.apiKey}` },
);
expect(res.status).toBe(200);
expect(res.body.authMaterial).toBeDefined();
expect(res.body.sourceNodeId).toBe("node-local-001");
// The actual providers depend on what's in ~/.pi/agent/auth.json
// Just verify we got a providerAuth snapshot payload
expect(typeof res.body.authMaterial.payload.providerAuth).toBe("object");
});
it("returns 401 when auth header is missing", async () => {
const res = await get(app, "/api/settings/auth-export");
expect(res.status).toBe(401);
});
});
});