ソースを参照

fix(code-runtime-python): close coverage gap and tighten the wire mirror

- Cover the log-frame `truncated` rebuild branch: assert a literal-true flag
  rides along and any other value (1, string, false) is dropped, closing the
  protocol.ts branch the coverage gate flagged.
- Correct encodeJsonPlain's JSDoc: it matches compact JSON.stringify EXCEPT on
  a beyond-safe-range integral double, where it emits the exact BigInt digits
  (`...846976`) rather than the rounded `...847000` — the divergence the
  "emits exact digits" test pins.
- Declare py/protocol.py's `global`-bearing frames (Namespace, CallMessage)
  with functional TypedDict syntax so they carry the real wire key instead of
  a `global_` attribute the wire never sends, and split optional-field messages
  (Namespace/LogMessage/DoneMessage) into a required base plus a total=False
  subclass so `type` and other required fields cannot be dropped. Widen
  HostToChild to include the boot and run frames the host sends before replies.
- Reword the mirror e2e's py/ directory assertion to describe the source-tree
  layout it actually checks.
Chinesezjc 1 ヶ月 前
親
コミット
f0d669883f

+ 43 - 31
packages/code-runtime/code-runtime-python/py/protocol.py

@@ -3,6 +3,13 @@
 Mirrors ``src/protocol.ts``. Frames travel on fd 3 as JSON-lines (one JSON
 object per line). The host validates every inbound frame; this side trusts
 host replies.
+
+The wire uses the JSON key ``global`` (a Python keyword), so the frame
+``TypedDict``s that carry it are declared with the functional syntax rather than
+class bodies: a class attribute cannot be named ``global``, and a ``global_``
+attribute would describe a key the wire never sends. Optional-field messages
+pair a required base with a ``total=False`` subclass so a required field such as
+``type`` cannot be dropped while ``value``/``error``/``truncated`` stay optional.
 """
 
 from __future__ import annotations
@@ -15,17 +22,6 @@ from typing import Any, Literal, TypedDict, Union
 PROTOCOL_FD = 3
 
 
-class BootMessage(TypedDict):
-    """Host → child, first frame on fd 3. Carries every cap and the namespaces."""
-
-    type: Literal["boot"]
-    cpuSeconds: int
-    addressSpaceBytes: int
-    maxLogBytes: int
-    maxValueBytes: int
-    namespaces: list["Namespace"]
-
-
 class ErrorClass(TypedDict):
     """A namespace's program-visible exception class: rejected calls raise its
     instances carrying the failed member name on ``memberNameProperty``."""
@@ -34,13 +30,27 @@ class ErrorClass(TypedDict):
     memberNameProperty: str
 
 
-class Namespace(TypedDict, total=False):
-    """One binding namespace declaration: the global name, its function names,
-    and an optional program-visible ``errorClass`` for rejected calls."""
+# ``global`` is a Python keyword, so the required part is declared functionally
+# to hold the real wire key; ``errorClass`` is optional per the TS `errorClass?`.
+_NamespaceRequired = TypedDict("_NamespaceRequired", {"global": str, "names": "list[str]"})
+
+
+class Namespace(_NamespaceRequired, total=False):
+    """One binding namespace declaration: the ``global`` name, its function
+    ``names``, and an optional program-visible ``errorClass`` for rejected calls."""
+
+    errorClass: ErrorClass
 
-    global_: str  # required; renamed on the wire: JSON field is ``global`` (Python keyword collision)
-    names: list[str]  # required
-    errorClass: ErrorClass  # optional — mirrors the TS `errorClass?`
+
+class BootMessage(TypedDict):
+    """Host → child, first frame on fd 3. Carries every cap and the namespaces."""
+
+    type: Literal["boot"]
+    cpuSeconds: int
+    addressSpaceBytes: int
+    maxLogBytes: int
+    maxValueBytes: int
+    namespaces: "list[Namespace]"
 
 
 class RunMessage(TypedDict):
@@ -56,17 +66,17 @@ class BootAckMessage(TypedDict):
     type: Literal["boot-ack"]
 
 
-class CallMessage(TypedDict):
-    """Child → host: one bridged binding call from the model program."""
+# ``global`` wire key: whole message declared functionally, all fields required.
+CallMessage = TypedDict(
+    "CallMessage",
+    {"type": Literal["call"], "id": int, "global": str, "name": str, "args": Any},
+)
 
-    type: Literal["call"]
-    id: int
-    global_: str  # wire field is ``global``
-    name: str
-    args: Any
 
+_LogMessageRequired = TypedDict("_LogMessageRequired", {"type": Literal["log"], "text": str})
 
-class LogMessage(TypedDict, total=False):
+
+class LogMessage(_LogMessageRequired, total=False):
     """Child → host: one captured text chunk, streamed eagerly.
 
     ``truncated`` is set only on the frame that IS the child ledger's truncation
