feat(FN-1747): merge fusion/fn-1747

This commit is contained in:
gsxdsm
2026-04-14 11:43:52 -07:00
parent 831477e6ea
commit 102af7f4f2
11 changed files with 286 additions and 37 deletions

View File

@@ -243,6 +243,106 @@ describe("WebSocketManager", () => {
expect(manager.getClientCount()).toBe(0);
expect(manager.getSubscribedTaskIds()).toEqual([]);
});
describe("project-scoped channel isolation", () => {
it("subscribe uses scoped channel keys - same task ID in different projects", () => {
const manager = new WebSocketManager();
const socketA = new MockSocket();
const socketB = new MockSocket();
// Add clients bound to different project scopes
manager.addClient(socketA as unknown as WebSocket, "client-1", "project-a");
manager.addClient(socketB as unknown as WebSocket, "client-2", "project-b");
// Subscribe to the same task ID in different projects
manager.subscribe("client-1", "FN-063", "project-a");
manager.subscribe("client-2", "FN-063", "project-b");
// Each project should have its own subscription count
expect(manager.getSubscriptionCount("FN-063", "project-a")).toBe(1);
expect(manager.getSubscriptionCount("FN-063", "project-b")).toBe(1);
// Unscoped query should return 0 (no unscoped subscriptions)
expect(manager.getSubscriptionCount("FN-063")).toBe(0);
});
it("broadcastBadgeUpdate does not leak across projects with same task ID", () => {
const manager = new WebSocketManager();
const clientA = new MockSocket();
const clientB = new MockSocket();
manager.addClient(clientA as unknown as WebSocket, "client-a", "project-a");
manager.addClient(clientB as unknown as WebSocket, "client-b", "project-b");
// Subscribe both to the same task ID in their respective projects
manager.subscribe("client-a", "FN-063", "project-a");
manager.subscribe("client-b", "FN-063", "project-b");
// Broadcast for project-a only
manager.broadcastBadgeUpdate("FN-063", {
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "merged", title: "Project A PR", headBranch: "feat", baseBranch: "main", commentCount: 0 },
timestamp: "2026-03-30T12:00:00.000Z",
}, "project-a");
// Only project-a client should receive the update
expect(clientA.send).toHaveBeenCalledTimes(1);
expect(clientB.send).not.toHaveBeenCalled();
const messageA = JSON.parse(clientA.sent[0]);
expect(messageA).toMatchObject({
type: "badge:updated",
taskId: "FN-063",
prInfo: { status: "merged", title: "Project A PR" },
});
});
it("unsubscribe is scoped to project", () => {
const manager = new WebSocketManager();
const socket = new MockSocket();
manager.addClient(socket as unknown as WebSocket, "client-1", "project-a");
// Subscribe in two different project scopes
manager.subscribe("client-1", "FN-063", "project-a");
manager.subscribe("client-1", "FN-063", "project-b");
// project-a subscription should exist
expect(manager.getSubscriptionCount("FN-063", "project-a")).toBe(1);
// project-b subscription should also exist
expect(manager.getSubscriptionCount("FN-063", "project-b")).toBe(1);
// Unsubscribe from project-a only
manager.unsubscribe("client-1", "FN-063", "project-a");
// project-a should be gone, project-b should remain
expect(manager.getSubscriptionCount("FN-063", "project-a")).toBe(0);
expect(manager.getSubscriptionCount("FN-063", "project-b")).toBe(1);
});
it("getSubscribedTaskIds returns scoped results", () => {
const manager = new WebSocketManager();
const socket = new MockSocket();
manager.addClient(socket as unknown as WebSocket, "client-1", "project-x");
// Subscribe to different tasks in different projects
manager.subscribe("client-1", "FN-001", "project-x");
manager.subscribe("client-1", "FN-002", "project-x");
manager.subscribe("client-1", "FN-003", "project-y");
// Get task IDs for project-x only
const projectXTasks = manager.getSubscribedTaskIds("project-x");
expect(projectXTasks).toContain("FN-001");
expect(projectXTasks).toContain("FN-002");
expect(projectXTasks).not.toContain("FN-003");
// Get task IDs for project-y only
const projectYTasks = manager.getSubscribedTaskIds("project-y");
expect(projectYTasks).toContain("FN-003");
expect(projectYTasks).not.toContain("FN-001");
expect(projectYTasks).not.toContain("FN-002");
});
});
});
describe("/api/ws integration", () => {