Quellcode durchsuchen

Merge pull request #3964 from deepseek-harness/turtle/master-ci-browser-realpath

fix(ci): repair browser roots and asynchronous test fixtures
Turtle vor 2 Wochen
Ursprung
Commit
ef0041b351

+ 0 - 100
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<ArrayBuffer> => {
-      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<ArrayBuffer> => {
-      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

+ 31 - 0
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)

+ 3 - 1
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.

+ 9 - 3
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)

+ 4 - 2
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<Set<string>> {
+  // Vite resolves HTML inputs to real paths, so its root must use the same spelling.
+  root = realpathSync(root)
   const manifests = new Map<string, Manifest>()
   for (const glob of ['packages/*/*/package.json', 'vendor/*/package.json']) {
     for (const path of globSync(glob, { cwd: root }).sort()) {