From 9b1a2ada546fbaead183faba0a2be078d41f31f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Thu, 16 Jul 2026 00:31:17 +0800 Subject: [PATCH 1/2] feat(cron): add deterministic test seam for cron-interactive E2E Add forceFireJob(id) method and QWEN_CODE_TEST_CRON_FAST env var to CronScheduler. When enabled, newly created session-only jobs auto-fire after 5s (configurable via QWEN_CODE_TEST_CRON_DELAY_MS) instead of waiting up to 60s for the wall-clock minute boundary. This removes the timing-flakiness from cron-interactive.test.ts without changing any production behavior (seam is env-gated and inactive by default). Refs #6982 --- packages/core/src/services/cronScheduler.ts | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/core/src/services/cronScheduler.ts b/packages/core/src/services/cronScheduler.ts index 866e279ade4..10ebfe3e656 100644 --- a/packages/core/src/services/cronScheduler.ts +++ b/packages/core/src/services/cronScheduler.ts @@ -307,6 +307,11 @@ export class CronScheduler { private fileWatcher: fsSync.FSWatcher | null = null; private lockProbeTimer: ReturnType | null = null; private debounceTimer: ReturnType | null = null; + // Test-only auto-fire timers (QWEN_CODE_TEST_CRON_FAST). Each timer + // fires its job via forceFireJob after a short delay so integration + // tests don't wait for the wall-clock minute boundary. Cleared on + // stop()/destroy() so a session teardown never leaks a pending fire. + private testFireTimers = new Map>(); // Catch-up work detected before start() installed onFire — flushed // through onFire as soon as it exists. private pendingFires: PendingFire[] = []; @@ -365,6 +370,26 @@ export class CronScheduler { }; this.jobs.set(id, job); + + // Test seam: when QWEN_CODE_TEST_CRON_FAST is set, schedule an + // auto-fire for newly created session-only jobs so integration tests + // don't wait up to 60s for the wall-clock minute boundary. The timer + // fires once after the configured delay (default 5s), then the normal + // tick takes over for subsequent fires of recurring jobs. Timers are + // tracked in testFireTimers and cleared on stop()/destroy(). + if (process.env['QWEN_CODE_TEST_CRON_FAST'] === '1' && !job.durable) { + const delayMs = Number(process.env['QWEN_CODE_TEST_CRON_DELAY_MS']) || 5000; + const timer = setTimeout(() => { + this.testFireTimers.delete(id); + this.forceFireJob(id); + }, delayMs); + timer.unref(); + this.testFireTimers.set(id, timer); + debugLogger.debug( + `Test seam: auto-fire scheduled for job ${id} in ${delayMs}ms`, + ); + } + return job; } @@ -1148,6 +1173,22 @@ export class CronScheduler { this.skipDurableFire = predicate; } + /** + * Immediately fires a job by ID, bypassing the cron schedule check. + * Sets lastFiredAt to prevent the normal tick from re-firing the same + * minute slot. Returns true if the job existed and was fired, false + * otherwise. Primarily a test seam (see QWEN_CODE_TEST_CRON_FAST in + * create()); also useful for manual debug triggers. + */ + forceFireJob(id: string): boolean { + const job = this.jobs.get(id); + if (!job || !this.onFire) return false; + job.lastFiredAt = Date.now(); + debugLogger.debug(`forceFireJob: firing ${id} (${job.cronExpr})`); + this.onFire(job); + return true; + } + /** * Starts the scheduler tick. Calls `onFire` when a job is due. * Only fires when called — does not auto-fire missed intervals. @@ -1194,6 +1235,10 @@ export class CronScheduler { clearTimeout(this.debounceTimer); this.debounceTimer = null; } + // Clear any pending test-seam auto-fire timers so a torn-down + // scheduler never leaks a late forceFireJob call. + for (const timer of this.testFireTimers.values()) clearTimeout(timer); + this.testFireTimers.clear(); if (this.wakeups.size > 0) { debugLogger.debug(`stop() discarding ${this.wakeups.size} wakeup(s)`); this.wakeups.clear(); From 4e1c5eb8c3582b3e3a4971fe00eef05a41ed9a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Thu, 16 Jul 2026 00:31:26 +0800 Subject: [PATCH 2/2] test(cron): use QWEN_CODE_TEST_CRON_FAST seam, reduce timeouts to 30s Enable the CronScheduler test seam in cron-interactive tests via QWEN_CODE_TEST_CRON_FAST=1 in makeEnv(). This makes newly created cron jobs auto-fire after 5s instead of waiting for the wall-clock minute boundary. Reduce all waitForScreen timeouts from 90s to 30s since the fire is now deterministic (~5s after job creation + model round-trip). Refs #6982 --- .../interactive/cron-interactive.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/integration-tests/interactive/cron-interactive.test.ts b/integration-tests/interactive/cron-interactive.test.ts index 7739e86342e..122a732ad62 100644 --- a/integration-tests/interactive/cron-interactive.test.ts +++ b/integration-tests/interactive/cron-interactive.test.ts @@ -31,6 +31,11 @@ function makeEnv(): NodeJS.ProcessEnv { QWEN_CODE_LANG: 'en', TERM: 'xterm-256color', NODE_NO_WARNINGS: '1', + // Enable the CronScheduler test seam: newly created session-only + // jobs auto-fire after 5s instead of waiting for the wall-clock + // minute boundary. Removes the timing-flakiness from these tests + // (see #6982). + QWEN_CODE_TEST_CRON_FAST: '1', }; } @@ -58,7 +63,7 @@ function makeEnv(): NodeJS.ProcessEnv { await session.waitForScreen( (scr) => scr.includes('Cron: PONG7742'), 'cron notification "Cron: PONG7742"', - 90_000, + 30_000, ); await session.idle(5000); @@ -82,7 +87,7 @@ function makeEnv(): NodeJS.ProcessEnv { await session.waitForScreen( (scr) => scr.includes('Cron: CRONTICK99'), 'first cron fire "Cron: CRONTICK99"', - 90_000, + 30_000, ); await session.idle(5000); @@ -113,7 +118,7 @@ function makeEnv(): NodeJS.ProcessEnv { await session.waitForScreen( (scr) => scr.includes('FILEERR88'), 'model reporting FILEERR88 from cron prompt', - 90_000, + 30_000, ); await session.idle(5000);