Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dotnet/test/E2E/RpcShellAndFleetE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task Should_Execute_Shell_Command()
[Fact]
public async Task Should_Kill_Shell_Process()
{
var session = await CreateSessionAsync();
await using var session = await CreateSessionAsync();
var command = OperatingSystem.IsWindows()
? "powershell -NoLogo -NoProfile -Command \"Start-Sleep -Seconds 30\""
: "sleep 30";
Expand Down
1 change: 1 addition & 0 deletions go/internal/e2e/rpc_shell_and_fleet_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestRPCShellAndFleetE2E(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
t.Cleanup(func() { _ = session.Disconnect() })

var command string
if runtime.GOOS == "windows" {
Expand Down
4 changes: 4 additions & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
asyncio_mode = "auto"
# Bound every test so a deadlock fails fast with a stack dump instead of occupying the
# whole CI leg until GitHub's 6-hour job limit. The full suite runs in ~10 minutes, so no
# individual test legitimately approaches this.
timeout = 300
57 changes: 5 additions & 52 deletions rust/tests/e2e/rpc_mcp_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,58 +194,11 @@ async fn should_start_and_restart_mcp_server() {
.await;
}

#[tokio::test]
async fn should_register_and_unregister_external_mcp_client() {
with_e2e_context(
"rpc_mcp_lifecycle",
"should_register_and_unregister_external_mcp_client",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let host_server = "rpc-lifecycle-extclient-host";
let client = ctx.start_client().await;
let session =
client
.create_session(ctx.approve_all_session_config().with_mcp_servers(
create_test_mcp_servers(ctx.repo_root(), host_server),
))
.await
.expect("create session");
wait_for_mcp_server_status(&session, host_server, McpServerStatus::Connected).await;

let external_name = "rpc-lifecycle-external-client";
assert!(!is_mcp_server_running(&session, external_name).await);

call_session_rpc(
&session,
"session.mcp.registerExternalClient",
json!({
"serverName": external_name,
"client": { "id": external_name },
"transport": { "kind": "in-process" },
"config": { "command": "noop" }
}),
)
.await
.expect("register external MCP client");
assert!(is_mcp_server_running(&session, external_name).await);

call_session_rpc(
&session,
"session.mcp.unregisterExternalClient",
json!({ "serverName": external_name }),
)
.await
.expect("unregister external MCP client");
assert!(!is_mcp_server_running(&session, external_name).await);

session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
// There is deliberately no e2e test for `session.mcp.registerExternalClient`. That method is
// marked `visibility: internal` in the shared API contract: its `client` and `transport` fields
// are live in-process MCP SDK instances, so it cannot be driven over JSON-RPC, and no SDK
// exposes it as a typed method. A raw-RPC test used to pass only because older CLIs routed
// internal methods generically; it never exercised a supported wire API.

#[tokio::test]
async fn should_reload_mcp_servers_with_config() {
Expand Down
43 changes: 43 additions & 0 deletions scripts/codegen/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,45 @@ function emitRustMapAlias(
);
}

/**
* Map a primitive JSON Schema type to its Rust equivalent, or `undefined` when
* the schema is not a plain scalar. Mirrors the primitive branches of
* {@link resolveRustType}.
*/
function rustScalarType(schema: JSONSchema7): string | undefined {
if (schema.enum || schema.const !== undefined) return undefined;
switch (schema.type) {
case "string":
return "String";
case "number":
return "f64";
case "integer":
return isIntegerSchemaBoundedToInt32(schema) ? "i32" : "i64";
case "boolean":
return "bool";
default:
return undefined;
}
}

/**
* Emit a type alias for a named schema that resolves to a primitive scalar
* (e.g. an RPC result declared as `{ "type": "integer" }`). Without this the
* generated RPC surface would reference a `*Result` type that was never
* defined.
*/
function emitRustScalarAlias(
typeName: string,
schema: JSONSchema7,
ctx: RustCodegenCtx,
description?: string,
): void {
if (ctx.generatedNames.has(typeName)) return;
const scalarType = rustScalarType(schema);
if (!scalarType) return;
emitRustTypeAlias(typeName, schema, scalarType, ctx, description);
}

function rustRpcResultDescription(
method: RpcMethod,
resultSchema: JSONSchema7 | undefined,
Expand Down Expand Up @@ -1527,6 +1566,8 @@ function generateApiTypesCode(
} else {
tryEmitRustUnion(schema, name, "", ctx);
}
} else {
emitRustScalarAlias(name, schema, ctx, schema.description);
}
}

Expand Down Expand Up @@ -1576,6 +1617,8 @@ function generateApiTypesCode(
emitRustMapAlias(resultName, resolved, ctx, resolved.description);
} else if (isObjectSchema(resolved)) {
emitRustStruct(resultName, resolved, ctx, resolved.description);
} else {
emitRustScalarAlias(resultName, resolved, ctx, resolved.description);
}
}
}
Expand Down

This file was deleted.

Loading