test(KB-316): add comprehensive test coverage for dashboard services
- Add file service tests (680 lines) covering file operations and error handling - Extend GitHub polling service tests with GitHubPollingService coverage - Extend rate-limit tests with additional boundary and edge cases - Total of 1405 new test lines across 3 test files
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GitHubRateLimiter } from "../github-poll.js";
|
||||
import { GitHubRateLimiter, GitHubPollingService, githubPoller, githubRateLimiter } from "../github-poll.js";
|
||||
import type { TaskStore } from "@kb/core";
|
||||
|
||||
// Mock the GitHubClient
|
||||
const mockGetBadgeStatusesBatch = vi.fn();
|
||||
|
||||
vi.mock("../github.js", () => ({
|
||||
GitHubClient: vi.fn().mockImplementation(() => ({
|
||||
getBadgeStatusesBatch: (...args: any[]) => mockGetBadgeStatusesBatch(...args),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("GitHubRateLimiter", () => {
|
||||
beforeEach(() => {
|
||||
@@ -84,3 +94,596 @@ describe("GitHubRateLimiter", () => {
|
||||
expect(limiter.canMakeRequest("owner/repo")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubPollingService", () => {
|
||||
let service: GitHubPollingService;
|
||||
let mockStore: TaskStore;
|
||||
let mockUpdatePrInfo: vi.Mock;
|
||||
let mockUpdateIssueInfo: vi.Mock;
|
||||
let mockGetTask: vi.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockUpdatePrInfo = vi.fn();
|
||||
mockUpdateIssueInfo = vi.fn();
|
||||
mockGetTask = vi.fn();
|
||||
|
||||
mockStore = {
|
||||
getTask: mockGetTask,
|
||||
updatePrInfo: mockUpdatePrInfo,
|
||||
updateIssueInfo: mockUpdateIssueInfo,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
service = new GitHubPollingService({
|
||||
store: mockStore,
|
||||
token: "test-token",
|
||||
pollingIntervalMs: 60_000,
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service.stop();
|
||||
service.reset();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("configure", () => {
|
||||
it("updates store, token, and polling interval", () => {
|
||||
const newStore = { getTask: vi.fn() } as unknown as TaskStore;
|
||||
|
||||
service.configure({
|
||||
store: newStore,
|
||||
token: "new-token",
|
||||
pollingIntervalMs: 30_000,
|
||||
});
|
||||
|
||||
// Verify by checking the service behavior uses new config
|
||||
expect(service.getWatchedTaskIds()).toEqual([]);
|
||||
});
|
||||
|
||||
it("restarts timer when interval changes while running", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
const originalTimer = service["timer"];
|
||||
|
||||
service.configure({ pollingIntervalMs: 30_000 });
|
||||
|
||||
// Timer should have been restarted with new interval
|
||||
expect(service["timer"]).not.toBe(originalTimer);
|
||||
});
|
||||
|
||||
it("does not restart timer if not running", () => {
|
||||
service.configure({ pollingIntervalMs: 30_000 });
|
||||
|
||||
expect(service["timer"]).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("start/stop", () => {
|
||||
it("begins polling when watches exist", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
|
||||
service.start();
|
||||
|
||||
expect(service["enabled"]).toBe(true);
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does nothing when no watches", () => {
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).toBeNull();
|
||||
});
|
||||
|
||||
it("clears timer on stop", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
|
||||
service.stop();
|
||||
|
||||
expect(service["timer"]).toBeNull();
|
||||
expect(service["enabled"]).toBe(false);
|
||||
});
|
||||
|
||||
it("multiple start calls are safe", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
const timer1 = service["timer"];
|
||||
|
||||
service.start();
|
||||
const timer2 = service["timer"];
|
||||
|
||||
expect(timer1).toBe(timer2);
|
||||
});
|
||||
|
||||
it("stop is safe when not running", () => {
|
||||
expect(() => service.stop()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("watchTask", () => {
|
||||
it("adds watch for task", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
expect(watch).toBeDefined();
|
||||
expect(watch?.pr).toEqual({
|
||||
taskId: "KB-001",
|
||||
type: "pr",
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
number: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces existing watch of same type", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 2);
|
||||
|
||||
const watch = service.getWatch("KB-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);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
expect(watch?.pr?.number).toBe(1);
|
||||
expect(watch?.issue?.number).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
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 },
|
||||
]);
|
||||
|
||||
const watch = service.getWatch("KB-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.replaceTaskWatches("KB-001", []);
|
||||
|
||||
expect(service.getWatch("KB-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
|
||||
]);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
expect(watch?.pr).toBeUndefined();
|
||||
expect(watch?.issue?.number).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
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.unwatchTask("KB-001");
|
||||
|
||||
expect(service.getWatch("KB-001")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stops polling when no watches remain", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.start();
|
||||
|
||||
expect(service["timer"]).not.toBeNull();
|
||||
|
||||
service.unwatchTask("KB-001");
|
||||
|
||||
expect(service["timer"]).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("unwatchTaskType", () => {
|
||||
it("removes specific type only", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 10);
|
||||
|
||||
service.unwatchTaskType("KB-001", "pr");
|
||||
|
||||
const watch = service.getWatch("KB-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.unwatchTaskType("KB-001", "pr");
|
||||
|
||||
expect(service.getWatch("KB-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.start();
|
||||
|
||||
service.reset();
|
||||
|
||||
expect(service.getWatchedTaskIds()).toEqual([]);
|
||||
expect(service["timer"]).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWatchedTaskIds", () => {
|
||||
it("returns all watched task IDs", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
service.watchTask("KB-002", "issue", "owner", "repo", 10);
|
||||
|
||||
const ids = service.getWatchedTaskIds();
|
||||
expect(ids).toContain("KB-001");
|
||||
expect(ids).toContain("KB-002");
|
||||
expect(ids).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWatch", () => {
|
||||
it("returns watch set for task", () => {
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
|
||||
const watch = service.getWatch("KB-001");
|
||||
expect(watch?.pr?.owner).toBe("owner");
|
||||
});
|
||||
|
||||
it("returns undefined for unwatched task", () => {
|
||||
expect(service.getWatch("KB-999")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLastCheckedAt", () => {
|
||||
it("returns timestamp for type", async () => {
|
||||
// Setup task with PR badge
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
const checkedAt = service.getLastCheckedAt("KB-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);
|
||||
|
||||
expect(service.getLastCheckedAt("KB-001", "issue")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("pollOnce", () => {
|
||||
it("batches requests by repo", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
// Should batch by repo
|
||||
expect(mockGetBadgeStatusesBatch).toHaveBeenCalledWith(
|
||||
"owner",
|
||||
"repo",
|
||||
expect.arrayContaining([expect.objectContaining({ alias: "pr_1", type: "pr", number: 1 })])
|
||||
);
|
||||
});
|
||||
|
||||
it("applies rate limiting per repo", async () => {
|
||||
// Create a custom rate limiter for testing
|
||||
const rateLimiter = new GitHubRateLimiter({ maxRequests: 1, windowMs: 60000 });
|
||||
|
||||
service = new GitHubPollingService({
|
||||
store: mockStore,
|
||||
token: "test-token",
|
||||
rateLimiter,
|
||||
});
|
||||
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
|
||||
// First poll should work
|
||||
await service.pollOnce();
|
||||
expect(mockGetBadgeStatusesBatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second poll should be rate limited (same repo)
|
||||
await service.pollOnce();
|
||||
// Should not make another request due to rate limiting
|
||||
expect(mockGetBadgeStatusesBatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles missing tasks (ENOENT unwatches)", async () => {
|
||||
mockGetTask.mockRejectedValue({ code: "ENOENT" });
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
|
||||
await service.pollOnce();
|
||||
|
||||
// Task should be unwatched after ENOENT
|
||||
expect(service.getWatch("KB-001")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates store when badge fields changed", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Old Title", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "New Title", headBranch: "feat", baseBranch: "main", commentCount: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).toHaveBeenCalledWith(
|
||||
"KB-001",
|
||||
expect.objectContaining({ title: "New Title", commentCount: 1 })
|
||||
);
|
||||
});
|
||||
|
||||
it("skips update when badge unchanged", async () => {
|
||||
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",
|
||||
prInfo,
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo,
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles PR status normalization", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "open", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
});
|
||||
|
||||
// PR status can be "open", "closed", or "merged"
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo: { url: "https://github.com/owner/repo/pull/1", number: 1, status: "merged", title: "Test", headBranch: "feat", baseBranch: "main", commentCount: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).toHaveBeenCalledWith(
|
||||
"KB-001",
|
||||
expect.objectContaining({ status: "merged" })
|
||||
);
|
||||
});
|
||||
|
||||
it("does nothing when store is not configured", async () => {
|
||||
service = new GitHubPollingService({ token: "test-token" });
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
|
||||
await expect(service.pollOnce()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("does nothing when already polling", async () => {
|
||||
service["isPolling"] = true;
|
||||
|
||||
await expect(service.pollOnce()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("does nothing when no watches", async () => {
|
||||
await expect(service.pollOnce()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("badge field comparison", () => {
|
||||
it("detects PR badge changes (url, number, status, title, headBranch, baseBranch, commentCount, lastCommentAt)", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
status: "open",
|
||||
title: "Test",
|
||||
headBranch: "feat",
|
||||
baseBranch: "main",
|
||||
commentCount: 0,
|
||||
lastCommentAt: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Each field change should trigger update
|
||||
const fieldChanges = [
|
||||
{ field: "url", newValue: "https://github.com/owner/repo/pull/2" },
|
||||
{ field: "number", newValue: 2 },
|
||||
{ field: "status", newValue: "closed" },
|
||||
{ field: "title", newValue: "New Title" },
|
||||
{ field: "headBranch", newValue: "feature" },
|
||||
{ field: "baseBranch", newValue: "develop" },
|
||||
{ field: "commentCount", newValue: 1 },
|
||||
{ field: "lastCommentAt", newValue: "2024-01-01T00:00:00Z" },
|
||||
];
|
||||
|
||||
for (const change of fieldChanges) {
|
||||
mockUpdatePrInfo.mockClear();
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: {
|
||||
type: "pr",
|
||||
prInfo: {
|
||||
url: "https://github.com/owner/repo/pull/1",
|
||||
number: 1,
|
||||
status: "open",
|
||||
title: "Test",
|
||||
headBranch: "feat",
|
||||
baseBranch: "main",
|
||||
commentCount: 0,
|
||||
[change.field]: change.newValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdatePrInfo).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("detects issue badge changes (url, number, state, title, stateReason)", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
issueInfo: {
|
||||
url: "https://github.com/owner/repo/issues/1",
|
||||
number: 1,
|
||||
state: "open",
|
||||
title: "Test Issue",
|
||||
stateReason: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const fieldChanges = [
|
||||
{ field: "url", newValue: "https://github.com/owner/repo/issues/2" },
|
||||
{ field: "number", newValue: 2 },
|
||||
{ field: "state", newValue: "closed" },
|
||||
{ field: "title", newValue: "New Issue Title" },
|
||||
{ field: "stateReason", newValue: "completed" },
|
||||
];
|
||||
|
||||
for (const change of fieldChanges) {
|
||||
mockUpdateIssueInfo.mockClear();
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
issue_1: {
|
||||
type: "issue",
|
||||
issueInfo: {
|
||||
url: "https://github.com/owner/repo/issues/1",
|
||||
number: 1,
|
||||
state: "open",
|
||||
title: "Test Issue",
|
||||
[change.field]: change.newValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(mockUpdateIssueInfo).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("unwatch when badge removed", () => {
|
||||
it("unwatches PR when task has no prInfo", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
// No prInfo
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
pr_1: null,
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "pr", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(service.getWatch("KB-001")?.pr).toBeUndefined();
|
||||
});
|
||||
|
||||
it("unwatches issue when task has no issueInfo", async () => {
|
||||
mockGetTask.mockResolvedValue({
|
||||
id: "KB-001",
|
||||
// No issueInfo
|
||||
});
|
||||
|
||||
mockGetBadgeStatusesBatch.mockResolvedValue({
|
||||
issue_1: null,
|
||||
});
|
||||
|
||||
service.watchTask("KB-001", "issue", "owner", "repo", 1);
|
||||
await service.pollOnce();
|
||||
|
||||
expect(service.getWatch("KB-001")?.issue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("githubPoller singleton", () => {
|
||||
it("is a singleton instance", () => {
|
||||
expect(githubPoller).toBeInstanceOf(GitHubPollingService);
|
||||
});
|
||||
|
||||
it("can be started and stopped", () => {
|
||||
// Should not throw
|
||||
githubPoller.start();
|
||||
githubPoller.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("githubRateLimiter singleton", () => {
|
||||
it("is a singleton instance", () => {
|
||||
expect(githubRateLimiter).toBeInstanceOf(GitHubRateLimiter);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user