From ef78353a053143f70b73ed931196ba1fde6fbc07 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 13 Jul 2026 16:19:48 +0800 Subject: [PATCH 1/4] test: cut slow suite runtimes and isolate experimental flag from host env - minidb compaction-race: drop the snapshot-phase stress from 150k to 10k records; writeSnapshot yields every 2000 records, so 10k still spans five yields and keeps the write-vs-compact race meaningful (~50s -> ~3s on CI) - image-compress / read-media (agent-core + agent-core-v2): shrink noise and solid-color fixtures to the smallest sizes that still exercise the ladder and downsample paths (60s/38s -> ~15s/11s, 23s/9s -> ~4s/3s) - goal-prompt, update/preflight: stub KIMI_CODE_EXPERIMENTAL_FLAG off in beforeEach so a host env leak cannot route tests down the unmocked v2 path (they hung until the 5s testTimeout) or bypass rollout batch holds - minidb compaction: give the concurrent-write recovery test a 15s timeout so full-suite CPU contention cannot trip the 5s default --- apps/kimi-code/test/cli/goal-prompt.test.ts | 6 + .../test/cli/update/preflight.test.ts | 4 + .../tools/support/image-compress.test.ts | 118 +++++++-------- .../test/agent/media/tools/read-media.test.ts | 26 ++-- .../test/tools/image-compress.test.ts | 142 +++++++++--------- .../agent-core/test/tools/read-media.test.ts | 38 ++--- packages/minidb/test/compaction.test.ts | 2 +- .../minidb/test/e2e/compaction-race.test.ts | 9 +- 8 files changed, 180 insertions(+), 165 deletions(-) diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index 0846a8e147..89770deef7 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -168,6 +168,11 @@ describe('runPrompt headless goal mode', () => { let savedExitCode: typeof process.exitCode; beforeEach(() => { + // Pin the experimental engine flag off so runPrompt stays on the v1 path + // this suite mocks, regardless of the host environment (matches + // run-prompt.test.ts). With the flag on, runPrompt dispatches to the + // native v2 runner, which ignores these mocks and hangs the test. + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', ''); savedExitCode = process.exitCode; mocks.experimentalFeatures = [{ id: 'micro_compaction', enabled: true }]; mocks.sessions = []; @@ -180,6 +185,7 @@ describe('runPrompt headless goal mode', () => { }); afterEach(() => { + vi.unstubAllEnvs(); process.exitCode = savedExitCode; }); diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index 4bc79289b2..ae6dd3bc03 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -227,6 +227,10 @@ async function flushBackgroundInstall(): Promise { describe('runUpdatePreflight', () => { beforeEach(() => { + // Pin the experimental flag off so rollout gating is deterministic + // regardless of the host environment (the flag bypasses batch holds). + // Tests that exercise the bypass opt back in with `vi.stubEnv(..., '1')`. + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', ''); mocks.readUpdateInstallState.mockResolvedValue(emptyUpdateInstallState()); mocks.writeUpdateInstallState.mockResolvedValue(undefined); mocks.loadTuiConfig.mockResolvedValue(tuiConfig()); diff --git a/packages/agent-core-v2/test/_base/tools/support/image-compress.test.ts b/packages/agent-core-v2/test/_base/tools/support/image-compress.test.ts index 9ab99d1495..a6cdc7bad8 100644 --- a/packages/agent-core-v2/test/_base/tools/support/image-compress.test.ts +++ b/packages/agent-core-v2/test/_base/tools/support/image-compress.test.ts @@ -182,11 +182,11 @@ describe('compressImageForModel — fast path', () => { describe('compressImageForModel — dimension cap', () => { it('scales the longest edge down to MAX_IMAGE_EDGE_PX, preserving aspect', async () => { - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(Math.max(result.width, result.height)).toBe(MAX_IMAGE_EDGE_PX); - // 4500x2250 → 2000x1000 (aspect 2:1 preserved). + // 2100x1050 → 2000x1000 (aspect 2:1 preserved). expect(result.width).toBe(2000); expect(result.height).toBe(1000); const dims = sniffImageDimensions(result.data); @@ -194,7 +194,7 @@ describe('compressImageForModel — dimension cap', () => { }); it('respects a custom maxEdge', async () => { - const png = await solidPng(1600, 800); + const png = await solidPng(1000, 500); const result = await compressImageForModel(png, 'image/png', { maxEdge: 800 }); expect(result.changed).toBe(true); expect(result.width).toBe(800); @@ -204,7 +204,7 @@ describe('compressImageForModel — dimension cap', () => { it('keeps a downscaled opaque PNG lossless (no needless JPEG conversion)', async () => { // A screenshot-like opaque PNG that only needs downscaling must stay PNG so // sharp text is not degraded by JPEG artifacts. - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/png'); @@ -216,7 +216,7 @@ describe('compressImageForModel — dimension cap', () => { describe('compressImageForModel — byte budget', () => { it('walks the JPEG ladder for an over-budget non-alpha image', async () => { - const png = await noisePng(900, 900); + const png = await noisePng(500, 500); const result = await compressImageForModel(png, 'image/png', { byteBudget: 8 * 1024 }); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/jpeg'); @@ -224,7 +224,7 @@ describe('compressImageForModel — byte budget', () => { }); it('keeps a translucent PNG as PNG when the budget allows', async () => { - const png = await translucentPng(3600, 1800); + const png = await translucentPng(2100, 1050); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/png'); @@ -233,7 +233,7 @@ describe('compressImageForModel — byte budget', () => { }); it('drops alpha to JPEG only as a last resort under a tiny budget', async () => { - const png = await noisePng(800, 800, /* alpha */ true); + const png = await noisePng(400, 400, /* alpha */ true); const result = await compressImageForModel(png, 'image/png', { byteBudget: 4 * 1024 }); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/jpeg'); @@ -355,7 +355,7 @@ describe('compressImageForModel — fallback', () => { it('skips compression for payloads over the byte cap without decoding', async () => { // Over the edge (so not the fast path), but capped by maxDecodeBytes. - const png = await solidPng(3200, 100); + const png = await solidPng(2100, 100); const result = await compressImageForModel(png, 'image/png', { maxDecodeBytes: 64 }); expect(result.changed).toBe(false); expect(result.data).toBe(png); // passthrough → Jimp was never called @@ -367,9 +367,9 @@ describe('compressImageForModel — fallback', () => { describe('compressImageForModel — invariants', () => { it('changed always yields a within-cap, decodable payload', async () => { const cases: Uint8Array[] = [ - await solidPng(4500, 2250), - await noisePng(900, 900), - await translucentPng(3600, 1800), + await solidPng(2100, 1050), + await noisePng(400, 400), + await translucentPng(2100, 1050), ]; for (const bytes of cases) { const result = await compressImageForModel(bytes, 'image/png'); @@ -393,7 +393,7 @@ describe('compressImageForModel — invariants', () => { describe('compressBase64ForModel', () => { it('round-trips an over-sized image', async () => { - const png = await noisePng(700, 700); + const png = await noisePng(500, 500); const base64 = Buffer.from(png).toString('base64'); const result = await compressBase64ForModel(base64, 'image/png', { byteBudget: 8 * 1024 }); expect(result.changed).toBe(true); @@ -412,7 +412,7 @@ describe('compressBase64ForModel', () => { }); it('skips a base64 payload over the byte cap without decoding', async () => { - const png = await solidPng(3200, 100); // over edge, would otherwise compress + const png = await solidPng(2100, 100); // over edge, would otherwise compress const base64 = Buffer.from(png).toString('base64'); const result = await compressBase64ForModel(base64, 'image/png', { maxDecodeBytes: 64 }); expect(result.changed).toBe(false); @@ -436,7 +436,7 @@ describe('compressImageForModel — performance', () => { }); it('compresses a large image within a generous time bound', async () => { - const png = await solidPng(4500, 3000); + const png = await solidPng(2100, 1050); const start = performance.now(); const result = await compressImageForModel(png, 'image/png'); const elapsed = performance.now() - start; @@ -458,7 +458,7 @@ describe('compressImageContentParts', () => { } it('compresses an oversized inline image part, leaving other parts untouched', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const parts = [ { type: 'text' as const, text: 'look at this' }, { type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big) } }, @@ -491,7 +491,7 @@ describe('compressImageContentParts', () => { }); it('keeps an image part id when rewriting the compressed url', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const parts = [ { type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big), id: 'att-1' } }, ]; @@ -543,21 +543,21 @@ describe('compressImageForModel — original dimensions metadata', () => { expect(pass.originalWidth).toBe(64); expect(pass.originalHeight).toBe(64); - const big = await solidPng(4500, 2250); + const big = await solidPng(2100, 1050); const shrunk = await compressImageForModel(big, 'image/png'); expect(shrunk.changed).toBe(true); - expect(shrunk.originalWidth).toBe(4500); - expect(shrunk.originalHeight).toBe(2250); + expect(shrunk.originalWidth).toBe(2100); + expect(shrunk.originalHeight).toBe(1050); expect(shrunk.width).toBe(2000); }); it('reports original dimensions through the base64 wrapper', async () => { - const big = await solidPng(3900, 1950); + const big = await solidPng(2100, 1050); const base64 = Buffer.from(big).toString('base64'); const result = await compressBase64ForModel(base64, 'image/png'); expect(result.changed).toBe(true); - expect(result.originalWidth).toBe(3900); - expect(result.originalHeight).toBe(1950); + expect(result.originalWidth).toBe(2100); + expect(result.originalHeight).toBe(1050); expect(result.width).toBe(2000); expect(result.height).toBe(1000); }); @@ -587,7 +587,7 @@ describe('cropImageForModel', () => { }); it('preserves the JPEG format when cropping a JPEG', async () => { - const jpeg = await solidJpeg(2400, 1200); + const jpeg = await solidJpeg(800, 400); const result = await cropImageForModel(jpeg, 'image/jpeg', { x: 0, y: 0, @@ -617,31 +617,31 @@ describe('cropImageForModel', () => { }); it('rejects a region fully outside the image, naming the original size', async () => { - const png = await solidPng(3000, 1500); + const png = await solidPng(2100, 1050); const result = await cropImageForModel(png, 'image/png', { - x: 3000, + x: 2100, y: 0, width: 100, height: 100, }); expect(result.ok).toBe(false); if (result.ok) return; - expect(result.error).toContain('3000x1500'); + expect(result.error).toContain('2100x1050'); }); it('downscales an oversized crop to the edge cap by default', async () => { - const png = await solidPng(4500, 2250); + const png = await solidPng(2500, 1250); const result = await cropImageForModel(png, 'image/png', { x: 0, y: 0, - width: 4000, + width: 2400, height: 1200, }); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.resized).toBe(true); expect(Math.max(result.width, result.height)).toBeLessThanOrEqual(MAX_IMAGE_EDGE_PX); - expect(result.region).toEqual({ x: 0, y: 0, width: 4000, height: 1200 }); + expect(result.region).toEqual({ x: 0, y: 0, width: 2400, height: 1200 }); }); it('keeps native resolution with skipResize', async () => { @@ -660,11 +660,11 @@ describe('cropImageForModel', () => { }); it('fails explicitly when a skipResize crop exceeds the byte budget', async () => { - const png = await noisePng(900, 900); + const png = await noisePng(400, 400); const result = await cropImageForModel( png, 'image/png', - { x: 0, y: 0, width: 900, height: 900 }, + { x: 0, y: 0, width: 400, height: 400 }, { skipResize: true, byteBudget: 8 * 1024 }, ); expect(result.ok).toBe(false); @@ -815,7 +815,7 @@ describe('compressImageContentParts — annotate', () => { } it('collects a caption for a compressed image and persists the original', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const persisted: { bytes: Uint8Array; mimeType: string }[] = []; const parts = [{ type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big) } }]; const out = await compressImageContentParts(parts, { @@ -831,7 +831,7 @@ describe('compressImageContentParts — annotate', () => { expect(out.parts).toHaveLength(1); expect(out.parts[0]?.type).toBe('image_url'); expect(out.captions).toHaveLength(1); - expect(out.captions[0]).toContain('3600x1800'); + expect(out.captions[0]).toContain('2100x1050'); expect(out.captions[0]).toContain('/tmp/originals/big.png'); expect(persisted).toHaveLength(1); expect(persisted[0]?.mimeType).toBe('image/png'); @@ -850,7 +850,7 @@ describe('compressImageContentParts — annotate', () => { }); it('captions without a path when persistence fails', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const parts = [{ type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big) } }]; const out = await compressImageContentParts(parts, { annotate: { persistOriginal: () => Promise.resolve(null) }, @@ -914,13 +914,13 @@ function grayStats(image: { bitmap: { data: Buffer | Uint8Array } }): GrayStats describe('compressImageForModel — downscale quality guards', () => { it('averages a 1px checkerboard to flat gray at an integer ratio (no aliasing)', async () => { - // 2000 → 500 (4:1). Every output pixel covers a 4×4 block holding 8 + // 1000 → 250 (4:1). Every output pixel covers a 4×4 block holding 8 // black and 8 white pixels, so a full-coverage average lands on ~127. // Aliasing would instead show up as black/white patches or moiré bands. - const png = await checkerboardPng(2000); - const result = await compressImageForModel(png, 'image/png', { maxEdge: 500 }); + const png = await checkerboardPng(1000); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 250 }); expect(result.changed).toBe(true); - expect(Math.max(result.width, result.height)).toBe(500); + expect(Math.max(result.width, result.height)).toBe(250); const decoded = await Jimp.fromBuffer(Buffer.from(result.data)); const { min, max } = grayStats(decoded); @@ -929,11 +929,11 @@ describe('compressImageForModel — downscale quality guards', () => { }); it('stays alias-free at a non-integer ratio (fractional pixel coverage)', async () => { - // 2000 → 780 (≈2.56:1). Non-integer ratios are where phase-dependent + // 1000 → 390 (≈2.56:1). Non-integer ratios are where phase-dependent // point sampling degrades worst. Fractional window coverage leaves the // average some mild texture, but nothing may approach black or white. - const png = await checkerboardPng(2000); - const result = await compressImageForModel(png, 'image/png', { maxEdge: 780 }); + const png = await checkerboardPng(1000); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 390 }); expect(result.changed).toBe(true); const decoded = await Jimp.fromBuffer(Buffer.from(result.data)); @@ -952,8 +952,8 @@ describe('compressImageForModel — downscale quality guards', () => { // distinguishes resamplers) and pins the library behavior the // mode-less default call relies on — if jimp ever changes either // side, revisit the fitWithinEdge comment. - const image = await Jimp.fromBuffer(Buffer.from(await checkerboardPng(2000))); - image.resize({ w: 500, h: 500, mode: ResizeStrategy.BILINEAR }); + const image = await Jimp.fromBuffer(Buffer.from(await checkerboardPng(1000))); + image.resize({ w: 250, h: 250, mode: ResizeStrategy.BILINEAR }); const { min, max, mean } = grayStats(image); // The correct answer is flat ~127 gray (mean ≈ 127, max-min ≈ 0). // Aliasing shows up as a solid black/white collapse or full-contrast @@ -967,11 +967,11 @@ describe('compressImageForModel — downscale quality guards', () => { // blends them into the average tints every transparency edge (halo). // Probe: a fully transparent BRIGHT RED field around an opaque blue // square — after a 4:1 downscale no visible pixel may pick up red. - const size = 1600; + const size = 800; const image = new Jimp({ width: size, height: size, color: 0xff000000 }); // red, alpha 0 const data = image.bitmap.data; - for (let y = 400; y < 1200; y += 1) { - for (let x = 400; x < 1200; x += 1) { + for (let y = 200; y < 600; y += 1) { + for (let x = 200; x < 600; x += 1) { const i = (y * size + x) * 4; data[i] = 0; data[i + 1] = 0; @@ -981,7 +981,7 @@ describe('compressImageForModel — downscale quality guards', () => { } const png = new Uint8Array(await image.getBuffer('image/png')); - const result = await compressImageForModel(png, 'image/png', { maxEdge: 400 }); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 200 }); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/png'); // alpha survives @@ -1000,11 +1000,11 @@ describe('compressImageForModel — downscale quality guards', () => { it('preserves mean brightness through the downscale (no energy drift)', async () => { // A normalized filter keeps the image mean; drift here would indicate // non-normalized weights (or a broken gamma pipeline stage). - const png = await noisePng(900, 900); + const png = await noisePng(400, 400); const input = await Jimp.fromBuffer(Buffer.from(png)); const inputMean = grayStats(input).mean; - const result = await compressImageForModel(png, 'image/png', { maxEdge: 225 }); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 100 }); expect(result.changed).toBe(true); const output = await Jimp.fromBuffer(Buffer.from(result.data)); expect(Math.abs(grayStats(output).mean - inputMean)).toBeLessThan(3); @@ -1014,7 +1014,7 @@ describe('compressImageForModel — downscale quality guards', () => { // Model-bound bytes can re-enter the pipeline (session replay, MCP // round-trips). Once within budget they must pass through untouched // instead of being shaved a little smaller on every pass. - const first = await compressImageForModel(await solidPng(4500, 2250), 'image/png'); + const first = await compressImageForModel(await solidPng(2100, 1050), 'image/png'); expect(first.changed).toBe(true); const second = await compressImageForModel(first.data, first.mimeType); @@ -1052,7 +1052,7 @@ function captureTelemetry(): { client: ImageCompressionTelemetryClient; events: describe('compressImageForModel — telemetry', () => { it('reports a compressed image with sizes, formats, and duration', async () => { const { client, events } = captureTelemetry(); - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png', { telemetry: { client, source: 'read_media' }, }); @@ -1067,8 +1067,8 @@ describe('compressImageForModel — telemetry', () => { expect(props['output_mime']).toBe(result.mimeType); expect(props['original_bytes']).toBe(png.length); expect(props['final_bytes']).toBe(result.finalByteLength); - expect(props['original_width']).toBe(4500); - expect(props['original_height']).toBe(2250); + expect(props['original_width']).toBe(2100); + expect(props['original_height']).toBe(1050); expect(props['final_width']).toBe(2000); expect(props['final_height']).toBe(1000); expect(props['exif_transposed']).toBe(false); @@ -1101,7 +1101,7 @@ describe('compressImageForModel — telemetry', () => { expect(bomb.events[0]!.props['outcome']).toBe('passthrough_guard'); const byteCap = captureTelemetry(); - await compressImageForModel(await solidPng(3200, 100), 'image/png', { + await compressImageForModel(await solidPng(2100, 100), 'image/png', { maxDecodeBytes: 64, telemetry: { client: byteCap.client, source: 'mcp_tool_result' }, }); @@ -1154,7 +1154,7 @@ describe('compressImageForModel — telemetry', () => { it('reports the base64 early size-skip as passthrough_guard', async () => { const { client, events } = captureTelemetry(); - const base64 = Buffer.from(await solidPng(3200, 100)).toString('base64'); + const base64 = Buffer.from(await solidPng(2100, 100)).toString('base64'); await compressBase64ForModel(base64, 'image/png', { maxDecodeBytes: 64, telemetry: { client, source: 'prompt_file' }, @@ -1166,7 +1166,7 @@ describe('compressImageForModel — telemetry', () => { it('threads telemetry through compressImageContentParts', async () => { const { client, events } = captureTelemetry(); - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const url = `data:image/png;base64,${Buffer.from(big).toString('base64')}`; await compressImageContentParts([{ type: 'image_url', imageUrl: { url } }], { telemetry: { client, source: 'mcp_tool_result' }, @@ -1183,7 +1183,7 @@ describe('compressImageForModel — telemetry', () => { throw new Error('sink down'); }, }; - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png', { telemetry: { client: throwing, source: 'read_media' }, }); @@ -1239,9 +1239,9 @@ describe('cropImageForModel — telemetry', () => { const budget = captureTelemetry(); await cropImageForModel( - await noisePng(900, 900), + await noisePng(400, 400), 'image/png', - { x: 0, y: 0, width: 900, height: 900 }, + { x: 0, y: 0, width: 400, height: 400 }, { skipResize: true, byteBudget: 8 * 1024, telemetry: { client: budget.client, source: 'read_media' } }, ); expect(budget.events[0]!.props['error_kind']).toBe('budget'); diff --git a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts index 166f561e8b..3c7c4ad7fb 100644 --- a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts +++ b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts @@ -312,9 +312,9 @@ describe('ReadMediaFileTool', () => { it('downsamples large images and points the model to region readback', async () => { const big = Buffer.from( - await new Jimp({ width: 3600, height: 3600, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 2200, height: 2200, color: 0x3366ccff }).getBuffer('image/png'), ); - expect(sniffImageDimensions(big)).toEqual({ width: 3600, height: 3600 }); + expect(sniffImageDimensions(big)).toEqual({ width: 2200, height: 2200 }); const result = await execute(makeTool({ '/workspace/big.png': { data: big } }), { path: '/workspace/big.png', @@ -322,7 +322,7 @@ describe('ReadMediaFileTool', () => { // The note keeps the ORIGINAL size so coordinate mapping holds. const systemText = noteText(result); - expect(systemText).toContain('3600x3600'); + expect(systemText).toContain('2200x2200'); expect(systemText).toContain(`${String(big.length)} bytes`); // Wording must not depend on serialization order: some providers keep // the note inline after the media, others flatten tool text and @@ -357,7 +357,7 @@ describe('ReadMediaFileTool', () => { it('reads image regions at native resolution', async () => { const big = Buffer.from( - await new Jimp({ width: 2600, height: 2600, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 600, height: 600, color: 0x3366ccff }).getBuffer('image/png'), ); const result = await execute(makeTool({ '/workspace/big.png': { data: big } }), { path: '/workspace/big.png', @@ -372,7 +372,7 @@ describe('ReadMediaFileTool', () => { height: 300, }); const systemText = noteText(result); - expect(systemText).toContain('2600x2600'); + expect(systemText).toContain('600x600'); expect(systemText).toMatch(/region \(x=100, y=50, width=400, height=300\)/); expect(systemText).toMatch(/native resolution/); expect(systemText).toContain('offset'); @@ -380,20 +380,20 @@ describe('ReadMediaFileTool', () => { it('rejects a region outside the image with the original size in the error', async () => { const big = Buffer.from( - await new Jimp({ width: 2600, height: 2600, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 600, height: 600, color: 0x3366ccff }).getBuffer('image/png'), ); const result = await execute(makeTool({ '/workspace/big.png': { data: big } }), { path: '/workspace/big.png', region: { x: 5000, y: 0, width: 100, height: 100 }, }); expect(result.isError).toBe(true); - expect(result.output).toContain('2600x2600'); + expect(result.output).toContain('600x600'); }); it('serves full_resolution when the bytes fit the per-image budget', async () => { const big = Buffer.from( // Over the edge cap, tiny in bytes. - await new Jimp({ width: 3900, height: 1950, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 2100, height: 1050, color: 0x3366ccff }).getBuffer('image/png'), ); const result = await execute(makeTool({ '/workspace/big.png': { data: big } }), { path: '/workspace/big.png', @@ -425,13 +425,13 @@ describe('ReadMediaFileTool', () => { }); it('reports an EXIF-rotated original in the decoded coordinate space', async () => { - // Orientation 6 (rotate 90° CW): the header says 3600x1800, but jimp - // decodes to 1800x3600 — the space the sent image and any region + // Orientation 6 (rotate 90° CW): the header says 2200x1100, but jimp + // decodes to 1100x2200 — the space the sent image and any region // readback live in. The note's original size must match that space, // not the pre-rotation header sniff. const portrait = withExifOrientation( new Uint8Array( - await new Jimp({ width: 3600, height: 1800, color: 0x3366ccff }).getBuffer('image/jpeg', { + await new Jimp({ width: 2200, height: 1100, color: 0x3366ccff }).getBuffer('image/jpeg', { quality: 90, }), ), @@ -442,7 +442,7 @@ describe('ReadMediaFileTool', () => { }); const systemText = noteText(result); - expect(systemText).toContain('Original dimensions: 1800x3600'); + expect(systemText).toContain('Original dimensions: 1100x2200'); expect(systemText).toMatch(/downsampled to 1000x2000/); }, 15000); @@ -489,7 +489,7 @@ describe('ReadMediaFileTool', () => { it('emits image_compress and image_crop telemetry tagged read_media', async () => { const records: TelemetryRecord[] = []; const big = Buffer.from( - await new Jimp({ width: 3600, height: 1800, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 2200, height: 1100, color: 0x3366ccff }).getBuffer('image/png'), ); const tool = makeTool( { '/workspace/big.png': { data: big } }, diff --git a/packages/agent-core/test/tools/image-compress.test.ts b/packages/agent-core/test/tools/image-compress.test.ts index 4f0cb5be33..a0560a3e6e 100644 --- a/packages/agent-core/test/tools/image-compress.test.ts +++ b/packages/agent-core/test/tools/image-compress.test.ts @@ -194,11 +194,11 @@ describe('compressImageForModel — fast path', () => { describe('compressImageForModel — dimension cap', () => { it('scales the longest edge down to MAX_IMAGE_EDGE_PX, preserving aspect', async () => { - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(Math.max(result.width, result.height)).toBe(MAX_IMAGE_EDGE_PX); - // 4500x2250 → 2000x1000 (aspect 2:1 preserved). + // 2100x1050 → 2000x1000 (aspect 2:1 preserved). expect(result.width).toBe(2000); expect(result.height).toBe(1000); const dims = sniffImageDimensions(result.data); @@ -206,7 +206,7 @@ describe('compressImageForModel — dimension cap', () => { }); it('respects a custom maxEdge', async () => { - const png = await solidPng(1600, 800); + const png = await solidPng(1000, 500); const result = await compressImageForModel(png, 'image/png', { maxEdge: 800 }); expect(result.changed).toBe(true); expect(result.width).toBe(800); @@ -216,7 +216,7 @@ describe('compressImageForModel — dimension cap', () => { it('keeps a downscaled opaque PNG lossless (no needless JPEG conversion)', async () => { // A screenshot-like opaque PNG that only needs downscaling must stay PNG so // sharp text is not degraded by JPEG artifacts. - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/png'); @@ -228,7 +228,7 @@ describe('compressImageForModel — dimension cap', () => { describe('compressImageForModel — byte budget', () => { it('walks the JPEG ladder for an over-budget non-alpha image', async () => { - const png = await noisePng(900, 900); + const png = await noisePng(500, 500); const result = await compressImageForModel(png, 'image/png', { byteBudget: 8 * 1024 }); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/jpeg'); @@ -236,7 +236,7 @@ describe('compressImageForModel — byte budget', () => { }); it('keeps a translucent PNG as PNG when the budget allows', async () => { - const png = await translucentPng(3600, 1800); + const png = await translucentPng(2100, 1050); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/png'); @@ -245,7 +245,7 @@ describe('compressImageForModel — byte budget', () => { }); it('drops alpha to JPEG only as a last resort under a tiny budget', async () => { - const png = await noisePng(800, 800, /* alpha */ true); + const png = await noisePng(400, 400, /* alpha */ true); const result = await compressImageForModel(png, 'image/png', { byteBudget: 4 * 1024 }); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/jpeg'); @@ -377,13 +377,13 @@ describe('compressImageForModel — webp', () => { it( 'downscales an oversized WebP to the edge cap', async () => { - const source = new Jimp({ width: 2600, height: 1300, color: 0x3366ccff }); + const source = new Jimp({ width: 2100, height: 1050, color: 0x3366ccff }); const webp = await encodeWebp(source); const result = await compressImageForModel(webp, 'image/webp'); expect(result.changed).toBe(true); expect(Math.max(result.width, result.height)).toBe(2000); - expect(result.originalWidth).toBe(2600); - expect(result.originalHeight).toBe(1300); + expect(result.originalWidth).toBe(2100); + expect(result.originalHeight).toBe(1050); expect(sniffImageDimensions(result.data)).toEqual({ width: 2000, height: 1000 }); }, 15_000, @@ -393,7 +393,7 @@ describe('compressImageForModel — webp', () => { 're-encodes an over-budget WebP within the byte budget', async () => { const budget = 128 * 1024; - const noisy = new Jimp({ width: 1200, height: 1200, color: 0x000000ff }); + const noisy = new Jimp({ width: 700, height: 700, color: 0x000000ff }); fillXorshiftNoise(noisy.bitmap.data); const webp = await encodeWebp(noisy, 100); expect(webp.length).toBeGreaterThan(budget); @@ -407,7 +407,7 @@ describe('compressImageForModel — webp', () => { it( 'keeps alpha when re-encoding a translucent WebP', async () => { - const translucent = new Jimp({ width: 2600, height: 1300, color: 0x33_66_cc_80 }); + const translucent = new Jimp({ width: 2100, height: 1050, color: 0x33_66_cc_80 }); const webp = await encodeWebp(translucent); const result = await compressImageForModel(webp, 'image/webp'); expect(result.changed).toBe(true); @@ -459,7 +459,7 @@ describe('compressImageForModel — small byte budgets', () => { // charts): the old [2000, 1000] fallback floor left q20@1000px at ~200KB, // over a read-scale budget. The extended ladder must land within it. const budget = 128 * 1024; - const png = await randomNoisePng(1200, 1200); + const png = await randomNoisePng(700, 700); expect(png.length).toBeGreaterThan(budget); const result = await compressImageForModel(png, 'image/png', { byteBudget: budget }); expect(result.changed).toBe(true); @@ -473,7 +473,7 @@ describe('compressImageForModel — small byte budgets', () => { 'converges under a 128KB budget for a JPEG source', async () => { const budget = 128 * 1024; - const jpeg = await randomNoiseJpeg(1200, 1200); + const jpeg = await randomNoiseJpeg(700, 700); expect(jpeg.length).toBeGreaterThan(budget); const result = await compressImageForModel(jpeg, 'image/jpeg', { byteBudget: budget }); expect(result.changed).toBe(true); @@ -489,7 +489,7 @@ describe('compressImageForModel — small byte budgets', () => { // A JPEG already at the encoder's quality floor for its size: re-encoding // at the same size cannot shrink it, so without sub-size fallbacks the // "unhelpful" guard used to return the original — silently over budget. - const image = new Jimp({ width: 900, height: 900, color: 0x000000ff }); + const image = new Jimp({ width: 500, height: 500, color: 0x000000ff }); fillXorshiftNoise(image.bitmap.data); const optimized = new Uint8Array(await image.getBuffer('image/jpeg', { quality: 20 })); const budget = optimized.length - 10 * 1024; @@ -498,7 +498,7 @@ describe('compressImageForModel — small byte budgets', () => { const result = await compressImageForModel(optimized, 'image/jpeg', { byteBudget: budget }); expect(result.changed).toBe(true); expect(result.finalByteLength).toBeLessThanOrEqual(budget); - expect(Math.max(result.width, result.height)).toBeLessThan(900); + expect(Math.max(result.width, result.height)).toBeLessThan(500); }, 15_000, ); @@ -561,7 +561,7 @@ describe('compressImageForModel — fallback', () => { it('skips compression for payloads over the byte cap without decoding', async () => { // Over the edge (so not the fast path), but capped by maxDecodeBytes. - const png = await solidPng(3200, 100); + const png = await solidPng(2100, 100); const result = await compressImageForModel(png, 'image/png', { maxDecodeBytes: 64 }); expect(result.changed).toBe(false); expect(result.data).toBe(png); // passthrough → Jimp was never called @@ -573,9 +573,9 @@ describe('compressImageForModel — fallback', () => { describe('compressImageForModel — invariants', () => { it('changed always yields a within-cap, decodable payload', async () => { const cases: Uint8Array[] = [ - await solidPng(4500, 2250), - await noisePng(900, 900), - await translucentPng(3600, 1800), + await solidPng(2100, 1050), + await noisePng(400, 400), + await translucentPng(2100, 1050), ]; for (const bytes of cases) { const result = await compressImageForModel(bytes, 'image/png'); @@ -599,7 +599,7 @@ describe('compressImageForModel — invariants', () => { describe('compressBase64ForModel', () => { it('round-trips an over-sized image', async () => { - const png = await noisePng(700, 700); + const png = await noisePng(500, 500); const base64 = Buffer.from(png).toString('base64'); const result = await compressBase64ForModel(base64, 'image/png', { byteBudget: 8 * 1024 }); expect(result.changed).toBe(true); @@ -618,7 +618,7 @@ describe('compressBase64ForModel', () => { }); it('skips a base64 payload over the byte cap without decoding', async () => { - const png = await solidPng(3200, 100); // over edge, would otherwise compress + const png = await solidPng(2100, 100); // over edge, would otherwise compress const base64 = Buffer.from(png).toString('base64'); const result = await compressBase64ForModel(base64, 'image/png', { maxDecodeBytes: 64 }); expect(result.changed).toBe(false); @@ -642,7 +642,7 @@ describe('compressImageForModel — performance', () => { }); it('compresses a large image within a generous time bound', async () => { - const png = await solidPng(4500, 3000); + const png = await solidPng(2100, 1050); const start = performance.now(); const result = await compressImageForModel(png, 'image/png'); const elapsed = performance.now() - start; @@ -679,7 +679,7 @@ describe('resolveMaxImageEdgePx', () => { it('drives compressImageForModel when no explicit maxEdge is passed', async () => { vi.stubEnv(MAX_IMAGE_EDGE_ENV, '1200'); - const png = await solidPng(1600, 800); + const png = await solidPng(1300, 650); const result = await compressImageForModel(png, 'image/png'); expect(result.changed).toBe(true); expect(result.width).toBe(1200); @@ -688,7 +688,7 @@ describe('resolveMaxImageEdgePx', () => { it('an explicit maxEdge option still wins over the env var', async () => { vi.stubEnv(MAX_IMAGE_EDGE_ENV, '900'); - const png = await solidPng(1600, 800); + const png = await solidPng(1000, 500); const result = await compressImageForModel(png, 'image/png', { maxEdge: 800 }); expect(result.changed).toBe(true); expect(result.width).toBe(800); @@ -697,7 +697,7 @@ describe('resolveMaxImageEdgePx', () => { it('drives cropImageForModel region fitting', async () => { vi.stubEnv(MAX_IMAGE_EDGE_ENV, '400'); - const png = await solidPng(1600, 800); + const png = await solidPng(1000, 800); const result = await cropImageForModel(png, 'image/png', { x: 0, y: 0, @@ -796,7 +796,7 @@ describe('compressImageContentParts', () => { } it('compresses an oversized inline image part, leaving other parts untouched', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const parts = [ { type: 'text' as const, text: 'look at this' }, { type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big) } }, @@ -829,7 +829,7 @@ describe('compressImageContentParts', () => { }); it('keeps an image part id when rewriting the compressed url', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const parts = [ { type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big), id: 'att-1' } }, ]; @@ -1153,21 +1153,21 @@ describe('compressImageForModel — original dimensions metadata', () => { expect(pass.originalWidth).toBe(64); expect(pass.originalHeight).toBe(64); - const big = await solidPng(4500, 2250); + const big = await solidPng(2100, 1050); const shrunk = await compressImageForModel(big, 'image/png'); expect(shrunk.changed).toBe(true); - expect(shrunk.originalWidth).toBe(4500); - expect(shrunk.originalHeight).toBe(2250); + expect(shrunk.originalWidth).toBe(2100); + expect(shrunk.originalHeight).toBe(1050); expect(shrunk.width).toBe(2000); }); it('reports original dimensions through the base64 wrapper', async () => { - const big = await solidPng(3900, 1950); + const big = await solidPng(2100, 1050); const base64 = Buffer.from(big).toString('base64'); const result = await compressBase64ForModel(base64, 'image/png'); expect(result.changed).toBe(true); - expect(result.originalWidth).toBe(3900); - expect(result.originalHeight).toBe(1950); + expect(result.originalWidth).toBe(2100); + expect(result.originalHeight).toBe(1050); expect(result.width).toBe(2000); expect(result.height).toBe(1000); }); @@ -1197,7 +1197,7 @@ describe('cropImageForModel', () => { }); it('preserves the JPEG format when cropping a JPEG', async () => { - const jpeg = await solidJpeg(2400, 1200); + const jpeg = await solidJpeg(800, 400); const result = await cropImageForModel(jpeg, 'image/jpeg', { x: 0, y: 0, @@ -1227,31 +1227,31 @@ describe('cropImageForModel', () => { }); it('rejects a region fully outside the image, naming the original size', async () => { - const png = await solidPng(3000, 1500); + const png = await solidPng(2100, 1050); const result = await cropImageForModel(png, 'image/png', { - x: 3000, + x: 2100, y: 0, width: 100, height: 100, }); expect(result.ok).toBe(false); if (result.ok) return; - expect(result.error).toContain('3000x1500'); + expect(result.error).toContain('2100x1050'); }); it('downscales an oversized crop to the edge cap by default', async () => { - const png = await solidPng(4500, 2250); + const png = await solidPng(2500, 1250); const result = await cropImageForModel(png, 'image/png', { x: 0, y: 0, - width: 4000, + width: 2400, height: 1200, }); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.resized).toBe(true); expect(Math.max(result.width, result.height)).toBeLessThanOrEqual(MAX_IMAGE_EDGE_PX); - expect(result.region).toEqual({ x: 0, y: 0, width: 4000, height: 1200 }); + expect(result.region).toEqual({ x: 0, y: 0, width: 2400, height: 1200 }); }); it('keeps native resolution with skipResize', async () => { @@ -1270,11 +1270,11 @@ describe('cropImageForModel', () => { }); it('fails explicitly when a skipResize crop exceeds the byte budget', async () => { - const png = await noisePng(900, 900); + const png = await noisePng(400, 400); const result = await cropImageForModel( png, 'image/png', - { x: 0, y: 0, width: 900, height: 900 }, + { x: 0, y: 0, width: 400, height: 400 }, { skipResize: true, byteBudget: 8 * 1024 }, ); expect(result.ok).toBe(false); @@ -1425,7 +1425,7 @@ describe('compressImageContentParts — annotate', () => { } it('collects a caption for a compressed image and persists the original', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const persisted: { bytes: Uint8Array; mimeType: string }[] = []; const parts = [{ type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big) } }]; const out = await compressImageContentParts(parts, { @@ -1441,7 +1441,7 @@ describe('compressImageContentParts — annotate', () => { expect(out.parts).toHaveLength(1); expect(out.parts[0]?.type).toBe('image_url'); expect(out.captions).toHaveLength(1); - expect(out.captions[0]).toContain('3600x1800'); + expect(out.captions[0]).toContain('2100x1050'); expect(out.captions[0]).toContain('/tmp/originals/big.png'); expect(persisted).toHaveLength(1); expect(persisted[0]?.mimeType).toBe('image/png'); @@ -1460,7 +1460,7 @@ describe('compressImageContentParts — annotate', () => { }); it('captions without a path when persistence fails', async () => { - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const parts = [{ type: 'image_url' as const, imageUrl: { url: dataUrl('image/png', big) } }]; const out = await compressImageContentParts(parts, { annotate: { persistOriginal: () => Promise.resolve(null) }, @@ -1524,13 +1524,13 @@ function grayStats(image: { bitmap: { data: Buffer | Uint8Array } }): GrayStats describe('compressImageForModel — downscale quality guards', () => { it('averages a 1px checkerboard to flat gray at an integer ratio (no aliasing)', async () => { - // 2000 → 500 (4:1). Every output pixel covers a 4×4 block holding 8 + // 1000 → 250 (4:1). Every output pixel covers a 4×4 block holding 8 // black and 8 white pixels, so a full-coverage average lands on ~127. // Aliasing would instead show up as black/white patches or moiré bands. - const png = await checkerboardPng(2000); - const result = await compressImageForModel(png, 'image/png', { maxEdge: 500 }); + const png = await checkerboardPng(1000); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 250 }); expect(result.changed).toBe(true); - expect(Math.max(result.width, result.height)).toBe(500); + expect(Math.max(result.width, result.height)).toBe(250); const decoded = await Jimp.fromBuffer(Buffer.from(result.data)); const { min, max } = grayStats(decoded); @@ -1539,11 +1539,11 @@ describe('compressImageForModel — downscale quality guards', () => { }); it('stays alias-free at a non-integer ratio (fractional pixel coverage)', async () => { - // 2000 → 780 (≈2.56:1). Non-integer ratios are where phase-dependent + // 1000 → 390 (≈2.56:1). Non-integer ratios are where phase-dependent // point sampling degrades worst. Fractional window coverage leaves the // average some mild texture, but nothing may approach black or white. - const png = await checkerboardPng(2000); - const result = await compressImageForModel(png, 'image/png', { maxEdge: 780 }); + const png = await checkerboardPng(1000); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 390 }); expect(result.changed).toBe(true); const decoded = await Jimp.fromBuffer(Buffer.from(result.data)); @@ -1562,8 +1562,8 @@ describe('compressImageForModel — downscale quality guards', () => { // distinguishes resamplers) and pins the library behavior the // mode-less default call relies on — if jimp ever changes either // side, revisit the fitWithinEdge comment. - const image = await Jimp.fromBuffer(Buffer.from(await checkerboardPng(2000))); - image.resize({ w: 500, h: 500, mode: ResizeStrategy.BILINEAR }); + const image = await Jimp.fromBuffer(Buffer.from(await checkerboardPng(1000))); + image.resize({ w: 250, h: 250, mode: ResizeStrategy.BILINEAR }); const { min, max, mean } = grayStats(image); // The correct answer is flat ~127 gray (mean ≈ 127, max-min ≈ 0). // Aliasing shows up as a solid black/white collapse or full-contrast @@ -1577,11 +1577,11 @@ describe('compressImageForModel — downscale quality guards', () => { // blends them into the average tints every transparency edge (halo). // Probe: a fully transparent BRIGHT RED field around an opaque blue // square — after a 4:1 downscale no visible pixel may pick up red. - const size = 1600; + const size = 800; const image = new Jimp({ width: size, height: size, color: 0xff000000 }); // red, alpha 0 const data = image.bitmap.data; - for (let y = 400; y < 1200; y += 1) { - for (let x = 400; x < 1200; x += 1) { + for (let y = 200; y < 600; y += 1) { + for (let x = 200; x < 600; x += 1) { const i = (y * size + x) * 4; data[i] = 0; data[i + 1] = 0; @@ -1591,7 +1591,7 @@ describe('compressImageForModel — downscale quality guards', () => { } const png = new Uint8Array(await image.getBuffer('image/png')); - const result = await compressImageForModel(png, 'image/png', { maxEdge: 400 }); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 200 }); expect(result.changed).toBe(true); expect(result.mimeType).toBe('image/png'); // alpha survives @@ -1610,11 +1610,11 @@ describe('compressImageForModel — downscale quality guards', () => { it('preserves mean brightness through the downscale (no energy drift)', async () => { // A normalized filter keeps the image mean; drift here would indicate // non-normalized weights (or a broken gamma pipeline stage). - const png = await noisePng(900, 900); + const png = await noisePng(400, 400); const input = await Jimp.fromBuffer(Buffer.from(png)); const inputMean = grayStats(input).mean; - const result = await compressImageForModel(png, 'image/png', { maxEdge: 225 }); + const result = await compressImageForModel(png, 'image/png', { maxEdge: 100 }); expect(result.changed).toBe(true); const output = await Jimp.fromBuffer(Buffer.from(result.data)); expect(Math.abs(grayStats(output).mean - inputMean)).toBeLessThan(3); @@ -1624,7 +1624,7 @@ describe('compressImageForModel — downscale quality guards', () => { // Model-bound bytes can re-enter the pipeline (session replay, MCP // round-trips). Once within budget they must pass through untouched // instead of being shaved a little smaller on every pass. - const first = await compressImageForModel(await solidPng(4500, 2250), 'image/png'); + const first = await compressImageForModel(await solidPng(2100, 1050), 'image/png'); expect(first.changed).toBe(true); const second = await compressImageForModel(first.data, first.mimeType); @@ -1662,7 +1662,7 @@ function captureTelemetry(): { client: TelemetryClient; events: CapturedEvent[] describe('compressImageForModel — telemetry', () => { it('reports a compressed image with sizes, formats, and duration', async () => { const { client, events } = captureTelemetry(); - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png', { telemetry: { client, source: 'read_media' }, }); @@ -1677,8 +1677,8 @@ describe('compressImageForModel — telemetry', () => { expect(props['output_mime']).toBe(result.mimeType); expect(props['original_bytes']).toBe(png.length); expect(props['final_bytes']).toBe(result.finalByteLength); - expect(props['original_width']).toBe(4500); - expect(props['original_height']).toBe(2250); + expect(props['original_width']).toBe(2100); + expect(props['original_height']).toBe(1050); expect(props['final_width']).toBe(2000); expect(props['final_height']).toBe(1000); expect(props['exif_transposed']).toBe(false); @@ -1711,7 +1711,7 @@ describe('compressImageForModel — telemetry', () => { expect(bomb.events[0]!.props['outcome']).toBe('passthrough_guard'); const byteCap = captureTelemetry(); - await compressImageForModel(await solidPng(3200, 100), 'image/png', { + await compressImageForModel(await solidPng(2100, 100), 'image/png', { maxDecodeBytes: 64, telemetry: { client: byteCap.client, source: 'mcp_tool_result' }, }); @@ -1764,7 +1764,7 @@ describe('compressImageForModel — telemetry', () => { it('reports the base64 early size-skip as passthrough_guard', async () => { const { client, events } = captureTelemetry(); - const base64 = Buffer.from(await solidPng(3200, 100)).toString('base64'); + const base64 = Buffer.from(await solidPng(2100, 100)).toString('base64'); await compressBase64ForModel(base64, 'image/png', { maxDecodeBytes: 64, telemetry: { client, source: 'prompt_file' }, @@ -1776,7 +1776,7 @@ describe('compressImageForModel — telemetry', () => { it('threads telemetry through compressImageContentParts', async () => { const { client, events } = captureTelemetry(); - const big = await solidPng(3600, 1800); + const big = await solidPng(2100, 1050); const url = `data:image/png;base64,${Buffer.from(big).toString('base64')}`; await compressImageContentParts([{ type: 'image_url', imageUrl: { url } }], { telemetry: { client, source: 'mcp_tool_result' }, @@ -1793,7 +1793,7 @@ describe('compressImageForModel — telemetry', () => { throw new Error('sink down'); }, }; - const png = await solidPng(4500, 2250); + const png = await solidPng(2100, 1050); const result = await compressImageForModel(png, 'image/png', { telemetry: { client: throwing, source: 'read_media' }, }); @@ -1849,9 +1849,9 @@ describe('cropImageForModel — telemetry', () => { const budget = captureTelemetry(); await cropImageForModel( - await noisePng(900, 900), + await noisePng(400, 400), 'image/png', - { x: 0, y: 0, width: 900, height: 900 }, + { x: 0, y: 0, width: 400, height: 400 }, { skipResize: true, byteBudget: 8 * 1024, telemetry: { client: budget.client, source: 'read_media' } }, ); expect(budget.events[0]!.props['error_kind']).toBe('budget'); diff --git a/packages/agent-core/test/tools/read-media.test.ts b/packages/agent-core/test/tools/read-media.test.ts index 78b61c819b..ea7cc8907b 100644 --- a/packages/agent-core/test/tools/read-media.test.ts +++ b/packages/agent-core/test/tools/read-media.test.ts @@ -692,9 +692,9 @@ describe('ReadMediaFileTool', () => { it('downsamples an oversized image but reports original dimensions', async () => { const big = Buffer.from( - await new Jimp({ width: 3600, height: 3600, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 2200, height: 2200, color: 0x3366ccff }).getBuffer('image/png'), ); - expect(sniffImageDimensions(big)).toEqual({ width: 3600, height: 3600 }); + expect(sniffImageDimensions(big)).toEqual({ width: 2200, height: 2200 }); const tool = makeReadMediaTool({ stat: vi.fn().mockResolvedValue({ ...DEFAULT_STAT, stSize: big.length }), @@ -719,18 +719,18 @@ describe('ReadMediaFileTool', () => { // The note keeps the ORIGINAL size so coordinate mapping holds. const systemText = noteText(result); - expect(systemText).toContain('3600x3600'); + expect(systemText).toContain('2200x2200'); expect(systemText).toContain(`${String(big.length)} bytes`); }); it('reports an EXIF-rotated original in the decoded coordinate space', async () => { - // Orientation 6 (rotate 90° CW): the header says 3600x1800, but jimp - // decodes to 1800x3600 — the space the sent image and any region + // Orientation 6 (rotate 90° CW): the header says 2200x1100, but jimp + // decodes to 1100x2200 — the space the sent image and any region // readback live in. The note's original size must match that space, // not the pre-rotation header sniff. const portrait = withExifOrientation( new Uint8Array( - await new Jimp({ width: 3600, height: 1800, color: 0x3366ccff }).getBuffer('image/jpeg', { + await new Jimp({ width: 2200, height: 1100, color: 0x3366ccff }).getBuffer('image/jpeg', { quality: 90, }), ), @@ -749,7 +749,7 @@ describe('ReadMediaFileTool', () => { }); const systemText = noteText(result); - expect(systemText).toContain('Original dimensions: 1800x3600'); + expect(systemText).toContain('Original dimensions: 1100x2200'); expect(systemText).toMatch(/downsampled to 1000x2000/); }); @@ -814,7 +814,7 @@ describe('ReadMediaFileTool', () => { track: (event, props) => events.push({ event, props: props ?? {} }), }; const big = Buffer.from( - await new Jimp({ width: 3600, height: 1800, color: 0x3366ccff }).getBuffer('image/png'), + await new Jimp({ width: 2200, height: 1100, color: 0x3366ccff }).getBuffer('image/png'), ); const tool = makeReadMediaTool({ stat: vi.fn().mockResolvedValue({ ...DEFAULT_STAT, stSize: big.length }), @@ -885,7 +885,7 @@ describe('ReadMediaFileTool', () => { }); it('announces a downsampled delivery and the region readback in the block', async () => { - const big = await bigPng(3600, 3600); + const big = await bigPng(2200, 2200); const result = await executeTool(toolFor(big), { turnId: 't1', toolCallId: 'c_note', @@ -894,7 +894,7 @@ describe('ReadMediaFileTool', () => { }); const systemText = noteText(result); - expect(systemText).toContain('3600x3600'); + expect(systemText).toContain('2200x2200'); // Wording must not depend on serialization order: some providers keep // the note inline after the media, others flatten tool text and // re-attach the image after it — so no "above"/"below". @@ -923,7 +923,7 @@ describe('ReadMediaFileTool', () => { }); it('reads a region crop at native resolution', async () => { - const big = await bigPng(2600, 2600); + const big = await bigPng(600, 600); const result = await executeTool(toolFor(big), { turnId: 't1', toolCallId: 'c_crop', @@ -938,14 +938,14 @@ describe('ReadMediaFileTool', () => { expect(sentDims).toEqual({ width: 400, height: 300 }); const systemText = noteText(result); - expect(systemText).toContain('2600x2600'); + expect(systemText).toContain('600x600'); expect(systemText).toMatch(/region \(x=100, y=50, width=400, height=300\)/); expect(systemText).toMatch(/native resolution/); expect(systemText).toContain('offset'); }); it('rejects a region outside the image with the original size in the error', async () => { - const big = await bigPng(2600, 2600); + const big = await bigPng(600, 600); const result = await executeTool(toolFor(big), { turnId: 't1', toolCallId: 'c_crop_oob', @@ -953,11 +953,11 @@ describe('ReadMediaFileTool', () => { signal, }); expect(result.isError).toBe(true); - expect(result.output).toContain('2600x2600'); + expect(result.output).toContain('600x600'); }); it('serves full_resolution when the bytes fit the per-image budget', async () => { - const big = await bigPng(3900, 1950); // over the edge cap, tiny in bytes + const big = await bigPng(2100, 1050); // over the edge cap, tiny in bytes const result = await executeTool(toolFor(big), { turnId: 't1', toolCallId: 'c_fullres', @@ -1187,7 +1187,7 @@ describe('ReadMediaFileTool', () => { async () => { const budget = 64 * 1024; const limits = new ImageLimits(process.env, { readByteBudget: budget }); - const data = await noisePng(1200, 1200); + const data = await noisePng(400, 400); expect(data.length).toBeGreaterThan(budget); const result = await executeTool(toolFor(data, limits), { @@ -1206,7 +1206,7 @@ describe('ReadMediaFileTool', () => { it('full_resolution ignores the read budget (per-image provider limit applies)', async () => { const limits = new ImageLimits(process.env, { readByteBudget: 64 * 1024 }); - const data = await noisePng(600, 600); + const data = await noisePng(300, 300); expect(data.length).toBeGreaterThan(64 * 1024); const result = await executeTool(toolFor(data, limits), { @@ -1226,7 +1226,7 @@ describe('ReadMediaFileTool', () => { 'region reads ignore the read budget so detail readback stays full-fidelity', async () => { const limits = new ImageLimits(process.env, { readByteBudget: 16 * 1024 }); - const data = await noisePng(800, 800); + const data = await noisePng(500, 500); const result = await executeTool(toolFor(data, limits), { turnId: 't1', @@ -1246,7 +1246,7 @@ describe('ReadMediaFileTool', () => { it( 'two tools honor their own limits independently (no shared process state)', async () => { - const data = await noisePng(1200, 1200); + const data = await noisePng(512, 512); const tight = toolFor(data, new ImageLimits(process.env, { readByteBudget: 48 * 1024 })); const roomy = toolFor(data, new ImageLimits(process.env, { maxEdgePx: 1200 })); diff --git a/packages/minidb/test/compaction.test.ts b/packages/minidb/test/compaction.test.ts index 29c92a94df..0808ceadb1 100644 --- a/packages/minidb/test/compaction.test.ts +++ b/packages/minidb/test/compaction.test.ts @@ -141,7 +141,7 @@ test('concurrent SET/UPDATE/DEL during compaction survive recovery', async () => } finally { await fs.rm(dir, { recursive: true, force: true }); } -}); +}, 15_000); test('compaction with no concurrent writes produces an empty WAL tail', async () => { // When nothing is written during compaction, the post-fence WAL tail is empty diff --git a/packages/minidb/test/e2e/compaction-race.test.ts b/packages/minidb/test/e2e/compaction-race.test.ts index 58cc7e06fb..ff964e9677 100644 --- a/packages/minidb/test/e2e/compaction-race.test.ts +++ b/packages/minidb/test/e2e/compaction-race.test.ts @@ -86,7 +86,12 @@ test('compaction-race: snapshot phase does not block writes', async () => { compactThresholdBytes: 1 << 30, // drive compaction manually }); try { - const N = 150_000; + // writeSnapshot yields to the event loop every 2000 entries (snapshot.ts + // `yieldEvery`, plus an async writev per ~1 MiB batch), so N just has to + // clear a few yield quanta for the write to land in a yield gap. 10_000 + // entries = 5 explicit yields — ~5x headroom over the minimum for + // "multiple yields", enough for slow CI machines without writing ~75 MB. + const N = 10_000; for (let i = 0; i < N; i++) await db.set('k' + i, { i, pad: 'x'.repeat(500) }); const cp = db.compact(); @@ -112,7 +117,7 @@ test('compaction-race: snapshot phase does not block writes', async () => { await db.close().catch(() => {}); await rmrf(dir); } -}, 60_000); +}, 15_000); test('compaction-race: heavy writes during compaction grow a WAL tail that survives recovery', async () => { // Sustained writes during compaction force the pre-copy loop to drain a real From f05832bb9cc4a623a8164db7458f84c773fa74c4 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 13 Jul 2026 16:53:24 +0800 Subject: [PATCH 2/4] test: keep region-read fixtures over the downsample cap The 600x600 fixtures could no longer distinguish cropping from the original coordinate space (the documented follow-up to a downsampled default read) from cropping the downsampled delivery: below the 2000px edge cap nothing is ever downsampled, so a regression in that path would pass silently. Same for the out-of-bounds error asserting the original size. Use 2100x2100 (just over the cap, still cheap solid PNGs) and note why the size must not shrink again. --- .../test/agent/media/tools/read-media.test.ts | 13 +++++++++---- packages/agent-core/test/tools/read-media.test.ts | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts index 3c7c4ad7fb..fe6968d82b 100644 --- a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts +++ b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts @@ -357,7 +357,10 @@ describe('ReadMediaFileTool', () => { it('reads image regions at native resolution', async () => { const big = Buffer.from( - await new Jimp({ width: 600, height: 600, color: 0x3366ccff }).getBuffer('image/png'), + // Over the 2000px edge cap on purpose: region reads must crop from the + // original coordinate space, which a sub-cap fixture cannot distinguish + // from cropping the downsampled delivery. + await new Jimp({ width: 2100, height: 2100, color: 0x3366ccff }).getBuffer('image/png'), ); const result = await execute(makeTool({ '/workspace/big.png': { data: big } }), { path: '/workspace/big.png', @@ -372,7 +375,7 @@ describe('ReadMediaFileTool', () => { height: 300, }); const systemText = noteText(result); - expect(systemText).toContain('600x600'); + expect(systemText).toContain('2100x2100'); expect(systemText).toMatch(/region \(x=100, y=50, width=400, height=300\)/); expect(systemText).toMatch(/native resolution/); expect(systemText).toContain('offset'); @@ -380,14 +383,16 @@ describe('ReadMediaFileTool', () => { it('rejects a region outside the image with the original size in the error', async () => { const big = Buffer.from( - await new Jimp({ width: 600, height: 600, color: 0x3366ccff }).getBuffer('image/png'), + // Over the edge cap so "original size" is distinguishable from any + // downsampled delivery size. + await new Jimp({ width: 2100, height: 2100, color: 0x3366ccff }).getBuffer('image/png'), ); const result = await execute(makeTool({ '/workspace/big.png': { data: big } }), { path: '/workspace/big.png', region: { x: 5000, y: 0, width: 100, height: 100 }, }); expect(result.isError).toBe(true); - expect(result.output).toContain('600x600'); + expect(result.output).toContain('2100x2100'); }); it('serves full_resolution when the bytes fit the per-image budget', async () => { diff --git a/packages/agent-core/test/tools/read-media.test.ts b/packages/agent-core/test/tools/read-media.test.ts index ea7cc8907b..b95dd04e37 100644 --- a/packages/agent-core/test/tools/read-media.test.ts +++ b/packages/agent-core/test/tools/read-media.test.ts @@ -923,7 +923,10 @@ describe('ReadMediaFileTool', () => { }); it('reads a region crop at native resolution', async () => { - const big = await bigPng(600, 600); + // Over the 2000px edge cap on purpose: region reads must crop from the + // original coordinate space, which a sub-cap fixture cannot distinguish + // from cropping the downsampled delivery. + const big = await bigPng(2100, 2100); const result = await executeTool(toolFor(big), { turnId: 't1', toolCallId: 'c_crop', @@ -938,14 +941,16 @@ describe('ReadMediaFileTool', () => { expect(sentDims).toEqual({ width: 400, height: 300 }); const systemText = noteText(result); - expect(systemText).toContain('600x600'); + expect(systemText).toContain('2100x2100'); expect(systemText).toMatch(/region \(x=100, y=50, width=400, height=300\)/); expect(systemText).toMatch(/native resolution/); expect(systemText).toContain('offset'); }); it('rejects a region outside the image with the original size in the error', async () => { - const big = await bigPng(600, 600); + // Over the edge cap so "original size" is distinguishable from any + // downsampled delivery size. + const big = await bigPng(2100, 2100); const result = await executeTool(toolFor(big), { turnId: 't1', toolCallId: 'c_crop_oob', @@ -953,7 +958,7 @@ describe('ReadMediaFileTool', () => { signal, }); expect(result.isError).toBe(true); - expect(result.output).toContain('600x600'); + expect(result.output).toContain('2100x2100'); }); it('serves full_resolution when the bytes fit the per-image budget', async () => { From bc3fb79f906e780f81a00c0d8879c4f3ba1c06fb Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 13 Jul 2026 17:15:11 +0800 Subject: [PATCH 3/4] test: trim oversized minidb cases, drop real-timer waits, deflake fs-watch AC #2 - minidb: fuzz-model 10 seeds x 500 steps -> 6 x 250; compaction-race heavy-writes 20k/5k -> 10k/2k (still spans five writeSnapshot yields and a >64KiB WAL tail); crash-recovery runs 8/4 -> 5/3 (cost is subprocess spawn, not rounds); compaction concurrent 10k/2k -> 5k/1k - replace real-second waits with injected short timeouts or fake timers: hooks runner timeout 1s -> 0.05s (both packages), lifecycle-hooks printWaitCeilingS 1s -> 0.05s, task-tools block-timeout driven by fake timers, mcp connection-manager slow-startup child exits on stdin end, kap-server 429 test injects a token-only auth service (12x bcrypt ~250ms was the real cost, not the rate limiter) - fs-watch AC #2: the burst test required chokidar to deliver >500 events inside one 200ms window, which trickles under CI load. Add an fsWatcherOptions seam to ServerStartOptions (default behavior unchanged) and run the test with a 500ms window / 100-event threshold, which still proves truncation but tolerates 10x slower delivery --- .../test/agent/externalHooks/runner.test.ts | 2 +- packages/agent-core/test/hooks/runner.test.ts | 2 +- .../test/mcp/connection-manager.test.ts | 17 ++++++--- .../test/session/lifecycle-hooks.test.ts | 4 +- .../test/tools/background/task-tools.test.ts | 7 +++- .../kap-server/test/hostExposure.e2e.test.ts | 8 +++- packages/minidb/test/compaction.test.ts | 6 ++- .../minidb/test/e2e/compaction-race.test.ts | 8 +++- .../minidb/test/e2e/crash-recovery.test.ts | 6 ++- packages/minidb/test/e2e/fuzz-model.test.ts | 6 ++- packages/server/src/start.ts | 11 +++++- packages/server/test/fs-watch.e2e.test.ts | 38 ++++++++++++++----- 12 files changed, 85 insertions(+), 30 deletions(-) diff --git a/packages/agent-core-v2/test/agent/externalHooks/runner.test.ts b/packages/agent-core-v2/test/agent/externalHooks/runner.test.ts index 0b569240f7..d1b09e8959 100644 --- a/packages/agent-core-v2/test/agent/externalHooks/runner.test.ts +++ b/packages/agent-core-v2/test/agent/externalHooks/runner.test.ts @@ -85,7 +85,7 @@ describe('runHook process runner', () => { hostProcess, nodeCommand('setTimeout(() => {}, 10000);'), { tool_name: 'Bash' }, - { timeout: 1 }, + { timeout: 0.05 }, ); expect(result.action).toBe('allow'); diff --git a/packages/agent-core/test/hooks/runner.test.ts b/packages/agent-core/test/hooks/runner.test.ts index ff3c33b97e..becd49baba 100644 --- a/packages/agent-core/test/hooks/runner.test.ts +++ b/packages/agent-core/test/hooks/runner.test.ts @@ -78,7 +78,7 @@ describe('runHook process runner', () => { it('returns allow with timedOut=true when the command exceeds the timeout', async () => { const runHook = await importRunHook(); - const result = await runHook('sleep 10', { tool_name: 'Shell' }, { timeout: 1 }); + const result = await runHook('sleep 10', { tool_name: 'Shell' }, { timeout: 0.05 }); expect(result.action).toBe('allow'); expect(result.timedOut).toBe(true); }); diff --git a/packages/agent-core/test/mcp/connection-manager.test.ts b/packages/agent-core/test/mcp/connection-manager.test.ts index 662d1cc882..3b7a9112c7 100644 --- a/packages/agent-core/test/mcp/connection-manager.test.ts +++ b/packages/agent-core/test/mcp/connection-manager.test.ts @@ -34,7 +34,6 @@ import { createScriptedGenerate } from '../agent/harness'; const here = import.meta.dirname; const stdioFixture = join(here, 'fixtures', 'mock-stdio-server.mjs'); const cwdStdioFixture = join(here, 'fixtures', 'cwd-stdio-server.mjs'); -const slowStdioFixture = join(here, 'fixtures', 'slow-stdio-server.mjs'); const crashAfterConnectFixture = join(here, 'fixtures', 'crash-after-connect-stdio-server.mjs'); const stderrThenExitFixture = join(here, 'fixtures', 'stderr-then-exit-stdio-server.mjs'); const MOCK_PROVIDER: ProviderConfig = { @@ -218,7 +217,7 @@ describe('McpConnectionManager', () => { cm.onStatusChange((entry) => { seen.push({ name: entry.name, status: entry.status }); }); - const delayedMockServer = `setTimeout(() => import(${JSON.stringify(pathToFileURL(stdioFixture).href)}), 250)`; + const delayedMockServer = `setTimeout(() => import(${JSON.stringify(pathToFileURL(stdioFixture).href)}), 160)`; const connect = cm.connectAll({ slow: { @@ -230,7 +229,9 @@ describe('McpConnectionManager', () => { }); try { - await sleep(50); + // Let the first attempt get in-flight, then supersede it: reconnect + // must land before the delayed child finishes its 160ms startup. + await sleep(40); await cm.reconnect('slow'); await connect; @@ -772,6 +773,12 @@ describe('Session MCP startup', () => { it('does not block main agent creation on slow MCP startup', async () => { const tmp = await mkdtemp(join(tmpdir(), 'kimi-session-mcp-startup-')); + // The child never completes the MCP handshake — it idles, keeping startup + // in-flight — but exits the instant the parent closes stdin, so + // session.close() does not wait out the SDK transport's close grace. The + // 800ms idle keeps startup pending long enough that a createMain blocked + // on MCP would lose the 500ms race below. + const idleServer = `process.stdin.on('end', () => process.exit(0)); process.stdin.resume(); setTimeout(() => {}, 800)`; const session = new Session({ id: 'test-mcp-slow', kaos: testKaos.withCwd(tmp), @@ -782,7 +789,7 @@ describe('Session MCP startup', () => { slow: { transport: 'stdio', command: process.execPath, - args: [slowStdioFixture], + args: ['-e', idleServer], startupTimeoutMs: 2_000, }, }, @@ -793,7 +800,7 @@ describe('Session MCP startup', () => { try { const result = await Promise.race([ create.then(() => 'resolved' as const), - sleep(1_000).then(() => 'blocked' as const), + sleep(500).then(() => 'blocked' as const), ]); expect(result).toBe('resolved'); } finally { diff --git a/packages/agent-core/test/session/lifecycle-hooks.test.ts b/packages/agent-core/test/session/lifecycle-hooks.test.ts index cc45993382..71e3847fb2 100644 --- a/packages/agent-core/test/session/lifecycle-hooks.test.ts +++ b/packages/agent-core/test/session/lifecycle-hooks.test.ts @@ -281,7 +281,9 @@ describe('Session lifecycle hooks', () => { homedir: sessionDir, rpc: createSessionRpc(), skills: { explicitDirs: [join(workDir, 'missing-skills')] }, - background: { keepAliveOnExit: true, printWaitCeilingS: 1 }, + // Sub-second ceiling: the deadline path is identical, but the test no + // longer waits a real second for the drain loop to time out. + background: { keepAliveOnExit: true, printWaitCeilingS: 0.05 }, }); const agent = await session.createMain(); const { proc } = pendingProcess(); diff --git a/packages/agent-core/test/tools/background/task-tools.test.ts b/packages/agent-core/test/tools/background/task-tools.test.ts index 433c6bf733..62f32c7dc5 100644 --- a/packages/agent-core/test/tools/background/task-tools.test.ts +++ b/packages/agent-core/test/tools/background/task-tools.test.ts @@ -328,10 +328,15 @@ describe('TaskOutputTool', () => { }); it('returns timeout for block=true when a running task does not finish', async () => { + // Fake timers drive the real 1s block timeout (taskOutput passes + // timeout: 1) so the test does not wait a real second. + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); const { manager } = createBackgroundManager(); const taskId = registerProcess(manager, pendingProcess(), 'sleep 60', 'blocking task'); - const content = await taskOutput(manager, taskId, true); + const contentPromise = taskOutput(manager, taskId, true); + await vi.advanceTimersByTimeAsync(1_000); + const content = await contentPromise; expect(content).toContain('retrieval_status: timeout'); expect(content).toContain('status: running'); diff --git a/packages/kap-server/test/hostExposure.e2e.test.ts b/packages/kap-server/test/hostExposure.e2e.test.ts index eff92a2a1e..7e62ebf7ea 100644 --- a/packages/kap-server/test/hostExposure.e2e.test.ts +++ b/packages/kap-server/test/hostExposure.e2e.test.ts @@ -140,14 +140,20 @@ describe('real password path (verifyPassword)', () => { describe('auth-failure rate limit on a real bind', () => { it('returns 429 on the 11th bad token', async () => { - process.env['KIMI_CODE_PASSWORD'] = 'test-pw'; const home = await tmpHome(); + // Inject a fast token-only auth service: this test exercises the rate + // limiter, not bcrypt — 12 sequential cost-12 compares would take seconds. const server = await startServer({ host: '0.0.0.0', port: 0, homeDir: home, logLevel: 'silent', insecureNoTls: true, + authTokenService: { + _serviceBrand: undefined, + getToken: () => 'persistent-token', + isValid: async (candidate) => candidate === 'persistent-token', + }, }); running.push(server); const url = `http://127.0.0.1:${server.port}/api/v1/sessions`; diff --git a/packages/minidb/test/compaction.test.ts b/packages/minidb/test/compaction.test.ts index 0808ceadb1..01d46d0d87 100644 --- a/packages/minidb/test/compaction.test.ts +++ b/packages/minidb/test/compaction.test.ts @@ -107,14 +107,16 @@ test('concurrent SET/UPDATE/DEL during compaction survive recovery', async () => const dir = await tmpDir(); try { let db = await MiniDb.open({ dir, valueCodec: 'string', fsyncPolicy: 'no' }); - const N = 10000; + // 5000 keys span 2 writeSnapshot yield windows (yieldEvery=2000, src/snapshot.ts), + // so the ops below genuinely race an in-progress snapshot. + const N = 5000; for (let i = 0; i < N; i++) await db.set('k' + i, 'v' + i); // Even keys are updated, keys == 1 (mod 4) are deleted, keys == 3 (mod 4) // are left untouched, and a batch of new keys is added — all racing the // in-progress snapshot. let deleted = 0; - const M = 2000; + const M = 1000; const compactP = db.compact(); const ops: Promise[] = []; for (let i = 0; i < N; i++) { diff --git a/packages/minidb/test/e2e/compaction-race.test.ts b/packages/minidb/test/e2e/compaction-race.test.ts index ff964e9677..f6ee40f050 100644 --- a/packages/minidb/test/e2e/compaction-race.test.ts +++ b/packages/minidb/test/e2e/compaction-race.test.ts @@ -130,11 +130,15 @@ test('compaction-race: heavy writes during compaction grow a WAL tail that survi compactThresholdBytes: 1 << 30, }); try { - const N = 20_000; + // 10k keys span 5 writeSnapshot yield windows (yieldEvery=2000, src/snapshot.ts), + // so compaction is still in progress while the writes below land. + const N = 10_000; for (let i = 0; i < N; i++) await db.set('k' + i, { i }); const cp = db.compact(); - const M = 5000; + // ~55 B/frame × 2000 ≈ 110 KB post-fence tail > SMALL_DELTA (64 KiB, + // src/compaction.ts), so the pre-copy loop still drains a real WAL tail. + const M = 2000; const writes: Promise[] = []; for (let i = 0; i < M; i++) writes.push(db.set('k' + i, { i, bumped: true })); await Promise.all(writes); diff --git a/packages/minidb/test/e2e/crash-recovery.test.ts b/packages/minidb/test/e2e/crash-recovery.test.ts index bdd1335b94..8ff41a66a9 100644 --- a/packages/minidb/test/e2e/crash-recovery.test.ts +++ b/packages/minidb/test/e2e/crash-recovery.test.ts @@ -54,7 +54,9 @@ async function verifyContiguous(dir, label) { } test('crash-recovery: kill mid-write, recovery yields a contiguous correct prefix', async () => { - const runs = 8; + // Each run spawns a child process (the dominant cost); 5 runs with random kill + // times still sample the crash window well. + const runs = 5; for (let r = 0; r < runs; r++) { const dir = await tmpDir(); try { @@ -69,7 +71,7 @@ test('crash-recovery: kill mid-write, recovery yields a contiguous correct prefi }); test('crash-recovery: kill during compaction, still consistent', async () => { - const runs = 4; + const runs = 3; for (let r = 0; r < runs; r++) { const dir = await tmpDir(); try { diff --git a/packages/minidb/test/e2e/fuzz-model.test.ts b/packages/minidb/test/e2e/fuzz-model.test.ts index cc4e72a15b..81c116205e 100644 --- a/packages/minidb/test/e2e/fuzz-model.test.ts +++ b/packages/minidb/test/e2e/fuzz-model.test.ts @@ -59,8 +59,10 @@ async function runSeed(seed, steps) { } test('fuzz-model: random op sequences match a reference model (many seeds)', async () => { - const seeds = [1, 2, 3, 42, 12345, 99999, 0xdeadbeef, 0xc0ffee, 777, 20240625]; + // Mix of small and large seeds. 6 seeds × 250 steps ≈ 1.5k ops total, still + // hitting every op branch (including reopen + full compare) many times per seed. + const seeds = [1, 2, 3, 99999, 0xdeadbeef, 20240625]; for (const seed of seeds) { - await expect(runSeed(seed, 500)).resolves.toBeUndefined(); + await expect(runSeed(seed, 250)).resolves.toBeUndefined(); } }, 60_000); diff --git a/packages/server/src/start.ts b/packages/server/src/start.ts index f3cc94d90f..9e664cc2a5 100644 --- a/packages/server/src/start.ts +++ b/packages/server/src/start.ts @@ -1,4 +1,4 @@ -import { InstantiationService, resolveConfigPath, resolveKimiHome, setUnexpectedErrorHandler, IApprovalService, IAuthSummaryService, IEnvironmentService, IEventService, ICoreProcessService, IModelCatalogService, IMcpService, IMessageService, IOAuthService, IFileStore, IFsGitService, IFsSearchService, IFsService, IFsWatcher, ILogService, IPromptService, IQuestionService, ISessionService, ISkillService, ITaskService, ITerminalService, IToolService, IWorkspaceFsService, IWorkspaceRegistry, FsPathEscapesError, FsWatchLimitError, FsWatcherService, SessionNotFoundError, SessionStore, createConnectionLookup, resolveSafePath, type ServiceIdentifier, type CoreProcessServiceOptions } from '@moonshot-ai/agent-core'; +import { InstantiationService, resolveConfigPath, resolveKimiHome, setUnexpectedErrorHandler, IApprovalService, IAuthSummaryService, IEnvironmentService, IEventService, ICoreProcessService, IModelCatalogService, IMcpService, IMessageService, IOAuthService, IFileStore, IFsGitService, IFsSearchService, IFsService, IFsWatcher, ILogService, IPromptService, IQuestionService, ISessionService, ISkillService, ITaskService, ITerminalService, IToolService, IWorkspaceFsService, IWorkspaceRegistry, FsPathEscapesError, FsWatchLimitError, FsWatcherService, SessionNotFoundError, SessionStore, createConnectionLookup, resolveSafePath, type ServiceIdentifier, type CoreProcessServiceOptions, type FsWatcherServiceOptions } from '@moonshot-ai/agent-core'; import { ErrorCode, createAsyncApiDocument } from '@moonshot-ai/protocol'; import Fastify from 'fastify'; import { promises as fspPromises } from 'node:fs'; @@ -61,6 +61,13 @@ export interface ServerStartOptions { wsGatewayOptions?: WSGatewayOptions; + /** + * Overrides for the fs watch aggregator (debounce window, per-window + * change cap, per-connection path cap). Unset keeps the production + * defaults; intended for tests that need deterministic overflow. + */ + fsWatcherOptions?: FsWatcherServiceOptions; + debugEndpoints?: boolean; /** @@ -504,7 +511,7 @@ export async function startServer(opts: ServerStartOptions): Promise registry.get(id)), - {}, + opts.fsWatcherOptions ?? {}, ); services.set(IFsWatcher, fsWatcher); a.get(IFsWatcher); diff --git a/packages/server/test/fs-watch.e2e.test.ts b/packages/server/test/fs-watch.e2e.test.ts index 4eedf833dd..de3794bc71 100644 --- a/packages/server/test/fs-watch.e2e.test.ts +++ b/packages/server/test/fs-watch.e2e.test.ts @@ -3,7 +3,7 @@ * * AC coverage (ROADMAP §Chain 14): * 1. subscribe `/src` → create file → receive `event.fs.changed` - * 2. burst > 500 changes / 200ms → truncated event + * 2. burst beyond the per-window change cap → truncated event * 3. two clients, two paths → no cross-delivery * 4. > 100 paths per connection → `42902 fs.watch_limit_exceeded` * @@ -42,6 +42,7 @@ import { IRestGateway, startServer, type RunningServer, + type ServerStartOptions, } from '../src'; import { fixedTokenAuth } from './helpers/serverHarness'; import { rawDataToString } from '../src/ws/rawData'; @@ -73,7 +74,9 @@ afterEach(async () => { rmSync(bridgeHome, { recursive: true, force: true }); }); -async function bootDaemon(): Promise { +async function bootDaemon( + fsWatcherOptions?: ServerStartOptions['fsWatcherOptions'], +): Promise { server = await startServer({ serviceOverrides: [fixedTokenAuth()], host: '127.0.0.1', @@ -82,6 +85,7 @@ async function bootDaemon(): Promise { logger: pino({ level: 'silent' }), coreProcessOptions: { homeDir: bridgeHome }, wsGatewayOptions: { pingIntervalMs: 5_000, pongTimeoutMs: 5_000 }, + fsWatcherOptions, }); return server; } @@ -229,6 +233,17 @@ const sleep = (ms: number): Promise => /** Time we give chokidar to register newly-watched paths before we mutate. */ const WATCH_SETTLE_MS = 150; +/** + * AC #2 aggregation settings: a lower per-window cap makes overflow + * deterministic even when full-suite CPU load spreads chokidar delivery + * across windows. With 600 burst events and a 500ms window, some window + * exceeds 100 events as long as delivery completes within ~2.5s of the first + * event (pigeonhole); on a normal machine the burst still lands in a single + * window that flushes ~500ms after the first event, so the test stays fast. + */ +const BURST_WINDOW_MS = 500; +const BURST_MAX_CHANGES_PER_WINDOW = 100; + describe('WS fs watch (W12 / Chain 14)', () => { it('AC #1: subscribe /src → create file → receive event.fs.changed', async () => { const r = await bootDaemon(); @@ -270,10 +285,13 @@ describe('WS fs watch (W12 / Chain 14)', () => { }); // Windows ReadDirectoryChangesW coalesces/spreads the burst, so no single - // 200ms window reliably crosses the 500-event overflow threshold. The - // truncation logic itself is covered by this same test on POSIX. - it.skipIf(process.platform === 'win32')('AC #2: burst > 500 changes inside 200ms window → truncated:true', { timeout: 5000 }, async () => { - const r = await bootDaemon(); + // aggregation window reliably crosses the per-window overflow threshold. + // The truncation logic itself is covered by this same test on POSIX. + it.skipIf(process.platform === 'win32')('AC #2: burst beyond per-window capacity → truncated:true', { timeout: 15000 }, async () => { + const r = await bootDaemon({ + debounceMs: BURST_WINDOW_MS, + maxChangesPerWindow: BURST_MAX_CHANGES_PER_WINDOW, + }); const sid = await createSession(r); const conn = await openConn(wsUrl(r.address)); await helloAndSubscribe(conn, 'A', sid); @@ -290,8 +308,8 @@ describe('WS fs watch (W12 / Chain 14)', () => { await sleep(WATCH_SETTLE_MS); - // Slam 600 files into a fresh dir; chokidar emits >500 add events well - // inside one 200ms window. + // Slam 600 files into a fresh dir; the burst far exceeds the injected + // per-window capacity. const burstDir = join(workspace, 'burst'); mkdirSync(burstDir, { recursive: true }); for (let i = 0; i < 600; i++) { @@ -299,7 +317,7 @@ describe('WS fs watch (W12 / Chain 14)', () => { } // Drain frames until we see truncated:true OR run out of time. - const deadline = Date.now() + (process.platform === 'win32' ? 8000 : 4000); + const deadline = Date.now() + 4000; let sawTruncated = false; while (Date.now() < deadline) { const remaining = deadline - Date.now(); @@ -312,7 +330,7 @@ describe('WS fs watch (W12 / Chain 14)', () => { if (frame.type !== 'event.fs.changed') continue; const payload = frame.payload as { truncated?: boolean; count?: number }; if (payload.truncated === true) { - expect(payload.count).toBeGreaterThan(500); + expect(payload.count).toBeGreaterThan(BURST_MAX_CHANGES_PER_WINDOW); sawTruncated = true; break; } From a77a8b4c346b0f709841e5c3cd8c280d8bb024ef Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 13 Jul 2026 17:35:40 +0800 Subject: [PATCH 4/4] test: run kap-server suites in parallel and harden shutdown dispatch fileParallelism: false serialized all 52 kap-server test files (~82s wall). Nothing actually requires it: servers bind port 0, homes and locks live in mkdtemp dirs. Two real blockers surfaced and are fixed: - v2 SessionFsWatchService hardcoded the 200ms/500 burst-truncation window, so the fs-watch e2e flaked whenever chokidar trickled events under CPU contention (same class of flake fixed earlier for the v1 server). Make the window env-overridable as a test seam (defaults unchanged when unset) and run the burst test at 500ms/100, which still proves truncation at 10x lower delivery rates. - dispatchSessionEvent could reject with 'InstantiationService has been disposed' when an event raced session teardown; the fire-and-forget caller made it an unhandled rejection (present in serial mode too). Extend the existing disposed-scope guard from the attach* block to the whole ensureState call. Parallel mode verified 5/5 runs green, 21.7-30.2s wall vs 82.3s serial. --- .../src/session/sessionFs/fsWatchService.ts | 30 +++++++++++++++---- .../ws/v1/sessionEventBroadcaster.ts | 13 +++++++- packages/kap-server/test/fs-watch.e2e.test.ts | 12 ++++++-- packages/kap-server/vitest.config.ts | 1 - 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/agent-core-v2/src/session/sessionFs/fsWatchService.ts b/packages/agent-core-v2/src/session/sessionFs/fsWatchService.ts index a360cafd8b..f3265817e6 100644 --- a/packages/agent-core-v2/src/session/sessionFs/fsWatchService.ts +++ b/packages/agent-core-v2/src/session/sessionFs/fsWatchService.ts @@ -30,8 +30,16 @@ import type { FsChangeEntry, FsChangeEvent } from '@moonshot-ai/protocol'; import { ISessionFsWatchService } from './fsWatch'; -const DEBOUNCE_MS = 200; -const MAX_CHANGES_PER_WINDOW = 500; +const DEFAULT_DEBOUNCE_MS = 200; +const DEFAULT_MAX_CHANGES_PER_WINDOW = 500; + +/** Positive-int env read for the test-only window overrides below. */ +function readPositiveIntEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (raw === undefined || raw === '') return fallback; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : fallback; +} export class SessionFsWatchService extends Disposable implements ISessionFsWatchService { declare readonly _serviceBrand: undefined; @@ -48,6 +56,18 @@ export class SessionFsWatchService extends Disposable implements ISessionFsWatch private rawCount = 0; private truncated = false; + // Env-overridable for tests: the burst-truncation e2e cannot rely on + // chokidar delivering >500 events inside one 200ms window under CPU + // contention. Production leaves both unset and gets the defaults. + private readonly debounceMs = readPositiveIntEnv( + 'KIMI_CODE_FS_WATCH_DEBOUNCE_MS', + DEFAULT_DEBOUNCE_MS, + ); + private readonly maxChangesPerWindow = readPositiveIntEnv( + 'KIMI_CODE_FS_WATCH_MAX_CHANGES_PER_WINDOW', + DEFAULT_MAX_CHANGES_PER_WINDOW, + ); + /** Always present; starts with `.git/` and is augmented with `.gitignore` once loaded. */ private readonly matcher: Ignore = ignore().add('.git/'); private gitignoreLoaded = false; @@ -116,12 +136,12 @@ export class SessionFsWatchService extends Disposable implements ISessionFsWatch this.pending.push({ path: rel, change: e.action, kind: e.kind }); this.rawCount += 1; - if (this.pending.length > MAX_CHANGES_PER_WINDOW) { + if (this.pending.length > this.maxChangesPerWindow) { this.truncated = true; this.pending = []; } if (this.debounceTimer === undefined) { - const timer = setTimeout(() => this.flush(), DEBOUNCE_MS); + const timer = setTimeout(() => this.flush(), this.debounceMs); timer.unref?.(); this.debounceTimer = timer; } @@ -139,7 +159,7 @@ export class SessionFsWatchService extends Disposable implements ISessionFsWatch const event: FsChangeEvent = { changes, - coalesced_window_ms: DEBOUNCE_MS, + coalesced_window_ms: this.debounceMs, ...(truncated ? { truncated: true, count } : {}), }; this.emitter.fire(event); diff --git a/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts b/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts index f011c8df00..6025c4d1f2 100644 --- a/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts +++ b/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts @@ -416,7 +416,18 @@ export class SessionEventBroadcaster { * (e.g. `session.meta.updated`); `isGlobalEvent` keeps the fan-out global. */ private async dispatchSessionEvent(sessionId: string, event: Event): Promise { - const state = await this.ensureState(sessionId); + let state: SessionState | undefined; + try { + state = await this.ensureState(sessionId); + } catch (error) { + // The session's core scope can be disposed mid-dispatch during shutdown; + // the event is moot once its session is gone. Same guard as ensureState + // applies around attach*, extended to the accessor reads above it. + if (error instanceof Error && error.message === 'InstantiationService has been disposed') { + return; + } + throw error; + } if (state === undefined) return; state.queue = state.queue .then(() => this.dispatch(state, event, isVolatileEventType(event.type))) diff --git a/packages/kap-server/test/fs-watch.e2e.test.ts b/packages/kap-server/test/fs-watch.e2e.test.ts index 99c63812c9..455d46ea77 100644 --- a/packages/kap-server/test/fs-watch.e2e.test.ts +++ b/packages/kap-server/test/fs-watch.e2e.test.ts @@ -20,7 +20,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pino } from 'pino'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { WebSocket, type RawData } from 'ws'; import { startServer, type RunningServer } from '../src/start'; @@ -48,6 +48,7 @@ afterEach(async () => { // ignore } server = undefined; + vi.unstubAllEnvs(); rmSync(tmpDir, { recursive: true, force: true }); rmSync(bridgeHome, { recursive: true, force: true }); }); @@ -215,6 +216,13 @@ describe('WS fs watch (kap-server)', () => { 'burst > 500 changes inside 200ms window → truncated:true', { timeout: 15000 }, async () => { + // Chokidar cannot reliably deliver >500 events inside one 200ms window + // under CPU contention (parallel test files), which flaked this test. + // Shrink the window capacity instead: 600 files over 500ms windows + // guarantees a >100-event window even at ~240 events/s delivery, while + // the truncation path under test is identical. + vi.stubEnv('KIMI_CODE_FS_WATCH_DEBOUNCE_MS', '500'); + vi.stubEnv('KIMI_CODE_FS_WATCH_MAX_CHANGES_PER_WINDOW', '100'); const r = await boot(); const sid = await createSession(r); const conn = await openConn(wsUrl(r)); @@ -246,7 +254,7 @@ describe('WS fs watch (kap-server)', () => { if (frame.type !== 'event.fs.changed') continue; const payload = frame.payload as { truncated?: boolean; count?: number }; if (payload.truncated === true) { - expect(payload.count).toBeGreaterThan(500); + expect(payload.count).toBeGreaterThan(100); sawTruncated = true; break; } diff --git a/packages/kap-server/vitest.config.ts b/packages/kap-server/vitest.config.ts index db9e8c73a4..af7349cd31 100644 --- a/packages/kap-server/vitest.config.ts +++ b/packages/kap-server/vitest.config.ts @@ -9,6 +9,5 @@ export default defineConfig({ test: { name: 'kap-server', include: ['test/**/*.{test,e2e}.ts'], - fileParallelism: false, }, });