fix: finish identity recovery before workspace readiness
All checks were successful
CI / lint (pull_request) Successful in 3m45s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 7m28s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-27 14:34:13 +00:00
parent 5ccf499b1a
commit 02b9fdd2e4
6 changed files with 111 additions and 8 deletions

View File

@ -126,7 +126,7 @@ function createContextPoller({
let request; let request;
try { try {
request = fetchContext({ ...revisions }, { signal: controller.signal }); request = fetchContext(options.full ? {} : { ...revisions }, { signal: controller.signal });
} catch (error) { } catch (error) {
request = Promise.reject(error); request = Promise.reject(error);
} }

View File

@ -358,6 +358,7 @@
qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click()); qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click());
qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click()); qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click());
let liveMode = true; let liveMode = true;
let initialAccountRecovery = Promise.resolve(false);
const WORK_FILTER_KEY = 'stackchain.my-work-filter.v1'; const WORK_FILTER_KEY = 'stackchain.my-work-filter.v1';
const WORK_MILESTONE_KEY = 'stackchain.my-work-milestone.v1'; const WORK_MILESTONE_KEY = 'stackchain.my-work-milestone.v1';
const WORK_FILTERS = ['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft']; const WORK_FILTERS = ['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft'];
@ -5644,6 +5645,9 @@
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : ''; activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
if (activeFlushLogin) { if (activeFlushLogin) {
confirmedOwnerLogin = activeFlushLogin; confirmedOwnerLogin = activeFlushLogin;
planningOwnerLogin = activeFlushLogin;
planningOwnerAccountKey = snapshot.context.user?.id ?
String(snapshot.context.user.id) + ':' + activeFlushLogin : '';
mobileQueuePriority.render(); mobileQueuePriority.render();
renderMobileQueuePresentation(); renderMobileQueuePresentation();
void mobileQueuePriority.load(); void mobileQueuePriority.load();
@ -5666,7 +5670,10 @@
if (planningOwnerLogin) { if (planningOwnerLogin) {
syncPendingTomorrow(); syncPendingTomorrow();
todaySync.migrate(todayWork.read()); todaySync.migrate(todayWork.read());
todaySync.flush(); initialAccountRecovery = Promise.all([
initialAccountRecovery,
todaySync.flush(),
]).then(() => true);
laterSync.migrate(laterWork.read()); laterSync.migrate(laterWork.read());
laterSync.flush(); laterSync.flush();
} }
@ -8597,6 +8604,20 @@
adoptedProgressiveSnapshot = await contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise); adoptedProgressiveSnapshot = await contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise);
} }
if (!adoptedProgressiveSnapshot) await load(); if (!adoptedProgressiveSnapshot) await load();
else if (!confirmedOwnerLogin) {
await contextPoller.refresh({ force:true, full:true });
if (!confirmedOwnerLogin) {
try {
const identity = await fetchReviewJson('api/v1/background-identity');
const login = String(identity?.login || '').trim();
if (login) {
confirmedOwnerLogin = login;
planningOwnerLogin = login;
initialAccountRecovery = timerView.restore(todaySync.flush());
}
} catch (_error) {}
}
}
if (progressiveWorkHandoff?.openWork) { if (progressiveWorkHandoff?.openWork) {
const progressiveItem = lastMyWork.find(item => const progressiveItem = lastMyWork.find(item =>
item.repository === progressiveWorkHandoff.openWork.repository && item.repository === progressiveWorkHandoff.openWork.repository &&
@ -8615,6 +8636,8 @@
function widgetTick() { const el=qs('#widget-clock'); if(el) el.textContent = fmt(new Date()); } function widgetTick() { const el=qs('#widget-clock'); if(el) el.textContent = fmt(new Date()); }
setInterval(widgetTick, 1000); setInterval(widgetTick, 1000);
await initialAccountRecovery;
if (planningOwnerLogin) await todaySync.flush();
await timerView.restore(Promise.resolve(true)); await timerView.restore(Promise.resolve(true));
workspaceLifecycle.markWorkspaceReady?.(); workspaceLifecycle.markWorkspaceReady?.();
})(); })();

View File

