Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions server/appRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,49 @@ test("listIssues stays cached-only when issue automation is off", async () => {
assert.equal(buildOctokitCalls, 0, "disabled issue automation must not enrich list rows through GitHub");
});

test("listIssues excludes closed worked issues from the default open issue count", async () => {
const storage = new MemStorage();
await storage.updateConfig({ autoIssues: false, watchedRepos: ["owner/repo"] });
const makeIssue = (number: number, title: string) => ({
number,
title,
body: null,
bodyHtml: null,
url: `https://github.com/owner/repo/issues/${number}`,
repoFullName: "owner/repo",
repoCloneUrl: "https://github.com/owner/repo.git",
author: "alice",
labels: ["needs-triage"],
assignees: [],
comments: 0,
createdAt: "2026-05-03T17:00:00.000Z",
updatedAt: "2026-05-03T18:00:00.000Z",
});
await storage.upsertSyncedIssues("owner/repo", [
makeIssue(1, "Open issue"),
makeIssue(2, "Open worked issue"),
makeIssue(3, "Closed worked issue"),
], "2026-05-03T18:00:00.000Z");
await storage.markSyncedIssueWorked("owner/repo", 2);
await storage.markSyncedIssueWorked("owner/repo", 3);
await storage.markRepoIssuesStale("owner/repo");
await storage.upsertSyncedIssues("owner/repo", [
makeIssue(1, "Open issue"),
makeIssue(2, "Open worked issue"),
], "2026-05-03T19:00:00.000Z");

const runtime = createAppRuntime({
storage,
startBackgroundServices: false,
startWatcher: false,
});

const page = await runtime.listIssues({ limit: 50, offset: 0 });

assert.equal(page.totalCount, 2);
assert.deepEqual(page.items.map((issue) => issue.number), [2, 1]);
});

test("getIssue stays cached-only when issue automation is off", async () => {
const storage = new MemStorage();
await storage.updateConfig({ autoIssues: false, watchedRepos: ["owner/repo"] });
Expand Down
4 changes: 2 additions & 2 deletions server/appRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,8 +1528,8 @@ export function createAppRuntime(dependencies: AppRuntimeDependencies = {}): App
};
}

const synced = await storage.listSyncedIssues({ repos: config.watchedRepos, limit, offset, includeWorked: true });
const counts = await storage.listSyncedIssueCounts({ repos: config.watchedRepos, includeWorked: true });
const synced = await storage.listSyncedIssues({ repos: config.watchedRepos, limit, offset });
const counts = await storage.listSyncedIssueCounts({ repos: config.watchedRepos });
const issueAutomationEnabled = config.autoIssues !== false;
const octokit = issueAutomationEnabled ? await buildOctokit(config) : null;
const workJobs = await storage.listBackgroundJobs({ kind: "work_issue" });
Expand Down
3 changes: 1 addition & 2 deletions server/memoryStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export class MemStorage implements IStorage {
const repos = new Set(input.repos.map((repo) => repo.toLowerCase()));
const includeWorked = input.includeWorked ?? false;
const all = Array.from(this.syncedIssues.values())
.filter((record) => (record.isOpen || (includeWorked && record.isWorked)) && repos.has(record.repo.toLowerCase()) && (includeWorked || !record.isWorked))
.filter((record) => (record.isOpen || (includeWorked && record.isWorked)) && repos.has(record.repo.toLowerCase()))
.sort((a, b) => b.number - a.number);
const items = all.slice(input.offset, input.offset + input.limit).map((record) => structuredClone(record));
return { items, hasMore: input.offset + items.length < all.length };
Expand All @@ -381,7 +381,6 @@ export class MemStorage implements IStorage {
for (const record of Array.from(this.syncedIssues.values())) {
if (!record.isOpen && !(includeWorked && record.isWorked)) continue;
if (!repos.has(record.repo.toLowerCase())) continue;
if (!includeWorked && record.isWorked) continue;
repoTotals[record.repo] = (repoTotals[record.repo] ?? 0) + 1;
totalCount += 1;
}
Expand Down
6 changes: 2 additions & 4 deletions server/sqliteStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2267,13 +2267,12 @@ export class SqliteStorage implements IStorage {
`
SELECT repo, issue_number, payload_json, is_open, is_worked, first_seen_at, last_seen_at
FROM synced_issues
WHERE (is_open = 1 OR (? = 1 AND is_worked = 1)) AND repo IN (${placeholders}) AND (? = 1 OR is_worked = 0)
WHERE (is_open = 1 OR (? = 1 AND is_worked = 1)) AND repo IN (${placeholders})
ORDER BY issue_number DESC
LIMIT ? OFFSET ?
`,
includeWorked ? 1 : 0,
...input.repos,
includeWorked ? 1 : 0,
input.limit + 1,
input.offset,
);
Expand All @@ -2292,12 +2291,11 @@ export class SqliteStorage implements IStorage {
`
SELECT repo, COUNT(*) as count
FROM synced_issues
WHERE (is_open = 1 OR (? = 1 AND is_worked = 1)) AND repo IN (${placeholders}) AND (? = 1 OR is_worked = 0)
WHERE (is_open = 1 OR (? = 1 AND is_worked = 1)) AND repo IN (${placeholders})
GROUP BY repo
`,
includeWorked ? 1 : 0,
...input.repos,
includeWorked ? 1 : 0,
);
const repoTotals: Record<string, number> = {};
let totalCount = 0;
Expand Down
Loading