From ffff333f482e7c353300fd3b2c7ddf1ff6810f8b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 07:37:07 -0400 Subject: [PATCH] TASK-conversation-console-workflow: refresh stale previews and focus deep links --- docs/TEST_INTEGRITY.md | 24 ++++- scripts/test-dashboard-demo.mjs | 18 +++- site/console.js | 44 +++++++- test/fleet/console-view-navigation.test.mjs | 111 +++++++++++++++++++- 4 files changed, 187 insertions(+), 10 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index ef5a0fe..5c10d5a 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2570,10 +2570,14 @@ test_integrity_note: given: "The existing dashboard scenarios and eleven screenshot destinations run in their established order" when: "The driver selects each target section's supported fragment immediately before its unchanged checks and captures" then: "Each original assertion and capture executes against the selected view without changing capture order, viewport coverage, artifact destination, or artifact safeguards" - - id: SCN-dashboard-draft-preview-invalidates-on-newer-evidence - given: "A task priority preview is created from task evidence revision N" - when: "A separate confirmed sample action advances that task to authoritative revision N+1, the owner navigates away and back, and refresh loads the newer evidence" - then: "The revision N token, preview, and confirmation control are absent; only a newly generated preview bound to revision N+1 can be presented" + - id: SCN-console-priority-preview-invalidates-on-newer-evidence + given: "A task priority preview and unsent priority and reason values are created from task evidence revision N" + when: "A separate confirmed sample API action advances that task to authoritative revision N+1 and Console refresh loads the newer evidence" + then: "The revision N token, preview, and confirmation control are absent; revision N+1 is visible, the unsent values remain intact and editable, and only a newly generated preview bound to N+1 can be presented" + - id: SCN-console-current-preview-and-draft-survive-unchanged-refresh + given: "A task has an active priority preview and unsent priority and reason values bound to its current evidence revision" + when: "Console refresh returns the same task revision" + then: "The preview token, review panel, confirmation boundary, and unsent field values remain available unchanged" - id: SCN-console-view-navigation given: "The Console has addressable work, fleet, evidence, and flow-watch views" when: "A reader opens fragments, follows navigation, and uses browser Back and Forward" @@ -2582,6 +2586,14 @@ test_integrity_note: given: "The Console is rendered at a 390x844 viewport" when: "Each supported destination is selected" then: "Every navigation destination remains visible and the document and navigation have no horizontal overflow" + - id: SCN-console-view-title-mapping + given: "The Console loads Overview and a reader selects each other supported view" + when: "The reader returns to Overview after visiting contextual views" + then: "Overview has the exact title TORCH Dashboard on initial and return navigation; all other views keep their exact contextual titles" + - id: SCN-console-secondary-deeplink-focus-and-history + given: "The Console is loaded at the organization-proposals or approval-request-list fragment" + when: "The reader opens either direct link and uses browser Back and Forward" + then: "Exactly the matching view is selected and the actual secondary target is focused inside the viewport" - id: SCN-console-view-drafts given: "A task draft and an owner approval preview are present" when: "The reader changes views and refreshes the sample Console" @@ -2589,11 +2601,15 @@ test_integrity_note: observable_outcomes: - "Selected fragment, visible view, original assertion results, and the unchanged ordered screenshot set" - "Revision N+1 displayed after refresh, with the old preview token and confirmation control unavailable" + - "Unsent priority and reason values retained and editable after newer evidence invalidates the old preview" + - "An unchanged current-revision preview, confirmation boundary, and draft retained after refresh" - "A regenerated preview token and displayed revision bound to N+1" + - "Exact Overview and contextual view titles, plus focused in-viewport secondary targets across direct loads and browser history" - "No browser requests to live project APIs and no changes to tracked screenshot expectations" determinism_controls: - "The existing isolated sample Console, fixed Playwright clock, and fixed desktop/mobile viewports" - "Newer evidence is introduced through the sample's supported preview/confirm API before an explicit refresh" + - "The browser test advances only the isolated sample API; it never submits the priority form or mutates a live project" - "No network, live project mutation, retry, sleep, timeout, golden update, or direct fixture-source change" anti_cheat_rationale: prevents: diff --git a/scripts/test-dashboard-demo.mjs b/scripts/test-dashboard-demo.mjs index eb975ff..770753a 100644 --- a/scripts/test-dashboard-demo.mjs +++ b/scripts/test-dashboard-demo.mjs @@ -332,14 +332,28 @@ try { 'revision N preview must not remain presented after revision N+1 is refreshed'); assert.equal(await refreshedForm.locator('[data-priority-confirm]').count(), 0, 'the stale revision N preview cannot be confirmed as current'); + assert.equal(await refreshedForm.locator('[name="priority"]').inputValue(), 'urgent', 'the unsent priority choice must survive invalidation'); + assert.equal(await refreshedForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.', + 'the unsent reason must survive invalidation'); + assert.equal(await refreshedForm.locator('[name="priority"]').isDisabled(), false, 'the preserved priority choice remains editable'); + assert.equal(await refreshedForm.locator('[name="reason"]').isDisabled(), false, 'the preserved reason remains editable'); + assert.match(await refreshedForm.locator('.priority-change-status').innerText(), new RegExp(`revision ${revisionN1}`), + 'the visible status identifies current task evidence'); await refreshedTicket.locator('.priority-change > summary').click(); - await refreshedForm.locator('[name="priority"]').selectOption('urgent'); - await refreshedForm.locator('[name="reason"]').fill('Review again using the refreshed task evidence.'); await refreshedForm.getByRole('button', { name: 'Review change', exact: true }).click(); await refreshedForm.locator('[data-priority-confirm]').waitFor(); assert.equal(await refreshedForm.locator('[data-priority-preview]').locator('dd').nth(2).innerText(), String(revisionN1)); assert.notEqual(await refreshedForm.getAttribute('data-preview-token'), staleToken, 'a current preview must be regenerated from revision N+1'); + const currentToken = await refreshedForm.getAttribute('data-preview-token'); + await stalePage.locator('#refresh-console').click(); + await stalePage.locator('#live-refresh-status[data-generation="3"]').waitFor(); + assert.equal(await refreshedForm.getAttribute('data-preview-token'), currentToken, + 'unchanged current evidence must retain its active preview'); + assert.equal(await refreshedForm.locator('[data-priority-preview]').isVisible(), true); + assert.equal(await refreshedForm.locator('[data-priority-confirm]').isVisible(), true); + assert.equal(await refreshedForm.locator('[name="priority"]').inputValue(), 'urgent'); + assert.equal(await refreshedForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.'); await stalePage.close(); assert.deepEqual(errors, []); assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs'); diff --git a/site/console.js b/site/console.js index fc016cf..db52b95 100644 --- a/site/console.js +++ b/site/console.js @@ -45,15 +45,19 @@ function installConsoleViewRouting() { }); const workspace = document.querySelector('.console-workspace'); if (workspace) workspace.dataset.currentView = view; - document.title = `${definition.title} | TORCH Local Fleet Console`; + document.title = view === 'overview' ? 'TORCH Dashboard' : `${definition.title} | TORCH Local Fleet Console`; const announcement = document.querySelector('#console-view-status'); if (announcement) announcement.textContent = `Viewing ${definition.title}.`; if (focus) { const targetId = decodeHash(hash); const target = document.getElementById(targetId); - if (target && target.dataset.consoleView !== view && target.closest('[data-console-view]')) { - target.scrollIntoView?.({ block: 'start' }); + const targetView = target?.closest('[data-console-view]')?.dataset.consoleView; + if (target && targetId !== view && targetView === view) { + if (!target.hasAttribute('tabindex') && !target.matches('a[href], button, input, select, textarea, summary')) { + target.tabIndex = -1; + } + target.scrollIntoView?.({ block: 'start', behavior: 'instant' }); target.focus?.({ preventScroll: true }); } else { const labelledBy = visible[0]?.getAttribute('aria-labelledby'); @@ -92,6 +96,39 @@ refreshProtection.remember(); let renderingSnapshot = false; let heldPanels = 0; +function invalidateStalePriorityPreviews(tasks = []) { + const currentById = new Map(tasks.map((task) => [task.id, task])); + for (const form of document.querySelectorAll('.priority-change-form[data-preview-token]')) { + const task = currentById.get(form.dataset.taskId); + const currentRevision = Number(task?.revision); + const previewRevision = Number(form.dataset.revision); + if (!task || !Number.isSafeInteger(currentRevision) || currentRevision === previewRevision) continue; + + const ticket = form.closest('.task-ticket'); + if (!ticket) continue; + const priority = form.querySelector('[name="priority"]')?.value; + const reason = form.querySelector('[name="reason"]')?.value; + const openDetails = new Set([...ticket.querySelectorAll('details[open]')] + .map((details) => details.querySelector(':scope > summary')?.textContent)); + const template = document.createElement('template'); + template.innerHTML = taskCard(task, currentBacklogHealth).trim(); + const refreshedTicket = template.content.firstElementChild; + if (!refreshedTicket) continue; + const refreshedForm = refreshedTicket.querySelector('.priority-change-form'); + if (refreshedForm) { + const priorityField = refreshedForm.querySelector('[name="priority"]'); + const reasonField = refreshedForm.querySelector('[name="reason"]'); + if (priorityField && priority) priorityField.value = priority; + if (reasonField) reasonField.value = reason ?? ''; + priorityStatus(refreshedForm, `Task evidence advanced to revision ${currentRevision}. The older preview was cleared; review the current task before previewing again.`); + } + refreshedTicket.querySelectorAll('details').forEach((details) => { + if (openDetails.has(details.querySelector(':scope > summary')?.textContent)) details.open = true; + }); + ticket.replaceWith(refreshedTicket); + } +} + const escapeHtml = (value) => { const element = document.createElement('span'); element.textContent = value ?? ''; @@ -1044,6 +1081,7 @@ const refreshScheduler = globalThis.TorchLiveRefresh.createScheduler({ heldPanels = 0; renderingSnapshot = true; try { render(snapshot); } finally { renderingSnapshot = false; } + invalidateStalePriorityPreviews(snapshot.backlog ?? []); refreshProtection.remember({ preserve: editedBeforeRender }); $('#console-error').hidden = true; const status = $('#live-refresh-status'); diff --git a/test/fleet/console-view-navigation.test.mjs b/test/fleet/console-view-navigation.test.mjs index 9d8221d..caae978 100644 --- a/test/fleet/console-view-navigation.test.mjs +++ b/test/fleet/console-view-navigation.test.mjs @@ -39,12 +39,121 @@ async function assertSelectedView(page, route, title) { const selected = await page.locator('.console-workspace > [data-console-view]:not([hidden])').evaluateAll((sections) => [...new Set(sections.map((section) => section.dataset.consoleView))]); assert.deepEqual(selected, [route], `only the ${route} view should be visible`); - assert.equal(await page.title(), `${title} | TORCH Local Fleet Console`); + assert.equal(await page.title(), route === 'overview' ? 'TORCH Dashboard' : `${title} | TORCH Local Fleet Console`); assert.equal(await page.locator(`.console-rail a[aria-current="page"][href="#${route}"]`).count(), 1); assert.equal(await page.locator('#project-name').isVisible(), true); assert.equal(await page.locator('#snapshot-time').isVisible(), true); } +test('SCN-console-view-title-mapping: Overview retains its dashboard title while other selected views stay contextual', async (t) => { + const { page } = await openDemo(t, { width: 1280, height: 900 }); + const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`; + await page.goto(`${base}#overview`); + await page.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await assertSelectedView(page, 'overview', 'Overview'); + + for (const [route, title] of views.filter(([route]) => route !== 'overview')) { + await page.locator(`.console-rail a[href="#${route}"]`).click(); + await assertSelectedView(page, route, title); + } + await page.locator('.console-rail a[href="#overview"]').click(); + await assertSelectedView(page, 'overview', 'Overview'); +}); + +test('SCN-console-secondary-deeplink-focus-and-history: direct fragments and history land on the actual target', async (t) => { + const { page } = await openDemo(t, { width: 390, height: 844 }); + const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`; + const assertTarget = async (id, route, title) => { + await assertSelectedView(page, route, title); + const state = await page.locator(`#${id}`).evaluate((target) => { + const bounds = target.getBoundingClientRect(); + return { + activeId: document.activeElement?.id, + top: bounds.top, + bottom: bounds.bottom, + height: innerHeight, + }; + }); + assert.equal(state.activeId, id, `the ${id} target must receive focus`); + assert.ok(state.top < state.height && state.bottom > 0, `${id} must intersect the viewport: ${JSON.stringify(state)}`); + }; + + await page.goto(`${base}#organization-proposals`); + await page.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await assertTarget('organization-proposals', 'organization', 'Organization'); + await page.goto(`${base}#approval-request-list`); + await page.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await assertTarget('approval-request-list', 'flow-watch', 'Flow watch'); + await page.goBack(); + await assertTarget('organization-proposals', 'organization', 'Organization'); + await page.goForward(); + await assertTarget('approval-request-list', 'flow-watch', 'Flow watch'); +}); + +test('SCN-console-priority-preview-invalidates-on-newer-evidence: refresh preserves editable drafts and rejects the old preview', async (t) => { + const { page } = await openDemo(t, { width: 1280, height: 900 }); + const form = page.locator('.priority-change-form[data-task-id="API-08"]'); + const ticket = form.locator('xpath=../..'); + await ticket.locator('.priority-change > summary').click(); + await form.locator('[name="priority"]').selectOption('urgent'); + await form.locator('[name="reason"]').fill('Preserve this unsent explanation across a newer task revision.'); + await form.getByRole('button', { name: 'Review change', exact: true }).click(); + await form.locator('[data-priority-confirm]').waitFor(); + const revisionN = Number(await form.getAttribute('data-revision')); + const staleToken = await form.getAttribute('data-preview-token'); + assert.equal(await form.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN)); + + const revisionN1 = await page.evaluate(async (expectedRevision) => { + const path = '/api/backlog/tasks/API-08/priority'; + const change = { priority: 'low', reason: 'Separate normal sample API action advances authoritative evidence.', expectedRevision }; + const preview = await globalThis.TorchConsoleDemo.request(`${path}/preview`, change); + const applied = await globalThis.TorchConsoleDemo.request(path, { ...change, token: preview.token, planHash: preview.planHash }); + return applied.task.revision; + }, revisionN); + assert.equal(revisionN1, revisionN + 1); + + await page.locator('#refresh-console').click(); + await page.locator('#live-refresh-status[data-generation="2"]').waitFor(); + assert.equal(Number(await form.getAttribute('data-revision')), revisionN1); + assert.equal(await ticket.locator('.priority').textContent(), 'low'); + assert.equal(await form.getAttribute('data-preview-token'), null); + assert.equal(await form.locator('[data-priority-preview]').isVisible(), false); + assert.equal(await form.locator('[data-priority-confirm]').count(), 0); + assert.equal(await form.locator('[name="priority"]').inputValue(), 'urgent'); + assert.equal(await form.locator('[name="reason"]').inputValue(), 'Preserve this unsent explanation across a newer task revision.'); + assert.equal(await form.locator('[name="priority"]').isDisabled(), false); + assert.equal(await form.locator('[name="reason"]').isDisabled(), false); + assert.match(await form.locator('.priority-change-status').innerText(), new RegExp(`revision ${revisionN1}`)); + + await form.getByRole('button', { name: 'Review change', exact: true }).click(); + await form.locator('[data-priority-confirm]').waitFor(); + const currentToken = await form.getAttribute('data-preview-token'); + assert.notEqual(currentToken, staleToken); + assert.equal(await form.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN1)); +}); + +test('SCN-console-current-preview-and-draft-survive-unchanged-refresh: a current preview and its fields remain intact', async (t) => { + const { page } = await openDemo(t, { width: 1280, height: 900 }); + const form = page.locator('.priority-change-form[data-task-id="API-08"]'); + const ticket = form.locator('xpath=../..'); + await ticket.locator('.priority-change > summary').click(); + await form.locator('[name="priority"]').selectOption('urgent'); + await form.locator('[name="reason"]').fill('Keep this current unsent explanation after an unchanged refresh.'); + await form.getByRole('button', { name: 'Review change', exact: true }).click(); + await form.locator('[data-priority-confirm]').waitFor(); + const revision = await form.getAttribute('data-revision'); + const token = await form.getAttribute('data-preview-token'); + + await page.locator('#refresh-console').click(); + await page.locator('#live-refresh-status[data-generation="2"]').waitFor(); + assert.equal(await form.getAttribute('data-revision'), revision); + assert.equal(await form.getAttribute('data-preview-token'), token); + assert.equal(await form.locator('[data-priority-preview]').isVisible(), true); + assert.equal(await form.locator('[data-priority-confirm]').isVisible(), true); + assert.equal(await form.locator('[name="priority"]').inputValue(), 'urgent'); + assert.equal(await form.locator('[name="reason"]').inputValue(), 'Keep this current unsent explanation after an unchanged refresh.'); +}); + test('SCN-console-view-navigation: fragments, direct loads, Back and Forward select one addressable view', async (t) => { const { page } = await openDemo(t, { width: 1280, height: 900 }); await assertSelectedView(page, 'work', 'Work');