Merge branch 'master' into fix/compact-before-model-switch
This commit is contained in:
commit
f9761a140c
7 files changed
+92
-42
No files matched your search
@@ -166,7 +166,6 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
const mediaResponses = new Map<string, number>()
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -191,7 +190,7 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
{ createdAt: WEB_FIXTURE_TIME - 60_000 })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
page.on('response', (response) => {
|
||||
const url = new URL(response.url())
|
||||
@@ -204,7 +203,6 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
unpinBrowserClock?.()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
if (imageOrigin !== undefined) await stopServer(imageOrigin.server)
|
||||
|
||||
@@ -191,7 +191,6 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
@@ -212,14 +211,13 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
await seedSession(scaffold, raw, SEED_ID, undefined, { createdAt: WEB_FIXTURE_TIME - 90_000 })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
unpinBrowserClock?.()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/** Fixture-day clock pins keep page scheduling native at every real-to-fixture distance. */
|
||||
import { chromium, type Browser } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { newEnglishPage, pinBrowserClock, WEB_FIXTURE_TIME } from './support.ts'
|
||||
|
||||
// Host clock shifts spread over one 2^32 ms period of the real-to-fixture
|
||||
// distance; Playwright's page clock stopped page timers in half of that period.
|
||||
const DISTANCE_PERIOD_MS = 2 ** 32
|
||||
const HOST_SHIFTS_MS = Array.from({ length: 8 }, (_, index) => index * DISTANCE_PERIOD_MS / 8)
|
||||
|
||||
describe('web e2e: pinned fixture clocks', () => {
|
||||
let browser: Browser
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch()
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
})
|
||||
|
||||
it.each(HOST_SHIFTS_MS)('runs page timers and reads the fixture day with the Host clock %d ms behind it', async (shift) => {
|
||||
const hostStartedAt = performance.now()
|
||||
const hostClock = vi.spyOn(Date, 'now').mockImplementation(() =>
|
||||
WEB_FIXTURE_TIME - shift + Math.floor(performance.now() - hostStartedAt))
|
||||
const page = await newEnglishPage(browser)
|
||||
try {
|
||||
await pinBrowserClock(page)
|
||||
for (const navigation of ['initial', 'replacement']) {
|
||||
await page.goto(`data:text/html,<body>${navigation}</body>`)
|
||||
const pageStartedAt = await page.evaluate(() => {
|
||||
requestAnimationFrame(() => { document.body.dataset.frame = 'done' })
|
||||
setTimeout(() => { document.body.dataset.timer = 'done' }, 20)
|
||||
return Date.now()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => ({
|
||||
frame: document.body.dataset.frame,
|
||||
timer: document.body.dataset.timer,
|
||||
})), { timeout: 5_000 }).toEqual({ frame: 'done', timer: 'done' })
|
||||
const dates = await page.evaluate(() => ({
|
||||
now: Date.now(),
|
||||
day: new Date().toISOString().slice(0, 10),
|
||||
explicit: new Date(0).getTime(),
|
||||
parsed: Date.parse('2000-01-01T00:00:00Z'),
|
||||
}))
|
||||
expect(pageStartedAt).toBeGreaterThanOrEqual(WEB_FIXTURE_TIME)
|
||||
expect(dates.now).toBeGreaterThan(pageStartedAt)
|
||||
expect(dates.day).toBe(new Date(WEB_FIXTURE_TIME).toISOString().slice(0, 10))
|
||||
expect(dates.explicit).toBe(0)
|
||||
expect(dates.parsed).toBe(946_684_800_000)
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await page.close()
|
||||
} finally {
|
||||
hostClock.mockRestore()
|
||||
}
|
||||
}
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -142,7 +142,6 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
@@ -151,7 +150,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
|
||||
await seedSession(scaffold, targetSessionFixture(), TARGET_SESSION_ID, undefined, { createdAt: targetCreatedAt })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
// Fixture files land before the workspace connects so the Host's file
|
||||
// index never races their creation (the connect helper mkdirs the same
|
||||
@@ -171,7 +170,6 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
unpinBrowserClock?.()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
@@ -230,7 +230,6 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
let seededThroughSeq = -1
|
||||
let openingWindow: unknown
|
||||
let hostClock: ReturnType<typeof pinHostClock> | undefined
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
// Seeded and later command timestamps share the renderer's fixture day across midnight.
|
||||
@@ -271,7 +270,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
if (MODE !== 'record') unpinBrowserClock = await pinBrowserClock(page)
|
||||
if (MODE !== 'record') await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
if (MODE !== 'record') {
|
||||
// A one-message tail makes this short recording exercise the real Load earlier path.
|
||||
@@ -303,7 +302,6 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
unpinBrowserClock?.()
|
||||
hostClock?.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -73,7 +73,6 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let hostClock: MockInstance<typeof Date.now> | undefined
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -87,7 +86,7 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -101,8 +100,7 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
// A failing close (unconsumed fixture, mismatched snapshot) must not
|
||||
// leave the pinned clocks running for the suites after this one.
|
||||
unpinBrowserClock?.()
|
||||
// leave the pinned Host clock running for the suites after this one.
|
||||
hostClock?.mockRestore()
|
||||
}
|
||||
})
|
||||
@@ -396,7 +394,6 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let hostClock: MockInstance<typeof Date.now> | undefined
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -416,7 +413,7 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -431,7 +428,6 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
unpinBrowserClock?.()
|
||||
hostClock?.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
+25
-23
@@ -50,9 +50,6 @@ export const WEB_FIXTURE_TIME = Date.parse('2026-01-15T12:00:00+08:00')
|
||||
*/
|
||||
export const PLUGIN_TOGGLE_SETTLE_MS = 10_000
|
||||
|
||||
/** How often a pinned browser clock advances, in milliseconds. */
|
||||
const PINNED_CLOCK_STEP_MS = 250
|
||||
|
||||
/**
|
||||
* Pin the Host clock to the shared fixture day for one scenario suite.
|
||||
*
|
||||
@@ -74,31 +71,36 @@ export function pinHostClock(): MockInstance<typeof Date.now> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a scenario page's clock to the shared fixture day, advancing with real
|
||||
* Pin a scenario page's calendar to the shared fixture day, advancing with real
|
||||
* time. Pair it with {@link pinHostClock} for a scenario that stamps its own
|
||||
* message times, and with `seedSession`'s `createdAt` for a seeded one.
|
||||
*
|
||||
* The frozen instant is re-applied from the anchor on an interval rather than
|
||||
* left at one value, because product logic reads two `Date.now()` values to
|
||||
* decide a minimum display hold or a deadline window; a permanently frozen
|
||||
* clock stops supplying elapsed time, so a title that owes its golden settled
|
||||
* text keeps rendering its previous one. Both clocks therefore advance at the
|
||||
* real rate and stay within one step of each other.
|
||||
* @param page - page whose clock is pinned.
|
||||
* @returns the disposer that stops re-pinning; call it in teardown.
|
||||
* Only the page's `Date` reads the fixture day; native timers and
|
||||
* `requestAnimationFrame` keep their real scheduling. Playwright's
|
||||
* `page.clock` is not used: its server records clock state with the Vitest
|
||||
* worker's `Date.now`, which {@link pinHostClock} replaces, so after a
|
||||
* navigation the page clock replays the gap between the real and fixture days
|
||||
* and hands the resulting negative delay to native `setTimeout`. The browser
|
||||
* truncates that delay to a 32-bit integer, and in about half of the dates the
|
||||
* result is a positive delay of weeks that stops every page timer. The `Date`
|
||||
* override is installed for every later document and on the current one, and
|
||||
* stays until the page closes.
|
||||
* @param page - page whose calendar is pinned.
|
||||
*/
|
||||
export async function pinBrowserClock(page: Page): Promise<() => void> {
|
||||
const startedAt = Date.now()
|
||||
const step = async (): Promise<void> => {
|
||||
try {
|
||||
await page.clock.setFixedTime(WEB_FIXTURE_TIME + (Date.now() - startedAt))
|
||||
} catch {
|
||||
// The page closed under the interval; teardown owns the last word.
|
||||
}
|
||||
export async function pinBrowserClock(page: Page): Promise<void> {
|
||||
// The worker's `new Date()` keeps the real clock even when `Date.now` is pinned.
|
||||
const offset = WEB_FIXTURE_TIME - new Date().getTime()
|
||||
const install = (offset: number): void => {
|
||||
const NativeDate = Date
|
||||
const now = (): number => NativeDate.now() + offset
|
||||
globalThis.Date = new Proxy(NativeDate, {
|
||||
construct: (target, args, newTarget): Date => Reflect.construct(target, args.length === 0 ? [now()] : args, newTarget) as Date,
|
||||
apply: () => new NativeDate(now()).toString(),
|
||||
get: (target, property, receiver): unknown => property === 'now' ? now : Reflect.get(target, property, receiver),
|
||||
})
|
||||
}
|
||||
await step()
|
||||
const timer = setInterval(() => { void step() }, PINNED_CLOCK_STEP_MS)
|
||||
return () => { clearInterval(timer) }
|
||||
await page.addInitScript(install, offset)
|
||||
await page.evaluate(install, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user