@ -489,7 +489,10 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
const snapshot = timer.snapshot(); const snapshot = timer.snapshot();
breakView?.render(); breakView?.render();
const active = Boolean(progress && isActive() && snapshot.identity); const active = Boolean(progress && isActive() && snapshot.identity);
queryAll('[data-mobile-today-hud]').forEach(element => { element.hidden = !active; }); queryAll('[data-mobile-today-hud]').forEach(element => {
const coaching = element.querySelector?.('[data-mobile-first-task-coach]:not([hidden])');
element.hidden = !active && !coaching;
});
queryAll('[data-mobile-today-open]').forEach(element => { queryAll('[data-mobile-today-open]').forEach(element => {
element.textContent = active ? String(getItem?.(snapshot.identity)?.title || 'Current Today item') : ''; element.textContent = active ? String(getItem?.(snapshot.identity)?.title || 'Current Today item') : '';
}); });

View File

@ -60,6 +60,7 @@ def test_release_artifact_guides_an_empty_mobile_account_to_first_work(
expect(sheet).to_be_hidden() expect(sheet).to_be_hidden()
expect(page.locator("#find-work-sheet")).to_be_visible() expect(page.locator("#find-work-sheet")).to_be_visible()
page.locator("#close-find-work").click() page.locator("#close-find-work").click()
expect(page.locator("#find-work-sheet")).to_be_hidden()
context.set_offline(True) context.set_offline(True)
page.locator('[data-mobile-task="work"]').click() page.locator('[data-mobile-task="work"]').click()
@ -70,8 +71,9 @@ def test_release_artifact_guides_an_empty_mobile_account_to_first_work(
expect(page.locator("#mobile-first-task-create")).to_be_focused() expect(page.locator("#mobile-first-task-create")).to_be_focused()
page.locator("#close-mobile-first-task").click() page.locator("#close-mobile-first-task").click()
expect(sheet).to_be_hidden()
context.set_offline(False) context.set_offline(False)
page.evaluate( probe_result = page.evaluate(
"""() => { """() => {
document.querySelector('[data-mobile-today-hud]').hidden = false; document.querySelector('[data-mobile-today-hud]').hidden = false;
localStorage.setItem('stackchain.first-task.v1:timmy', 'coaching'); localStorage.setItem('stackchain.first-task.v1:timmy', 'coaching');
@ -82,17 +84,54 @@ def test_release_artifact_guides_an_empty_mobile_account_to_first_work(
getLogin: () => 'timmy', hasWork: () => true, isTodayActive: () => true, getLogin: () => 'timmy', hasWork: () => true, isTodayActive: () => true,
coach: isolatedCoach, coach: isolatedCoach,
}); });
return window.firstTaskOutcomeProbe.refresh(); return {
result: window.firstTaskOutcomeProbe.refresh(),
state: localStorage.getItem('stackchain.first-task.v1:timmy'),
mobile: matchMedia('(max-width: 600px)').matches,
};
}""" }"""
) )
assert probe_result == {"result": "coaching", "state": "coaching", "mobile": True}
coach = page.locator("[data-mobile-first-task-coach]") coach = page.locator("[data-mobile-first-task-coach]")
try:
expect(coach).to_be_visible() expect(coach).to_be_visible()
except AssertionError as error:
raise AssertionError(page.evaluate("""() => {
const coach = document.querySelector('[data-mobile-first-task-coach]');
const hud = coach.closest('[data-mobile-today-hud]');
return {
coachHidden: coach.hidden,
coachDisplay: getComputedStyle(coach).display,
hudHidden: hud.hidden,
hudOverlay: hud.getAttribute('data-overlay-hidden'),
hudDisplay: getComputedStyle(hud).display,
open: Array.from(document.querySelectorAll('dialog, .open'))
.filter(element => element.open || element.classList.contains('open'))
.map(element => element.id || element.className),
};
}""")) from error
expect(coach).to_contain_text("Complete your first task") expect(coach).to_contain_text("Complete your first task")
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
assert page.evaluate("window.firstTaskOutcomeProbe.completeOutcome()") is True completion = page.evaluate("""() => {
const completed = window.firstTaskOutcomeProbe.completeOutcome();
return {
completed,
status: document.querySelector('#today-sync-status').textContent,
pending: Object.keys(localStorage).filter(key =>
key.startsWith('stackchain.today-sync.v1.timmy.operation.')),
};
}""")
assert completion["completed"] is True
assert completion["pending"]
assert "sync pending" in completion["status"]
expect(page.locator("#mobile-first-task-receipt")).to_be_visible() expect(page.locator("#mobile-first-task-receipt")).to_be_visible()
expect(coach).to_be_hidden() expect(coach).to_be_hidden()
expect(page.locator("#today-sync-status")).to_contain_text("Today saved to account") expect(page.locator("#today-sync-status")).to_contain_text("Today saved to account")
remote_activation = page.evaluate("""async () => {
const response = await fetch('api/v1/today');
return (await response.json()).first_task_state;
}""")
assert remote_activation == "complete"
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
fresh_context = browser.new_context( fresh_context = browser.new_context(

View File

@ -278,7 +278,16 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
assert page.evaluate("""() => !Object.keys(localStorage).some( assert page.evaluate("""() => !Object.keys(localStorage).some(
key => key.startsWith('stackchain.today-sync.v1.timmy.operation.') key => key.startsWith('stackchain.today-sync.v1.timmy.operation.')
)""") )""")
try:
expect(page.locator("#today-break-status")).to_contain_text("On break · resume in") expect(page.locator("#today-break-status")).to_contain_text("On break · resume in")
except AssertionError as error:
raise AssertionError(page.evaluate("""() => ({
login: document.querySelector('#current-user')?.textContent,
timers: Object.fromEntries(Object.keys(localStorage)
.filter(key => key.startsWith('stackchain.today-timer.v1.'))
.map(key => [key, JSON.parse(localStorage.getItem(key))])),
status: document.querySelector('#today-break-status').outerHTML,
})""")) from error
if page.locator("#issue-sheet").get_attribute("class") == "issue-sheet open": if page.locator("#issue-sheet").get_attribute("class") == "issue-sheet open":
page.locator("#close-issue-sheet").click() page.locator("#close-issue-sheet").click()
if page.locator("#plan-today-sheet").is_visible(): if page.locator("#plan-today-sheet").is_visible():

View File

@ -117,6 +117,32 @@ const poller = createContextPoller({{
} }
def test_context_poller_can_force_a_full_identity_refresh_after_adoption():
script = f"""
const createContextPoller = require({json.dumps(str(POLLER))});
const requested = [];
const seed = {{
context:{{user:{{login:'timmy'}}}}, events:[], notifications:[],
revisions:{{context:'0123456789abcdef.1',events:'fedcba9876543210.2'}},
}};
const poller = createContextPoller({{
fetchContext: revisions => {{
requested.push({{...revisions}});
return Promise.resolve({{...seed, freshness:{{sections:{{context:{{degraded:false}}}}}}}});
}},
onSnapshot: () => {{}}, onError: error => {{ throw error; }},
setTimer: () => 1, clearTimer: () => {{}},
}});
poller.adopt(seed);
(async () => {{
await poller.refresh({{force:true, full:true}});
process.stdout.write(JSON.stringify({{requested}}));
}})();
"""
assert run_node(script) == {"requested": [{}]}
def test_context_poller_adopts_progressive_snapshot_before_revision_conditional_refresh(): def test_context_poller_adopts_progressive_snapshot_before_revision_conditional_refresh():
script = f""" script = f"""
const createContextPoller = require({json.dumps(str(POLLER))}); const createContextPoller = require({json.dumps(str(POLLER))});
@ -200,6 +226,9 @@ def test_dashboard_adopts_progressive_snapshot_or_falls_back_to_immediate_load()
assert "let adoptedProgressiveSnapshot = contextPoller.adopt(progressiveWorkHandoff?.liveSnapshot);" in source assert "let adoptedProgressiveSnapshot = contextPoller.adopt(progressiveWorkHandoff?.liveSnapshot);" in source
assert "contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise)" in source assert "contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise)" in source
assert "if (!adoptedProgressiveSnapshot) await load();" in source assert "if (!adoptedProgressiveSnapshot) await load();" in source
assert "contextPoller.refresh({ force:true, full:true })" in source
assert "fetchReviewJson('api/v1/background-identity')" in source
assert "initialAccountRecovery = timerView.restore(todaySync.flush());" in source
def test_context_poller_uses_failed_section_retry_before_healthy_freshness_deadline(): def test_context_poller_uses_failed_section_retry_before_healthy_freshness_deadline():