From 464e1cce496617d98f6c3afed156517dee0ca00d Mon Sep 17 00:00:00 2001 From: turtle1999 <301751225+turtle1999@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:01:00 +0800 Subject: [PATCH 1/3] fix(ci): canonicalize browser dependency scan roots --- scripts/browser-bundled-externals.spec.ts | 12 +++++++++--- scripts/browser-bundled-externals.ts | 6 ++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/browser-bundled-externals.spec.ts b/scripts/browser-bundled-externals.spec.ts index b4d95cf96d..cadc8fb6ae 100644 --- a/scripts/browser-bundled-externals.spec.ts +++ b/scripts/browser-bundled-externals.spec.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' @@ -78,7 +78,7 @@ describe('browser dependency discovery', () => { await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config') }) - it('follows shell workspace aliases, CSS assets and lazy imports without writing output', async () => { + it.each([false, true])('follows shell aliases, CSS and lazy imports without writing output (symlinked root: %s)', async (linked) => { const root = fixture() library(root, 'shell-lib') library(root, 'lazy-lib') @@ -104,7 +104,13 @@ describe('browser dependency discovery', () => { }`) write(root, 'apps/web/dist/sentinel.txt', 'untouched') - expect(await browserBundledExternals(root)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib'])) + const scanRoot = linked ? join(fixture(), 'linked') : root + if (linked) symlinkSync(root, scanRoot, 'junction') + try { + expect(await browserBundledExternals(scanRoot)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib'])) + } finally { + if (linked) unlinkSync(scanRoot) + } expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched') expect(existsSync(join(app, 'dist/index.html'))).toBe(false) expect(existsSync(join(root, 'packages/client/static/lib'))).toBe(false) diff --git a/scripts/browser-bundled-externals.ts b/scripts/browser-bundled-externals.ts index 85053be30f..9139f6b4c2 100644 --- a/scripts/browser-bundled-externals.ts +++ b/scripts/browser-bundled-externals.ts @@ -1,6 +1,6 @@ /** Resolve direct third-party browser inputs through the shipping build configurations, without emitting files. */ -import { globSync, readFileSync } from 'node:fs' +import { globSync, readFileSync, realpathSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, resolve } from 'node:path' import { pathToFileURL } from 'node:url' @@ -159,10 +159,12 @@ async function collectShell( /** * Direct third-party packages resolved by published browser builds. - * @param root - Repository root with installed build dependencies; lib/ is not required. + * @param root - Repository root, possibly symlinked, with installed build dependencies; lib/ is not required. * @returns Names of distributed browser inputs, excluding workspace packages and erased types. */ export async function browserBundledExternals(root: string): Promise> { + // Vite resolves HTML inputs to real paths, so its root must use the same spelling. + root = realpathSync(root) const manifests = new Map() for (const glob of ['packages/*/*/package.json', 'vendor/*/package.json']) { for (const path of globSync(glob, { cwd: root }).sort()) { From 96ee6e49f98997eb40c40946b7279949c48d26cc Mon Sep 17 00:00:00 2001 From: turtle1999 <301751225+turtle1999@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:22:14 +0800 Subject: [PATCH 2/3] test(terminal): await PowerShell prompt delivery --- packages/terminal/terminal-bash/tests/local.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/terminal/terminal-bash/tests/local.spec.ts b/packages/terminal/terminal-bash/tests/local.spec.ts index 9800a79d54..2cf46dd9c2 100644 --- a/packages/terminal/terminal-bash/tests/local.spec.ts +++ b/packages/terminal/terminal-bash/tests/local.spec.ts @@ -330,7 +330,9 @@ describe.skipIf(!hasPwsh)('terminal-bash pwsh real shell', () => { timeoutMs: 8_000, }, 'pwsh') const created = await ctx.terminals.spawn(agent, { type: 'shell', name: 'main', cwd: root }) - expect(created.motd).toContain('dsh> ') + // stdin_read can precede delivery of the printable prompt to the PTY reader. + await expect.poll(() => ctx.terminals.read(agent, created.sessionId, { offset: 0, count: 100 }).text, + { timeout: 8_000 }).toContain('dsh> ') const releaseFile = join(root, 'release-command') // Hold the command across the silence settlement without relying on host load. From 40e39f467365c7aef02d55acbde3b8ac4a5c5049 Mon Sep 17 00:00:00 2001 From: turtle1999 <301751225+turtle1999@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:31:22 +0800 Subject: [PATCH 3/3] test(ci): control Python stray UTF-8 fragment delivery --- .../code-runtime-python/tests/runtime.spec.ts | 100 ------------------ .../tests/stray-fragments.spec.ts | 31 ++++++ 2 files changed, 31 insertions(+), 100 deletions(-) diff --git a/packages/experimental/code-runtime-python/tests/runtime.spec.ts b/packages/experimental/code-runtime-python/tests/runtime.spec.ts index 1f3fd6cb1e..287cf8f785 100644 --- a/packages/experimental/code-runtime-python/tests/runtime.spec.ts +++ b/packages/experimental/code-runtime-python/tests/runtime.spec.ts @@ -1494,106 +1494,6 @@ describe('PythonCodeRuntime — programs and bindings', () => { await fiber.dispose() }) - it('bounds an illegal-UTF-8 native residual by its U+FFFD-decoded cost', async () => { - // Every 0xFF byte is illegal in any UTF-8 sequence, so `toString('utf8')` - // renders each as U+FFFD (3 serialized bytes). `accrueStrayCost` must charge - // that 3, not the raw 1: otherwise the newline-free residual grows to a full - // budget's worth of RAW bytes before flushing — a ~3x undercount that near a - // large maxLogBytes retains hundreds of MiB then expands toward a ~1 GiB peak - // in flushStray's concat + toString. Paced single-byte writes (each its own - // `data` chunk, like the sealing case) expose the sub-chunk accrual: charged - // at 3 the residual crosses a 3072-byte budget after ~1024 bytes and flushes; - // charged at 1 it would need ~3072 bytes, so the peak residual triples. The - // largest merged buffer is the discriminator. - const realConcat = Buffer.concat.bind(Buffer) - let maxConcat = 0 - Buffer.concat = (list: readonly Uint8Array[], total?: number): Buffer => { - const merged = realConcat(list, total) - if (merged.length > maxConcat) maxConcat = merged.length - return merged - } - let result: CodeRunResult - try { - const { runtime } = await setup({ maxLogBytes: 3072, maxWallMs: 30_000 }) - result = await runtime.run({ - program: [ - 'import os, time', - // One byte per chunk on every host: a plain yield lets a loaded - // reader coalesce, and the coalesced chunk is what the bound below - // measures. The payload stays above the 2048 discriminator, so a - // raw-byte undercount still flushes the whole residual at EOF. - 'for _ in range(3200):', - ' os.write(1, b"\\xff")', - ' time.sleep(0.001)', - 'return None', - ].join('\n'), - bindings: [], - }) - } finally { - Buffer.concat = realConcat - } - expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(3072)) - // Charged at 3, the residual flushes around 1024 raw bytes; the largest - // merged buffer stays well under 2048. A raw-byte undercount would let it - // reach ~3072 before flushing, so 2048 discriminates. - expect(maxConcat).toBeLessThan(2048) - // The paced payload costs ~3.2s deterministically, which is above the - // 5000ms default the local unit entry grants, so the case carries its own - // bound instead of relying on the lane to widen it. - }, 20_000) - - it('charges a structurally-valid but illegal UTF-8 sequence its U+FFFD-decoded cost', async () => { - // A CESU-8 lone surrogate `ED A0 80` is structurally well-formed (a 3-byte - // lead plus two 0x80–0xBF continuations) but ILLEGAL: `toString('utf8')` - // renders each of the three bytes as its own U+FFFD (serialized cost 9), not - // one width-3 character. The newline-free flush trigger weighs the residual - // through `accrueStrayCost`, which must validate each lead's - // first-continuation range (ED excludes A0–BF) and charge the true 9 — else a - // CESU flood undercounts 3x and the residual grows toward a full budget's raw - // bytes before flushing, the same peak-memory vector as the 0xFF case. The - // bytes are written one at a time (each its own `data` chunk, no pipe - // coalescing) and `Buffer.concat` is wrapped to measure the peak residual. - const realConcat = Buffer.concat.bind(Buffer) - let maxConcat = 0 - Buffer.concat = (list: readonly Uint8Array[], total?: number): Buffer => { - const merged = realConcat(list, total) - if (merged.length > maxConcat) maxConcat = merged.length - return merged - } - let result: CodeRunResult - try { - const { runtime } = await setup({ maxLogBytes: 3072, maxWallMs: 30_000 }) - result = await runtime.run({ - program: [ - 'import os, time', - 'seq = (0xed, 0xa0, 0x80)', - // 1100 sequences are 3300 raw bytes, past the 3072-byte budget a - // raw-byte undercount reaches, so the undercount flushes above the - // 2048 discriminator instead of only at EOF. - 'for _ in range(1100):', - ' for b in seq:', - ' os.write(1, bytes((b,)))', - ' time.sleep(0.001)', - 'return None', - ].join('\n'), - bindings: [], - }) - } finally { - Buffer.concat = realConcat - } - expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(3072)) - // Each 3-byte sequence costs 9 (three U+FFFD), so single-byte-paced the - // residual crosses the 3072 budget after ~342 raw bytes and flushes; the - // largest merged buffer stays well under 2048. Charging the structural width - // 3 would need ~1024 raw bytes, tripling the peak past 2048. - expect(maxConcat).toBeLessThan(2048) - // The paced payload costs ~3.3s deterministically, which is above the - // 5000ms default the local unit entry grants, so the case carries its own - // bound instead of relying on the lane to widen it. - }, 20_000) - it('charges a lone surrogate its full six escaped bytes, not three', async () => { // A forged `log` frame carrying `\ud800` escapes materializes lone // surrogates after JSON.parse. `Buffer.byteLength` of U+FFFD is 3, but diff --git a/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts b/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts index 9ed70c558b..500082b238 100644 --- a/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts +++ b/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import { expect, it, vi } from 'vitest' +import { logTruncationMarker } from '../src/protocol.ts' // Keep the interpreter and pipe lifecycle real; only OS-dependent read sizes // change. Each byte reaches the runtime as its own data event. @@ -54,3 +55,33 @@ it('seals stray fragments without recopying the sealed prefix', async () => { await fiber.dispose() } }, 40_000) + +it.each([ + { name: 'illegal UTF-8 bytes', payload: 'b"\\xff" * 3200' }, + { name: 'CESU-8 lone surrogates', payload: 'b"\\xed\\xa0\\x80" * 1100' }, +])('bounds $name by their U+FFFD-decoded cost', async ({ payload }) => { + const ctx = new Context() + const fiber = await ctx.plugin(PythonCodeRuntime, { maxLogBytes: 3072, maxWallMs: 30_000 }) + const realConcat = Buffer.concat.bind(Buffer) + let maxConcat = 0 + const concat = vi.spyOn(Buffer, 'concat').mockImplementation((list, total) => { + const merged = realConcat(list, total) + maxConcat = Math.max(maxConcat, merged.length) + return merged + }) + try { + const result = await ctx.codeRuntime.run({ + program: `import os\nos.write(1, ${payload})\nreturn None`, + bindings: [], + }) + expect(result.error).toBeUndefined() + expect(result.logs.at(-1)).toBe(logTruncationMarker(3072)) + // Each raw byte decodes to U+FFFD (three UTF-8 bytes), so a 3072-byte + // budget flushes near 1024 raw bytes. Charging raw or structural widths + // instead retains over 2048 bytes before flushing these payloads. + expect(maxConcat).toBeLessThan(2048) + } finally { + concat.mockRestore() + await fiber.dispose() + } +}, 20_000)