add simulation network log
This commit is contained in:
@@ -17,6 +17,7 @@ import type { EventSource } from "./context/sdk"
|
||||
import type { SimulationMcpRuntimeState } from "./simulation-mcp"
|
||||
import type { rpc } from "./worker"
|
||||
import { SimulationDebugLog } from "../../../testing/simulation/debug-log"
|
||||
import { SimulationNetworkLog } from "./simulation-network-log"
|
||||
import { fileURLToPath } from "url"
|
||||
import { writeHeapSnapshot } from "v8"
|
||||
|
||||
@@ -49,18 +50,52 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
|
||||
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const request = new Request(input, init)
|
||||
const body = request.body ? await request.text() : undefined
|
||||
const headers = Object.fromEntries(request.headers.entries())
|
||||
SimulationDebugLog.write("simulate.fetch.start", { method: request.method, url: request.url })
|
||||
const result = await client.call("fetch", {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
body,
|
||||
})
|
||||
SimulationDebugLog.write("simulate.fetch.end", { method: request.method, url: request.url, status: result.status })
|
||||
return new Response(result.body, {
|
||||
status: result.status,
|
||||
headers: result.headers,
|
||||
})
|
||||
const startedAt = Date.now()
|
||||
const startedAtIso = new Date(startedAt).toISOString()
|
||||
try {
|
||||
const result = await client.call("fetch", {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
SimulationDebugLog.write("simulate.fetch.end", {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
status: result.status,
|
||||
})
|
||||
SimulationNetworkLog.record({
|
||||
time: startedAtIso,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
status: result.status,
|
||||
durationMs: Date.now() - startedAt,
|
||||
requestHeaders: headers,
|
||||
requestBody: body,
|
||||
responseHeaders: result.headers,
|
||||
responseBody: result.body,
|
||||
})
|
||||
return new Response(result.body, {
|
||||
status: result.status,
|
||||
headers: result.headers,
|
||||
})
|
||||
} catch (err) {
|
||||
SimulationNetworkLog.record({
|
||||
time: startedAtIso,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
status: 0,
|
||||
durationMs: Date.now() - startedAt,
|
||||
requestHeaders: headers,
|
||||
requestBody: body,
|
||||
responseHeaders: {},
|
||||
responseBody: "",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
return fn as typeof fetch
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SimulationActions } from "@/testing/simulation/actions"
|
||||
import { SimulationNetworkLog } from "./simulation-network-log"
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
|
||||
@@ -548,6 +549,39 @@ function createServer(options: Options) {
|
||||
toolResult(await control(options, "GET", "/experimental/simulation/snapshot")),
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"simulation_network_log_get",
|
||||
{
|
||||
description:
|
||||
"Return the persistent network log: every HTTP request the simulated TUI sent to the backend, with method, URL, status code, headers, and response body (body truncated at 32KB per entry). Capped to the last 500 entries.",
|
||||
inputSchema: z.object({
|
||||
limit: z.number().int().min(1).max(500).optional(),
|
||||
urlIncludes: z.string().optional(),
|
||||
statusMin: z.number().int().optional(),
|
||||
statusMax: z.number().int().optional(),
|
||||
}),
|
||||
},
|
||||
async (input) => {
|
||||
let entries = SimulationNetworkLog.snapshot()
|
||||
if (input.urlIncludes) entries = entries.filter((e) => e.url.includes(input.urlIncludes!))
|
||||
if (typeof input.statusMin === "number") entries = entries.filter((e) => e.status >= input.statusMin!)
|
||||
if (typeof input.statusMax === "number") entries = entries.filter((e) => e.status <= input.statusMax!)
|
||||
if (typeof input.limit === "number") entries = entries.slice(-input.limit)
|
||||
return toolResult({ entries, total: entries.length })
|
||||
},
|
||||
)
|
||||
|
||||
server.registerTool(
|
||||
"simulation_network_log_clear",
|
||||
{
|
||||
description: "Clear the simulated TUI's persistent network log. Use between scripted runs to isolate observations.",
|
||||
},
|
||||
async () => {
|
||||
SimulationNetworkLog.clear()
|
||||
return toolResult({ cleared: true })
|
||||
},
|
||||
)
|
||||
|
||||
if (masterEnabled()) {
|
||||
server.registerTool(
|
||||
"simulation_instances_discover",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Per-process network log for the simulated TUI. Captures every HTTP request
|
||||
// the TUI sends to the worker-backed backend (via `createWorkerFetch`).
|
||||
// Exposed through the simulation MCP server so tests / agents can inspect what
|
||||
// the running TUI is actually doing.
|
||||
|
||||
export interface NetworkLogEntry {
|
||||
readonly id: number
|
||||
readonly time: string
|
||||
readonly method: string
|
||||
readonly url: string
|
||||
readonly status: number
|
||||
readonly durationMs: number
|
||||
readonly requestHeaders: Record<string, string>
|
||||
readonly requestBody?: string
|
||||
readonly responseHeaders: Record<string, string>
|
||||
readonly responseBody: string
|
||||
readonly responseTruncated: boolean
|
||||
readonly error?: string
|
||||
}
|
||||
|
||||
const MAX_ENTRIES = 500
|
||||
const MAX_BODY_BYTES = 32_768
|
||||
|
||||
const entries: NetworkLogEntry[] = []
|
||||
let nextId = 1
|
||||
|
||||
function truncate(text: string): { body: string; truncated: boolean } {
|
||||
if (text.length <= MAX_BODY_BYTES) return { body: text, truncated: false }
|
||||
return { body: text.slice(0, MAX_BODY_BYTES), truncated: true }
|
||||
}
|
||||
|
||||
export function record(entry: Omit<NetworkLogEntry, "id" | "responseTruncated"> & { responseBody: string }) {
|
||||
const { body, truncated } = truncate(entry.responseBody)
|
||||
entries.push({ ...entry, id: nextId++, responseBody: body, responseTruncated: truncated })
|
||||
if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES)
|
||||
}
|
||||
|
||||
export function snapshot(): NetworkLogEntry[] {
|
||||
return entries.slice()
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
entries.length = 0
|
||||
}
|
||||
|
||||
export * as SimulationNetworkLog from "./simulation-network-log"
|
||||
@@ -13,7 +13,10 @@ import { SimulationDebugLog } from "./debug-log"
|
||||
import { Simulation, type LLMScript } from "./service"
|
||||
|
||||
const providerID = ProviderID.make("simulation")
|
||||
const modelID = ModelID.make("mock")
|
||||
// Use a model id that contains "gpt-" (and not "oss" / "gpt-4") so the tool
|
||||
// registry's GPT-style gate enables `apply_patch` in the simulated chain.
|
||||
// See registry.ts:319-322 for the gating logic.
|
||||
const modelID = ModelID.make("gpt-mock")
|
||||
|
||||
const model: Provider.Model = {
|
||||
id: modelID,
|
||||
|
||||
@@ -12,16 +12,14 @@
|
||||
{ "type": "text", "content": "Looking at `src/greeting.ts`, the function currently returns `\"Hi, …\"`. " },
|
||||
{ "type": "text", "content": "You asked for a friendlier greeting, so I'll change the prefix from `Hi` to `Hello`. " },
|
||||
{ "type": "text", "content": "I'll keep the template literal and the `name` interpolation untouched. " },
|
||||
{ "type": "text", "content": "Here's the plan: I'll use the `edit` tool to replace the single return line. " },
|
||||
{ "type": "text", "content": "Here's the plan: I'll use the `apply_patch` tool to replace the single return line. " },
|
||||
{ "type": "text", "content": "Patching `src/greeting.ts` now." },
|
||||
{
|
||||
"type": "tool-call",
|
||||
"toolCallId": "patch-greeting-1",
|
||||
"toolName": "edit",
|
||||
"toolName": "apply_patch",
|
||||
"input": {
|
||||
"filePath": "/opencode/src/greeting.ts",
|
||||
"oldString": "return `Hi, ${name}`",
|
||||
"newString": "return `Hello, ${name}`"
|
||||
"patchText": "*** Begin Patch\n*** Update File: /opencode/src/greeting.ts\n@@\n- return `Hi, ${name}`\n+ return `Hello, ${name}`\n*** End Patch\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user