diff --git a/changelog.d/734-unfiltered-first-run.fixed.md b/changelog.d/734-unfiltered-first-run.fixed.md new file mode 100644 index 00000000..03807207 --- /dev/null +++ b/changelog.d/734-unfiltered-first-run.fixed.md @@ -0,0 +1 @@ +The first-run card now reads the account's unfiltered job count. A status, repository, kind or priority filter on the recent-jobs table no longer makes the first-run checklist reappear after the first job or changes the page's next action; the table still shows the filtered count. diff --git a/docs/architecture.md b/docs/architecture.md index 17088899..eff664ef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -96,7 +96,10 @@ defect. the first job. Once the quota is exhausted or the platform model is not configured, the same step becomes required again; the answer is derived from the rows and config on every request, so no skip cookie or second store is - needed. Once a job exists the checklist is replaced by a first-run summary + needed. The checklist reads the account's unfiltered job count, so a + `status`/`repo`/`kind`/`priority` filter on the recent-jobs table cannot bring + the checklist back after the first job. Once a job exists the checklist is + replaced by a first-run summary whose outcome is the first job's own status, read all-time (not a window-scoped count), in four shapes: the job's PR link when it opened one, "finished" with a link to Recent jobs when it diff --git a/src/app.ts b/src/app.ts index 7713bfe1..69ae72b6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1229,6 +1229,9 @@ export async function buildApp(options: BuildOptions): Promise { const firstJob = store.firstJobForUser(viewer.user.id) // #227: the checklist state is read from the database on this request, never // from a cookie, so a second tab or a fresh browser sees the same step. + // #734: the checklist describes the account, not the job table. `jobsTotal` + // carries the current filter for the table; the checklist must not read it. + const setupJobsTotal = store.countJobsForUser(viewer.user.id) const platformAvailable = config.llm.baseUrl !== '' && config.llm.key !== '' && @@ -1241,7 +1244,7 @@ export async function buildApp(options: BuildOptions): Promise { .some((provider) => store.listProviderModels(provider.id).length > 0), platformAvailable, instanceReady: allRepos.some((repo) => store.getRepoInstance(repo.id)?.status === 'ready'), - jobs: jobsTotal, + jobs: setupJobsTotal, firstJobStatus: firstJob?.status ?? null, firstPrUrl: firstJob?.pr_url ?? null, repoHref: allRepos[0] === undefined ? null : `/repos/${allRepos[0].id}`, diff --git a/test/setupChecklist.test.ts b/test/setupChecklist.test.ts index 2f2f76bd..44d82202 100644 --- a/test/setupChecklist.test.ts +++ b/test/setupChecklist.test.ts @@ -246,6 +246,34 @@ test('a job that carries a PR replaces the checklist and the summary links it', } }) +// #734: the table filter is table state. A filter that matches no job must not +// make an account with a finished first run look new again. +test('a job-table filter does not rewrite the first-run state', async () => { + const config = testConfig() + const store = new Store(':memory:') + const built = await buildApp({ config, store, mailer: new MemoryMailer(), runner: new MockRunner(), startScheduler: false }) + try { + const user = makeUser(store, { login: 'checklist-filtered', status: 'approved' }) + const session = sessionFor(built, user) + const cookie = `${session.cookie}; fledge_lang=en` + const repo = store.createRepo({ id: 'repo_filtered', userId: user.id, owner: 'o', name: 'filtered', defaultBranch: 'main', tokenEnc: 'x', permission: 'admin', repoRole: 'developer', goal: '', autoIterate: false, intervalMinutes: 1440 }) + store.createJob({ id: 'job_filtered', repoId: repo.id, userId: user.id, trigger: 'manual', goal: 'g', callbackHash: 'cb', platformTokenHash: '' }) + store.updateJob('job_filtered', { status: 'succeeded' }) + + const clean = await built.app.inject({ method: 'GET', url: '/dashboard', headers: { cookie } }) + assert.match(clean.body, /data-first-pr/, 'the first job gives the account its first-run summary') + + const filtered = await built.app.inject({ method: 'GET', url: '/dashboard?status=queued', headers: { cookie } }) + assert.equal(filtered.statusCode, 200) + assert.match(filtered.body, /data-first-pr/, 'a filter that matches no job does not bring the checklist back') + assert.match(filtered.body, /Your first job finished/, 'the first-run outcome still reads the first job') + assert.doesNotMatch(filtered.body, /data-setup-next=/, 'the checklist is not rendered again') + } finally { + await built.app.close() + store.close() + } +}) + // Finding 1 on #241: the first-run summary must read the *first job*, not a // window-scoped success count. A success that fell out of the 7-day window used // to render "Your first job did not succeed" and the retry path, while