@@ -74,9 +84,7 @@ class LogMessage(TypedDict, total=False):
     the child did — mirrors the TS `truncated?`.
     """
 
-    type: Literal["log"]  # required
-    text: str  # required
-    truncated: bool  # optional
+    truncated: bool
 
 
 class DoneErrorField(TypedDict):
@@ -87,10 +95,12 @@ class DoneErrorField(TypedDict):
     message: str
 
 
-class DoneMessage(TypedDict, total=False):
+_DoneMessageRequired = TypedDict("_DoneMessageRequired", {"type": Literal["done"]})
+
+
+class DoneMessage(_DoneMessageRequired, total=False):
     """Child → host: the program settled. ``value`` and ``error`` are optional per the TS mirror."""
 
-    type: Literal["done"]  # required — TypedDict(total=False) allows this via a required subclass in Py 3.11+; MVP keeps it flat
     value: Any
     error: DoneErrorField
 
@@ -113,7 +123,9 @@ class ReplyErr(TypedDict):
 
 
 ReplyMessage = Union[ReplyOk, ReplyErr]
-HostToChild = ReplyMessage
+# The host sends ``boot`` and ``run`` before any ``reply``, so the child-facing
+# inbound union covers all three, not replies alone.
+HostToChild = Union[BootMessage, RunMessage, ReplyMessage]
 
 
 def log_truncation_marker(max_bytes: int) -> str:

+ 5 - 1
packages/code-runtime/code-runtime-python/src/protocol.ts

@@ -124,7 +124,11 @@ export function logTruncationMarker(maxBytes: number): string {
  * (the worker backend's wire is equally stack-safe). Callers must pass a value
  * produced by `JSON.parse` (or equally JSON-plain): only `null`, finite
  * numbers, booleans, strings, dense arrays, and plain objects — this encoder
- * validates nothing. Output is byte-identical to compact `JSON.stringify`.
+ * validates nothing. Output matches compact `JSON.stringify` byte for byte
+ * EXCEPT on an integral double beyond the safe range, where {@link scalarJson}
+ * emits the exact integer's BigInt digits rather than `JSON.stringify`'s rounded
+ * spelling (`1152921504606846976`, not `...847000`) so the seam's lossless-JSON
+ * promise holds across the wire.
  * @param value - a JSON-plain value (e.g. straight from `JSON.parse`).
  * @returns the compact JSON encoding.
  */

+ 4 - 3
packages/code-runtime/code-runtime-python/tests/protocol-mirror.e2e.ts

@@ -53,8 +53,9 @@ describe.skipIf(!python3Available)('protocol.py mirrors protocol.ts at runtime',
 })
 
 it('names the py/ directory that ships with the package', () => {
-  // The package.json `files` list ships `py/**/*.py`; the mirror test resolves
-  // the marker source relative to the built package, so the directory must exist
-  // beside the tests even when python3 is absent from the runner.
+  // Resolves py/ relative to this test file; the same directory ships in the
+  // package.json `files` whitelist (`py/**/*.py`). The tests/ directory itself
+  // is not published — this asserts the source-tree layout the mirror test
+  // depends on, so it holds even when python3 is absent from the runner.
   expect(existsSync(pyDir)).toBe(true)
 })

+ 12 - 0
packages/code-runtime/code-runtime-python/tests/protocol.spec.ts

@@ -22,6 +22,18 @@ describe('validateChildFrame', () => {
     expect(validateChildFrame({ type: 'log' })).toBeUndefined()
   })
 
+  it('carries a log frame truncation flag only for the literal true', () => {
+    // The child's own ledger marker sets `truncated: true`; the host rebuilds
+    // it so it stops capturing at the same point.
+    expect(validateChildFrame({ type: 'log', text: 'x', truncated: true }))
+      .toEqual({ type: 'log', text: 'x', truncated: true })
+    // Any other truthy or non-boolean value is a forgery and is dropped from
+    // the rebuild — otherwise it would silence capture for the rest of the run.
+    expect(validateChildFrame({ type: 'log', text: 'x', truncated: 1 })).toEqual({ type: 'log', text: 'x' })
+    expect(validateChildFrame({ type: 'log', text: 'x', truncated: 'yes' })).toEqual({ type: 'log', text: 'x' })
+    expect(validateChildFrame({ type: 'log', text: 'x', truncated: false })).toEqual({ type: 'log', text: 'x' })
+  })
+
   it('rebuilds call frames with a numeric id, string global, and string name', () => {
     expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'echo', args: { x: 1 } }))
       .toEqual({ type: 'call', id: 1, global: 'tools', name: 'echo', args: { x: 1 } })