From a6bf18f3114f93b6396afd9b94b4ff4f27ed7624 Mon Sep 17 00:00:00 2001 From: mindfn Date: Wed, 8 Apr 2026 02:56:02 +0800 Subject: [PATCH 1/4] fix(#320): remove !s.lastRun gate on SchedulePanel kind-match filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The zero-run path in schedule.ts only showed PR scheduler tasks (cicd-check, conflict-check, review-feedback) when the task had NEVER run. Since builtin tasks have prior runs for other PRs, newly registered PR tracking never appeared in the SchedulePanel. Fix: drop the !s.lastRun condition so threads with active pr_tracking tasks see PR-related scheduler tasks via subjectKind match. Also: taskStore.ts imports TaskItem from @cat-cafe/shared instead of maintaining a stale local copy (missing kind/subjectKey/automationState). Split from PR #379 per upstream review — TaskPanel changes dropped to align with cat-cafe #958 direction (filter pr_tracking out of TaskPanel). [宪宪/Opus-46🐾] Co-Authored-By: Claude Opus 4.6 --- packages/api/src/routes/schedule.ts | 9 +++++---- packages/api/test/schedule-route.test.js | 11 ++++++++++- packages/web/src/stores/taskStore.ts | 14 ++------------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/api/src/routes/schedule.ts b/packages/api/src/routes/schedule.ts index 76655450a..9e74a1549 100644 --- a/packages/api/src/routes/schedule.ts +++ b/packages/api/src/routes/schedule.ts @@ -84,7 +84,7 @@ export const scheduleRoutes: FastifyPluginAsync = async ( threadSubjectKeys.add(`thread:${threadId}`); // P1-2 fix: don't rely solely on lastRun — query ledger for ANY matching run. - // Also include tasks whose subjectKind matches even with zero runs (newly registered). + // Also include tasks whose subjectKind matches active thread task kinds. const ledger = taskRunner.getLedger(); const filtered = summaries.filter((s) => { // Quick path: if lastRun matches, include immediately @@ -94,9 +94,10 @@ export const scheduleRoutes: FastifyPluginAsync = async ( const runs = ledger.queryBySubject(s.id, sk, 1); if (runs.length > 0) return true; } - // Zero-run path only: newly registered tasks should show up immediately, - // but once a task has runs we must not leak it across threads by subject kind. - if (!s.lastRun && s.display?.subjectKind && activeThreadSubjectKinds.has(s.display.subjectKind)) return true; + // Kind-match path (#320 P1): thread has active task of matching kind → include. + // Previously gated by !s.lastRun, which blocked builtin tasks (cicd-check etc.) + // that had already run for OTHER PRs from appearing for a newly tracked PR. + if (s.display?.subjectKind && activeThreadSubjectKinds.has(s.display.subjectKind)) return true; return false; }); diff --git a/packages/api/test/schedule-route.test.js b/packages/api/test/schedule-route.test.js index c0da78057..0886e3108 100644 --- a/packages/api/test/schedule-route.test.js +++ b/packages/api/test/schedule-route.test.js @@ -149,7 +149,7 @@ describe('Schedule Routes', () => { assert.ok(!body.tasks.some((task) => task.id === 'review-feedback')); }); - it('does not leak cross-thread PR summaries via subjectKind fallback once a task has runs', async () => { + it('includes PR scheduler tasks for threads with active PR tracking even if task has prior runs (#320 P1)', async () => { taskStore.upsertBySubject({ kind: 'pr_tracking', subjectKey: 'pr:another/repo#7', @@ -162,6 +162,15 @@ describe('Schedule Routes', () => { const res = await app.inject({ method: 'GET', url: '/api/schedule/tasks?threadId=abc123' }); assert.equal(res.statusCode, 200); const body = JSON.parse(res.payload); + // cicd-check has subjectKind='pr' and thread has active pr_tracking → must appear + assert.ok(body.tasks.some((task) => task.id === 'cicd-check')); + }); + + it('excludes PR scheduler tasks for threads without any PR tracking', async () => { + // thread xyz999 has no tasks at all + const res = await app.inject({ method: 'GET', url: '/api/schedule/tasks?threadId=xyz999' }); + assert.equal(res.statusCode, 200); + const body = JSON.parse(res.payload); assert.ok(!body.tasks.some((task) => task.id === 'cicd-check')); }); diff --git a/packages/web/src/stores/taskStore.ts b/packages/web/src/stores/taskStore.ts index 6c86563d9..b0e64278d 100644 --- a/packages/web/src/stores/taskStore.ts +++ b/packages/web/src/stores/taskStore.ts @@ -1,17 +1,7 @@ +import type { TaskItem } from '@cat-cafe/shared'; import { create } from 'zustand'; -export interface TaskItem { - id: string; - kind: 'work' | 'pr_tracking'; - threadId: string; - title: string; - ownerCatId: string | null; - status: 'todo' | 'doing' | 'blocked' | 'done'; - why: string; - createdBy: string; - createdAt: number; - updatedAt: number; -} +export type { TaskItem } from '@cat-cafe/shared'; interface TaskState { tasks: TaskItem[]; From e87040ec1eba35ffa2b9e0c317de57de1a106d50 Mon Sep 17 00:00:00 2001 From: mindfn Date: Wed, 8 Apr 2026 03:03:30 +0800 Subject: [PATCH 2/4] fix(#320): scrub foreign lastRun/subjectPreview on kind-match inclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a task is included via subjectKind match (not actual run match), its lastRun and subjectPreview belong to a different thread/PR. Returning them leaks cross-thread metadata. Fix: kind-match path returns a scrubbed copy with lastRun and subjectPreview set to null. Quick/slow paths (actual matches) are unchanged. [宪宪/Opus-46🐾] Co-Authored-By: Claude Opus 4.6 --- packages/api/src/routes/schedule.ts | 18 ++++++++++-------- packages/api/test/schedule-route.test.js | 6 +++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/api/src/routes/schedule.ts b/packages/api/src/routes/schedule.ts index 9e74a1549..248dd74c6 100644 --- a/packages/api/src/routes/schedule.ts +++ b/packages/api/src/routes/schedule.ts @@ -86,19 +86,21 @@ export const scheduleRoutes: FastifyPluginAsync = async ( // P1-2 fix: don't rely solely on lastRun — query ledger for ANY matching run. // Also include tasks whose subjectKind matches active thread task kinds. const ledger = taskRunner.getLedger(); - const filtered = summaries.filter((s) => { + const filtered = summaries.flatMap((s) => { // Quick path: if lastRun matches, include immediately - if (s.lastRun && threadSubjectKeys.has(s.lastRun.subject_key)) return true; + if (s.lastRun && threadSubjectKeys.has(s.lastRun.subject_key)) return [s]; // Slow path: check if ANY run for this task matches thread's subject keys for (const sk of threadSubjectKeys) { const runs = ledger.queryBySubject(s.id, sk, 1); - if (runs.length > 0) return true; + if (runs.length > 0) return [s]; } - // Kind-match path (#320 P1): thread has active task of matching kind → include. - // Previously gated by !s.lastRun, which blocked builtin tasks (cicd-check etc.) - // that had already run for OTHER PRs from appearing for a newly tracked PR. - if (s.display?.subjectKind && activeThreadSubjectKinds.has(s.display.subjectKind)) return true; - return false; + // Kind-match path (#320 P1): thread has active task of matching kind → include, + // but scrub run metadata that belongs to other threads/PRs. + if (s.display?.subjectKind && activeThreadSubjectKinds.has(s.display.subjectKind)) { + const { lastRun: _, subjectPreview: __, ...rest } = s; + return [{ ...rest, lastRun: null, subjectPreview: null }]; + } + return []; }); return { tasks: filtered }; diff --git a/packages/api/test/schedule-route.test.js b/packages/api/test/schedule-route.test.js index 0886e3108..3fbdb7c0f 100644 --- a/packages/api/test/schedule-route.test.js +++ b/packages/api/test/schedule-route.test.js @@ -163,7 +163,11 @@ describe('Schedule Routes', () => { assert.equal(res.statusCode, 200); const body = JSON.parse(res.payload); // cicd-check has subjectKind='pr' and thread has active pr_tracking → must appear - assert.ok(body.tasks.some((task) => task.id === 'cicd-check')); + const cicd = body.tasks.find((task) => task.id === 'cicd-check'); + assert.ok(cicd, 'cicd-check should appear for thread with active PR tracking'); + // Kind-match included tasks must have foreign run metadata scrubbed + assert.equal(cicd.lastRun, null, 'lastRun from other PR must be scrubbed'); + assert.equal(cicd.subjectPreview, null, 'subjectPreview from other PR must be scrubbed'); }); it('excludes PR scheduler tasks for threads without any PR tracking', async () => { From 2f7c4dc6aec0555a0ede6501d6d4dac109f82e06 Mon Sep 17 00:00:00 2001 From: mindfn Date: Thu, 9 Apr 2026 10:13:23 +0800 Subject: [PATCH 3/4] fix(#320): also scrub runStats on kind-match inclusion (P2) Kind-match path already zeroed lastRun/subjectPreview but left runStats from unrelated subjects intact, leaking cross-thread activity totals (e.g. nonzero delivered count for a brand-new PR tracking thread). Now resets runStats to all zeros. Co-Authored-By: Claude Opus 4.6 --- packages/api/src/routes/schedule.ts | 6 ++++-- packages/api/test/schedule-route.test.js | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/api/src/routes/schedule.ts b/packages/api/src/routes/schedule.ts index 248dd74c6..07dd5c3c8 100644 --- a/packages/api/src/routes/schedule.ts +++ b/packages/api/src/routes/schedule.ts @@ -97,8 +97,10 @@ export const scheduleRoutes: FastifyPluginAsync = async ( // Kind-match path (#320 P1): thread has active task of matching kind → include, // but scrub run metadata that belongs to other threads/PRs. if (s.display?.subjectKind && activeThreadSubjectKinds.has(s.display.subjectKind)) { - const { lastRun: _, subjectPreview: __, ...rest } = s; - return [{ ...rest, lastRun: null, subjectPreview: null }]; + const { lastRun: _, subjectPreview: __, runStats: ___, ...rest } = s; + return [ + { ...rest, lastRun: null, subjectPreview: null, runStats: { total: 0, delivered: 0, failed: 0, skipped: 0 } }, + ]; } return []; }); diff --git a/packages/api/test/schedule-route.test.js b/packages/api/test/schedule-route.test.js index 3fbdb7c0f..c534d2f97 100644 --- a/packages/api/test/schedule-route.test.js +++ b/packages/api/test/schedule-route.test.js @@ -168,6 +168,11 @@ describe('Schedule Routes', () => { // Kind-match included tasks must have foreign run metadata scrubbed assert.equal(cicd.lastRun, null, 'lastRun from other PR must be scrubbed'); assert.equal(cicd.subjectPreview, null, 'subjectPreview from other PR must be scrubbed'); + assert.deepEqual( + cicd.runStats, + { total: 0, delivered: 0, failed: 0, skipped: 0 }, + 'runStats from other PRs must be zeroed', + ); }); it('excludes PR scheduler tasks for threads without any PR tracking', async () => { From 577098db82d738a055f4908542b1ee7e532da497 Mon Sep 17 00:00:00 2001 From: mindfn Date: Fri, 10 Apr 2026 15:21:06 +0800 Subject: [PATCH 4/4] fix: biome formatting in project-setup-card-ime test Co-Authored-By: Claude Opus 4.6 --- .../components/__tests__/project-setup-card-ime.test.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/web/src/components/__tests__/project-setup-card-ime.test.tsx b/packages/web/src/components/__tests__/project-setup-card-ime.test.tsx index 6a8bd8ac2..f27f03332 100644 --- a/packages/web/src/components/__tests__/project-setup-card-ime.test.tsx +++ b/packages/web/src/components/__tests__/project-setup-card-ime.test.tsx @@ -40,13 +40,7 @@ describe('ProjectSetupCard IME guard', () => { await act(async () => { root.render( - , + , ); });