feat(KB-332): rename task prefix from KB to FN and branches from kb/ to fusion/
- Change default task prefix from KB to FN across all packages
- Rename branch naming pattern from kb/{id} to fusion/{id}
- Update all test assertions and fixtures to use new prefixes
- Add changeset for the breaking change
- Resolve merge conflicts in TaskCard.test.tsx touch gesture tests
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
function createValidMessage(overrides: Partial<BadgePubSubMessage> = {}): BadgePubSubMessage {
|
||||
return {
|
||||
sourceId: "server-a",
|
||||
taskId: "KB-001",
|
||||
taskId: "FN-001",
|
||||
timestamp: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
@@ -39,7 +39,7 @@ describe("InMemoryBadgePubSub", () => {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].taskId).toBe("KB-001");
|
||||
expect(messages[0].taskId).toBe("FN-001");
|
||||
});
|
||||
|
||||
it("handles multiple subscribers", async () => {
|
||||
@@ -146,7 +146,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value.taskId).toBe("KB-001");
|
||||
expect(result.value.taskId).toBe("FN-001");
|
||||
expect(result.value.sourceId).toBe("server-remote");
|
||||
}
|
||||
});
|
||||
@@ -161,7 +161,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
});
|
||||
|
||||
it("rejects messages without sourceId", () => {
|
||||
const json = JSON.stringify({ taskId: "KB-001", timestamp: new Date().toISOString() });
|
||||
const json = JSON.stringify({ taskId: "FN-001", timestamp: new Date().toISOString() });
|
||||
|
||||
const result = parseBadgePubSubMessage(json, localSourceId);
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
});
|
||||
|
||||
it("rejects messages with empty sourceId", () => {
|
||||
const json = JSON.stringify({ sourceId: "", taskId: "KB-001", timestamp: new Date().toISOString() });
|
||||
const json = JSON.stringify({ sourceId: "", taskId: "FN-001", timestamp: new Date().toISOString() });
|
||||
|
||||
const result = parseBadgePubSubMessage(json, localSourceId);
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
});
|
||||
|
||||
it("rejects messages without timestamp", () => {
|
||||
const json = JSON.stringify({ sourceId: "server-remote", taskId: "KB-001" });
|
||||
const json = JSON.stringify({ sourceId: "server-remote", taskId: "FN-001" });
|
||||
|
||||
const result = parseBadgePubSubMessage(json, localSourceId);
|
||||
|
||||
@@ -201,7 +201,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
});
|
||||
|
||||
it("rejects messages with empty timestamp", () => {
|
||||
const json = JSON.stringify({ sourceId: "server-remote", taskId: "KB-001", timestamp: "" });
|
||||
const json = JSON.stringify({ sourceId: "server-remote", taskId: "FN-001", timestamp: "" });
|
||||
|
||||
const result = parseBadgePubSubMessage(json, localSourceId);
|
||||
|
||||
@@ -252,7 +252,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
it("rejects messages with non-object prInfo", () => {
|
||||
const json = JSON.stringify({
|
||||
sourceId: "server-remote",
|
||||
taskId: "KB-001",
|
||||
taskId: "FN-001",
|
||||
timestamp: new Date().toISOString(),
|
||||
prInfo: "invalid",
|
||||
});
|
||||
@@ -265,7 +265,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
it("rejects messages with prInfo missing required fields", () => {
|
||||
const json = JSON.stringify({
|
||||
sourceId: "server-remote",
|
||||
taskId: "KB-001",
|
||||
taskId: "FN-001",
|
||||
timestamp: new Date().toISOString(),
|
||||
prInfo: { number: 1 }, // missing url
|
||||
});
|
||||
@@ -310,7 +310,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
it("rejects messages with non-object issueInfo", () => {
|
||||
const json = JSON.stringify({
|
||||
sourceId: "server-remote",
|
||||
taskId: "KB-001",
|
||||
taskId: "FN-001",
|
||||
timestamp: new Date().toISOString(),
|
||||
issueInfo: 123,
|
||||
});
|
||||
@@ -323,7 +323,7 @@ describe("parseBadgePubSubMessage validation", () => {
|
||||
it("rejects messages with issueInfo missing required fields", () => {
|
||||
const json = JSON.stringify({
|
||||
sourceId: "server-remote",
|
||||
taskId: "KB-001",
|
||||
taskId: "FN-001",
|
||||
timestamp: new Date().toISOString(),
|
||||
issueInfo: { state: "open" }, // missing url and number
|
||||
});
|
||||
|
||||
@@ -152,36 +152,36 @@ describe("path traversal protection", () => {
|
||||
describe("via listFiles (task)", () => {
|
||||
it("rejects path traversal in task context", async () => {
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockGetTask.mockResolvedValue({ id: "KB-123", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-123", worktree: undefined });
|
||||
|
||||
await expect(listFiles(mockStore, "KB-123", "../other-task")).rejects.toThrow(FileServiceError);
|
||||
await expect(listFiles(mockStore, "KB-123", "../other-task")).rejects.toThrow("Path traversal detected");
|
||||
await expect(listFiles(mockStore, "FN-123", "../other-task")).rejects.toThrow(FileServiceError);
|
||||
await expect(listFiles(mockStore, "FN-123", "../other-task")).rejects.toThrow("Path traversal detected");
|
||||
});
|
||||
|
||||
it("rejects absolute paths in task context", async () => {
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockGetTask.mockResolvedValue({ id: "KB-123", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-123", worktree: undefined });
|
||||
|
||||
await expect(listFiles(mockStore, "KB-123", "/etc/passwd")).rejects.toThrow(FileServiceError);
|
||||
await expect(listFiles(mockStore, "FN-123", "/etc/passwd")).rejects.toThrow(FileServiceError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("via readFile (task)", () => {
|
||||
it("rejects path traversal when reading task files", async () => {
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockGetTask.mockResolvedValue({ id: "KB-123", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-123", worktree: undefined });
|
||||
|
||||
await expect(readFile(mockStore, "KB-123", "../../secret.txt")).rejects.toThrow(FileServiceError);
|
||||
await expect(readFile(mockStore, "KB-123", "../../secret.txt")).rejects.toThrow("Path traversal detected");
|
||||
await expect(readFile(mockStore, "FN-123", "../../secret.txt")).rejects.toThrow(FileServiceError);
|
||||
await expect(readFile(mockStore, "FN-123", "../../secret.txt")).rejects.toThrow("Path traversal detected");
|
||||
});
|
||||
});
|
||||
|
||||
describe("via writeFile (task)", () => {
|
||||
it("rejects path traversal when writing task files", async () => {
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockGetTask.mockResolvedValue({ id: "KB-123", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-123", worktree: undefined });
|
||||
|
||||
await expect(writeFile(mockStore, "KB-123", "../../outside.txt", "data")).rejects.toThrow(FileServiceError);
|
||||
await expect(writeFile(mockStore, "FN-123", "../../outside.txt", "data")).rejects.toThrow(FileServiceError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -418,7 +418,7 @@ describe("task file operations", () => {
|
||||
const worktreePath = "/worktrees/kb-123";
|
||||
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-123",
|
||||
id: "FN-123",
|
||||
worktree: worktreePath,
|
||||
});
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
@@ -429,7 +429,7 @@ describe("task file operations", () => {
|
||||
});
|
||||
mockReadFile.mockResolvedValue("Task content");
|
||||
|
||||
const result = await readFile(mockStore, "KB-123", "PROMPT.md");
|
||||
const result = await readFile(mockStore, "FN-123", "PROMPT.md");
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
"/worktrees/kb-123/PROMPT.md",
|
||||
@@ -440,7 +440,7 @@ describe("task file operations", () => {
|
||||
|
||||
it("falls back to task directory when worktree doesn't exist", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-123",
|
||||
id: "FN-123",
|
||||
worktree: "/missing/worktree",
|
||||
});
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
@@ -452,7 +452,7 @@ describe("task file operations", () => {
|
||||
});
|
||||
mockReadFile.mockResolvedValue("Task content");
|
||||
|
||||
await readFile(mockStore, "KB-123", "PROMPT.md");
|
||||
await readFile(mockStore, "FN-123", "PROMPT.md");
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
"/project/.kb/tasks/KB-123/PROMPT.md",
|
||||
@@ -462,7 +462,7 @@ describe("task file operations", () => {
|
||||
|
||||
it("falls back to task directory when worktree is undefined", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-123",
|
||||
id: "FN-123",
|
||||
worktree: undefined,
|
||||
});
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
@@ -473,7 +473,7 @@ describe("task file operations", () => {
|
||||
});
|
||||
mockReadFile.mockResolvedValue("Task content");
|
||||
|
||||
await readFile(mockStore, "KB-123", "PROMPT.md");
|
||||
await readFile(mockStore, "FN-123", "PROMPT.md");
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
"/project/.kb/tasks/KB-123/PROMPT.md",
|
||||
@@ -539,7 +539,7 @@ describe("workspace operations", () => {
|
||||
});
|
||||
|
||||
it("task ID workspace resolves to task path", async () => {
|
||||
mockGetTask.mockResolvedValue({ id: "KB-456", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-456", worktree: undefined });
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockStat.mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
@@ -557,7 +557,7 @@ describe("workspace operations", () => {
|
||||
mtime: new Date(),
|
||||
});
|
||||
|
||||
const result = await listWorkspaceFiles(mockStore, "KB-456");
|
||||
const result = await listWorkspaceFiles(mockStore, "FN-456");
|
||||
|
||||
expect(result.entries).toHaveLength(1);
|
||||
expect(result.entries[0].name).toBe("PROMPT.md");
|
||||
@@ -584,7 +584,7 @@ describe("workspace operations", () => {
|
||||
});
|
||||
|
||||
it("reads file from task workspace", async () => {
|
||||
mockGetTask.mockResolvedValue({ id: "KB-123", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-123", worktree: undefined });
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockStat.mockResolvedValue({
|
||||
isFile: () => true,
|
||||
@@ -593,7 +593,7 @@ describe("workspace operations", () => {
|
||||
});
|
||||
mockReadFile.mockResolvedValue("Task description");
|
||||
|
||||
const result = await readWorkspaceFile(mockStore, "KB-123", "PROMPT.md");
|
||||
const result = await readWorkspaceFile(mockStore, "FN-123", "PROMPT.md");
|
||||
|
||||
expect(result.content).toBe("Task description");
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
@@ -626,7 +626,7 @@ describe("workspace operations", () => {
|
||||
});
|
||||
|
||||
it("writes file to task workspace", async () => {
|
||||
mockGetTask.mockResolvedValue({ id: "KB-123", worktree: undefined });
|
||||
mockGetTask.mockResolvedValue({ id: "FN-123", worktree: undefined });
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
mockStat
|
||||
.mockRejectedValueOnce({ code: "ENOENT" })
|
||||
@@ -637,7 +637,7 @@ describe("workspace operations", () => {
|
||||
mtime: new Date("2024-04-01"),
|
||||
});
|
||||
|
||||
const result = await writeWorkspaceFile(mockStore, "KB-123", "output.txt", "Task output");
|
||||
const result = await writeWorkspaceFile(mockStore, "FN-123", "output.txt", "Task output");
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(
|
||||
|
||||
@@ -145,7 +145,7 @@ describe("GitHubPollingService", () => {
|
||||
});
|
||||
|
||||
it("restarts timer when interval changes while running", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
@@ -166,7 +166,7 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
describe("start/stop", () => {
|
||||
it("begins polling when watches exist", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
service.start();
|
||||
|
||||
@@ -181,7 +181,7 @@ describe("GitHubPollingService", () => {
|
||||
});
|
||||
|
||||
it("clears timer on stop", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
@@ -193,7 +193,7 @@ describe("GitHubPollingService", () => {
|
||||
});
|
||||
|
||||
it("multiple start calls are safe", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
const timer1 = service["timer"];
|
||||
|
||||
@@ -210,12 +210,12 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
describe("watchTask", () => {
|
||||
it("adds watch for task", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch).toBeDefined();
|
||||
expect(watch?.pr).toEqual({
|
||||
taskId: "KB-001",
|
||||
taskId: "FN-001",
|
||||
type: "pr",
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
@@ -224,18 +224,18 @@ describe("GitHubPollingService", () => {
|
||||
});
|
||||
|
||||
it("replaces existing watch of same type", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 2);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 2);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch?.pr?.number).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps other watch type when replacing", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 10);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "issue", "owner", "repo", 10);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch?.pr?.number).toBe(1);
|
||||
expect(watch?.issue?.number).toBe(10);
|
||||
});
|
||||
@@ -243,31 +243,31 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
describe("replaceTaskWatches", () => {
|
||||
it("handles multiple watch types", () => {
|
||||
service.replaceTaskWatches("KB-001", [
|
||||
{ taskId: "KB-001", type: "pr", owner: "owner", repo: "repo", number: 1 },
|
||||
{ taskId: "KB-001", type: "issue", owner: "owner", repo: "repo", number: 10 },
|
||||
service.replaceTaskWatches("FN-001", [
|
||||
{ taskId: "FN-001", type: "pr", owner: "owner", repo: "repo", number: 1 },
|
||||
{ taskId: "FN-001", type: "issue", owner: "owner", repo: "repo", number: 10 },
|
||||
]);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch?.pr?.number).toBe(1);
|
||||
expect(watch?.issue?.number).toBe(10);
|
||||
});
|
||||
|
||||
it("unwatches when empty array", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
service.replaceTaskWatches("KB-001", []);
|
||||
service.replaceTaskWatches("FN-001", []);
|
||||
|
||||
expect(service.getWatch("KB-001")).toBeUndefined();
|
||||
expect(service.getWatch("FN-001")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters invalid watches", () => {
|
||||
service.replaceTaskWatches("KB-001", [
|
||||
{ taskId: "KB-001", type: "pr", owner: "", repo: "repo", number: 1 }, // invalid - empty owner
|
||||
{ taskId: "KB-001", type: "issue", owner: "owner", repo: "repo", number: 10 }, // valid
|
||||
service.replaceTaskWatches("FN-001", [
|
||||
{ taskId: "FN-001", type: "pr", owner: "", repo: "repo", number: 1 }, // invalid - empty owner
|
||||
{ taskId: "FN-001", type: "issue", owner: "owner", repo: "repo", number: 10 }, // valid
|
||||
]);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch?.pr).toBeUndefined();
|
||||
expect(watch?.issue?.number).toBe(10);
|
||||
});
|
||||
@@ -275,21 +275,21 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
describe("unwatchTask", () => {
|
||||
it("removes all watches for task", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 10);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "issue", "owner", "repo", 10);
|
||||
|
||||
service.unwatchTask("KB-001");
|
||||
service.unwatchTask("FN-001");
|
||||
|
||||
expect(service.getWatch("KB-001")).toBeUndefined();
|
||||
expect(service.getWatch("FN-001")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stops polling when no watches remain", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
|
||||
service.unwatchTask("KB-001");
|
||||
service.unwatchTask("FN-001");
|
||||
|
||||
expect(service["timer"]).toBeNull();
|
||||
});
|
||||
@@ -297,29 +297,29 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
describe("unwatchTaskType", () => {
|
||||
it("removes specific type only", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 10);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "issue", "owner", "repo", 10);
|
||||
|
||||
service.unwatchTaskType("KB-001", "pr");
|
||||
service.unwatchTaskType("FN-001", "pr");
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch?.pr).toBeUndefined();
|
||||
expect(watch?.issue?.number).toBe(10);
|
||||
});
|
||||
|
||||
it("unwatches task if no types remain", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
service.unwatchTaskType("KB-001", "pr");
|
||||
service.unwatchTaskType("FN-001", "pr");
|
||||
|
||||
expect(service.getWatch("KB-001")).toBeUndefined();
|
||||
expect(service.getWatch("FN-001")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reset", () => {
|
||||
it("clears all watches and stops", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-002", "issue", "owner", "repo", 10);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-002", "issue", "owner", "repo", 10);
|
||||
service.start();
|
||||
|
||||
service.reset();
|
||||
@@ -331,21 +331,21 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
describe("getWatchedTaskIds", () => {
|
||||
it("returns all watched task IDs", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-002", "issue", "owner", "repo", 10);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-002", "issue", "owner", "repo", 10);
|
||||
|
||||
const ids = service.getWatchedTaskIds();
|
||||
expect(ids).toContain("KB-001");
|
||||
expect(ids).toContain("KB-002");
|
||||
expect(ids).toContain("FN-001");
|
||||
expect(ids).toContain("FN-002");
|
||||
expect(ids).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWatch", () => {
|
||||
it("returns watch set for task", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
const watch = service.getWatch("FN-001");
|
||||
expect(watch?.pr?.owner).toBe("owner");
|
||||
});
|
||||
|
||||
@@ -358,7 +358,7 @@ describe("GitHubPollingService", () => {
|
||||
it("returns timestamp for type", async () => {
|
||||
// Setup task with PR badge
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
@@ -369,25 +369,25 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
const checkedAt = service.getLastCheckedAt("KB-001", "pr");
|
||||
const checkedAt = service.getLastCheckedAt("FN-001", "pr");
|
||||
expect(checkedAt).toBeDefined();
|
||||
expect(new Date(checkedAt!).getTime()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("returns undefined for unwatched type", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
expect(service.getLastCheckedAt("KB-001", "issue")).toBeUndefined();
|
||||
expect(service.getLastCheckedAt("FN-001", "issue")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("pollOnce", () => {
|
||||
it("batches requests by repo", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
@@ -398,7 +398,7 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
// Should batch by repo
|
||||
@@ -420,7 +420,7 @@ describe("GitHubPollingService", () => {
|
||||
});
|
||||
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
@@ -431,7 +431,7 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
// First poll should work
|
||||
await service.pollOnce();
|
||||
@@ -446,17 +446,17 @@ describe("GitHubPollingService", () => {
|
||||
it("handles missing tasks (ENOENT unwatches)", async () => {
|
||||
mockGetTask.mockRejectedValue({ code: "ENOENT" });
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
await service.pollOnce();
|
||||
|
||||
// Task should be unwatched after ENOENT
|
||||
expect(service.getWatch("KB-001")).toBeUndefined();
|
||||
expect(service.getWatch("FN-001")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates store when badge fields changed", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Old Title", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
@@ -467,11 +467,11 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).toHaveBeenCalledWith(
|
||||
"KB-001",
|
||||
"FN-001",
|
||||
expect.objectContaining({ title: "New Title", commentCount: 1 })
|
||||
);
|
||||
});
|
||||
@@ -480,7 +480,7 @@ describe("GitHubPollingService", () => {
|
||||
const prInfo = { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Same Title", headBranch: "feat", baseBranch: "main", commentCount: 0 };
|
||||
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo,
|
||||
});
|
||||
|
||||
@@ -491,7 +491,7 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).not.toHaveBeenCalled();
|
||||
@@ -499,7 +499,7 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
it("handles PR status normalization", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
@@ -511,11 +511,11 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).toHaveBeenCalledWith(
|
||||
"KB-001",
|
||||
"FN-001",
|
||||
expect.objectContaining({ status: "merged" })
|
||||
);
|
||||
});
|
||||
@@ -523,7 +523,7 @@ describe("GitHubPollingService", () => {
|
||||
it("does nothing when store is not configured", async () => {
|
||||
service = new GitHubPollingService({ token: "test-token" });
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
|
||||
await expect(service.pollOnce()).resolves.toBeUndefined();
|
||||
});
|
||||
@@ -542,7 +542,7 @@ describe("GitHubPollingService", () => {
|
||||
describe("badge field comparison", () => {
|
||||
it("detects PR badge changes (url, number, status, title, headBranch, baseBranch, commentCount, lastCommentAt)", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
@@ -585,7 +585,7 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).toHaveBeenCalled();
|
||||
@@ -594,7 +594,7 @@ describe("GitHubPollingService", () => {
|
||||
|
||||
it("detects issue badge changes (url, number, state, title, stateReason)", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
issueInfo: {
|
||||
url: "https://github.com/owner/repo/issues/1",
|
||||
number: 1,
|
||||
@@ -627,7 +627,7 @@ describe("GitHubPollingService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "issue", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdateIssueInfo).toHaveBeenCalled();
|
||||
@@ -638,7 +638,7 @@ describe("GitHubPollingService", () => {
|
||||
describe("unwatch when badge removed", () => {
|
||||
it("unwatches PR when task has no prInfo", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
// No prInfo
|
||||
});
|
||||
|
||||
@@ -646,15 +646,15 @@ describe("GitHubPollingService", () => {
|
||||
pr_1: null,
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(service.getWatch("KB-001")?.pr).toBeUndefined();
|
||||
expect(service.getWatch("FN-001")?.pr).toBeUndefined();
|
||||
});
|
||||
|
||||
it("unwatches issue when task has no issueInfo", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
id: "FN-001",
|
||||
// No issueInfo
|
||||
});
|
||||
|
||||
@@ -662,10 +662,10 @@ describe("GitHubPollingService", () => {
|
||||
issue_1: null,
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 1);
|
||||
service.watchTask("FN-001", "issue", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(service.getWatch("KB-001")?.issue).toBeUndefined();
|
||||
expect(service.getWatch("FN-001")?.issue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ class MockStore extends EventEmitter {
|
||||
|
||||
function createTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "KB-063",
|
||||
id: "FN-063",
|
||||
title: "Realtime badge updates",
|
||||
description: "Test task",
|
||||
column: "in-review",
|
||||
@@ -104,11 +104,11 @@ describe("WebSocketManager", () => {
|
||||
const socket = new MockSocket();
|
||||
|
||||
manager.addClient(socket as unknown as WebSocket, "client-1");
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "KB-063" })));
|
||||
expect(manager.getSubscriptionCount("KB-063")).toBe(1);
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "FN-063" })));
|
||||
expect(manager.getSubscriptionCount("FN-063")).toBe(1);
|
||||
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "unsubscribe", taskId: "KB-063" })));
|
||||
expect(manager.getSubscriptionCount("KB-063")).toBe(0);
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "unsubscribe", taskId: "FN-063" })));
|
||||
expect(manager.getSubscriptionCount("FN-063")).toBe(0);
|
||||
});
|
||||
|
||||
it("broadcasts badge updates only to subscribed clients", () => {
|
||||
@@ -119,10 +119,10 @@ describe("WebSocketManager", () => {
|
||||
manager.addClient(first as unknown as WebSocket, "client-1");
|
||||
manager.addClient(second as unknown as WebSocket, "client-2");
|
||||
|
||||
first.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "KB-063" })));
|
||||
second.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "KB-064" })));
|
||||
first.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "FN-063" })));
|
||||
second.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "FN-064" })));
|
||||
|
||||
manager.broadcastBadgeUpdate("KB-063", {
|
||||
manager.broadcastBadgeUpdate("FN-063", {
|
||||
prInfo: null,
|
||||
issueInfo: {
|
||||
url: "https://github.com/owner/repo/issues/2",
|
||||
@@ -138,7 +138,7 @@ describe("WebSocketManager", () => {
|
||||
expect(second.send).not.toHaveBeenCalled();
|
||||
expect(JSON.parse(first.sent[0])).toMatchObject({
|
||||
type: "badge:updated",
|
||||
taskId: "KB-063",
|
||||
taskId: "FN-063",
|
||||
prInfo: null,
|
||||
issueInfo: { number: 2 },
|
||||
});
|
||||
@@ -165,13 +165,13 @@ describe("WebSocketManager", () => {
|
||||
const socket = new MockSocket();
|
||||
|
||||
manager.addClient(socket as unknown as WebSocket, "client-1");
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "KB-063" })));
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "subscribe", taskId: "FN-063" })));
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
|
||||
expect(socket.terminate).toHaveBeenCalled();
|
||||
expect(manager.getClientCount()).toBe(0);
|
||||
expect(manager.getSubscriptionCount("KB-063")).toBe(0);
|
||||
expect(manager.getSubscriptionCount("FN-063")).toBe(0);
|
||||
});
|
||||
|
||||
it("disposes sockets without leaking tracked clients", () => {
|
||||
@@ -250,7 +250,7 @@ describe("multi-instance /api/ws integration", () => {
|
||||
|
||||
// Create two separate stores (simulating separate instances)
|
||||
const taskA = createTask({
|
||||
id: "KB-MULTI-001",
|
||||
id: "FN-MULTI-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
@@ -298,7 +298,7 @@ describe("multi-instance /api/ws integration", () => {
|
||||
|
||||
// Emit badge-changing task:updated on instance A (no local subscribers)
|
||||
const updatedTaskA = createTask({
|
||||
id: "KB-MULTI-001",
|
||||
id: "FN-MULTI-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
@@ -344,7 +344,7 @@ describe("multi-instance /api/ws integration", () => {
|
||||
const sharedPubSub: BadgePubSub = new InMemoryBadgePubSub();
|
||||
await sharedPubSub.start();
|
||||
|
||||
const task = createTask({ id: "KB-ECHO-001" });
|
||||
const task = createTask({ id: "FN-ECHO-001" });
|
||||
const store = new MockStore(task);
|
||||
|
||||
const app = createServer(store as any, {
|
||||
@@ -369,7 +369,7 @@ describe("multi-instance /api/ws integration", () => {
|
||||
|
||||
// Emit task:updated on the same instance
|
||||
const updatedTask = createTask({
|
||||
id: "KB-ECHO-001",
|
||||
id: "FN-ECHO-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/99",
|
||||
number: 99,
|
||||
@@ -418,7 +418,7 @@ describe("multi-instance /api/ws integration", () => {
|
||||
await sharedPubSub.start();
|
||||
|
||||
const task = createTask({
|
||||
id: "KB-LATE-001",
|
||||
id: "FN-LATE-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
@@ -453,7 +453,7 @@ describe("multi-instance /api/ws integration", () => {
|
||||
|
||||
// First, emit an update on instance A (no subscribers)
|
||||
const updatedTask = createTask({
|
||||
id: "KB-LATE-001",
|
||||
id: "FN-LATE-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
|
||||
Reference in New Issue
Block a user