feat(FN-2520): add remoteAccess auth link token mode support

- Extend project settings schema/types with remoteAccess defaults and auth link token mode fields
- Update settings store patch handling to deep-merge remoteAccess updates without clobbering sibling keys
- Add dashboard/API wiring for remoteAccess controls, including legacy settings route handling
- Expand core and dashboard tests for remoteAccess settings behavior, merge semantics, and UI coverage
- Align settings reference docs with the implemented remoteAccess schema and options
This commit is contained in:
Fusion
2026-04-26 03:55:57 -07:00
committed by gsxdsm
parent b0c6b14022
commit 8a261ae052
10 changed files with 560 additions and 32 deletions

View File

@@ -12986,6 +12986,84 @@ describe("PUT /settings", () => {
expect(store.updateSettings).toHaveBeenCalledWith({ maxConcurrent: 8, autoMerge: false });
});
it("accepts partial remoteAccess patches and GET /settings returns merged sibling branches", async () => {
const mergedRemoteAccess = {
enabled: true,
activeProvider: "tailscale",
providers: {
tailscale: {
enabled: true,
hostname: "tail.example.ts.net",
targetPort: 5173,
acceptRoutes: true,
},
cloudflare: {
enabled: false,
tunnelName: "existing-tunnel",
tunnelToken: null,
ingressUrl: "",
},
},
tokenStrategy: {
persistent: {
enabled: true,
token: null,
},
shortLived: {
enabled: false,
ttlMs: 900000,
maxTtlMs: 86400000,
},
},
lifecycle: {
rememberLastRunning: false,
wasRunningOnShutdown: false,
lastRunningProvider: null,
},
};
const updatedSettings = {
...DEFAULT_SETTINGS,
remoteAccess: mergedRemoteAccess,
};
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
const app = buildApp();
const patch = {
remoteAccess: {
activeProvider: "tailscale",
providers: {
tailscale: {
enabled: true,
hostname: "tail.example.ts.net",
targetPort: 5173,
acceptRoutes: true,
},
},
},
};
const updateRes = await REQUEST(
app,
"PUT",
"/api/settings",
JSON.stringify(patch),
{ "Content-Type": "application/json" },
);
expect(updateRes.status).toBe(200);
expect(store.updateSettings).toHaveBeenCalledWith(patch);
const getRes = await GET(app, "/api/settings");
expect(getRes.status).toBe(200);
expect(getRes.body.remoteAccess.providers.tailscale.hostname).toBe("tail.example.ts.net");
expect(getRes.body.remoteAccess.providers.cloudflare.tunnelName).toBe("existing-tunnel");
expect(getRes.body.remoteAccess.tokenStrategy.persistent.enabled).toBe(true);
expect(getRes.body.remoteAccess.lifecycle.rememberLastRunning).toBe(false);
});
it("validates auto-archive age settings", async () => {
const res = await REQUEST(
buildApp(),
@@ -13379,6 +13457,54 @@ describe("GET /settings/scopes", () => {
expect(res.body.project.titleSummarizerModelId).toBe("gemini-2.5-pro");
});
it("returns remoteAccess only under project scope", async () => {
(store.getSettingsByScope as ReturnType<typeof vi.fn>).mockResolvedValue({
global: { themeMode: "dark" },
project: {
remoteAccess: {
enabled: true,
activeProvider: "tailscale",
providers: {
tailscale: {
enabled: true,
hostname: "tail.example.ts.net",
targetPort: 5173,
acceptRoutes: true,
},
cloudflare: {
enabled: false,
tunnelName: "",
tunnelToken: null,
ingressUrl: "",
},
},
tokenStrategy: {
persistent: {
enabled: true,
token: null,
},
shortLived: {
enabled: false,
ttlMs: 900000,
maxTtlMs: 86400000,
},
},
lifecycle: {
rememberLastRunning: false,
wasRunningOnShutdown: false,
lastRunningProvider: null,
},
},
},
});
const res = await GET(buildApp(), "/api/settings/scopes");
expect(res.status).toBe(200);
expect(res.body.project.remoteAccess).toBeDefined();
expect(res.body.global.remoteAccess).toBeUndefined();
});
it("returns 500 on store error", async () => {
(store.getSettingsByScope as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Failed"));