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
133 changes: 75 additions & 58 deletions apps/server/src/device/DeviceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1782,67 +1782,84 @@ it.effect("reconciles SSH host settings without publishing no-op changes", () =>
),
);

it.effect("a failed tool update reports npm's exit code but not its output", () =>
Effect.gen(function* () {
const host: DeviceHost.DeviceHost["Service"] = {
id: LOCAL_DEVICE_HOST_ID,
label: "This machine",
summary: Effect.succeed({
it.effect.each(["exit", "missing"] as const)(
"a failed tool update reports a safe %s failure",
(failure) =>
Effect.gen(function* () {
const host: DeviceHost.DeviceHost["Service"] = {
id: LOCAL_DEVICE_HOST_ID,
kind: "local",
label: "Test server",
platforms: [],
hubInstalled: false,
agentDeviceInstalled: false,
}),
platformAvailability: (platform) => Effect.succeed({ platform, available: false }),
ensureReady: () => Effect.die("Updating a tool does not start the hub"),
ensureAgentReady: () => Effect.die("Updating a tool does not start the agent"),
current: Effect.succeed(null),
stopAgent: Effect.void,
stop: Effect.void,
};
const npmFailure = {
code: ChildProcessSpawner.ExitCode(243),
stdout: "",
stderr: "npm error EACCES https://private:credential@registry.example.test/expo-device-hub",
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
stdoutInvalidUtf8: false,
stderrInvalidUtf8: false,
};
const service = yield* DeviceService.make.pipe(
Effect.provideService(DeviceHost.DeviceHost, host),
Effect.provideService(ProcessRunner.ProcessRunner, {
run: () => Effect.succeed(npmFailure),
}),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("Updating a tool makes no hub requests")),
),
);
label: "This machine",
summary: Effect.succeed({
id: LOCAL_DEVICE_HOST_ID,
kind: "local",
label: "Test server",
platforms: [],
hubInstalled: false,
agentDeviceInstalled: false,
}),
platformAvailability: (platform) => Effect.succeed({ platform, available: false }),
ensureReady: () => Effect.die("Updating a tool does not start the hub"),
ensureAgentReady: () => Effect.die("Updating a tool does not start the agent"),
current: Effect.succeed(null),
stopAgent: Effect.void,
stop: Effect.void,
};
const npmFailure = {
code: ChildProcessSpawner.ExitCode(243),
stdout: "",
stderr: "npm error EACCES https://private:credential@registry.example.test/expo-device-hub",
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
stdoutInvalidUtf8: false,
stderrInvalidUtf8: false,
};
const service = yield* DeviceService.make.pipe(
Effect.provideService(DeviceHost.DeviceHost, host),
Effect.provideService(ProcessRunner.ProcessRunner, {
run: ({ command, args }) =>
failure === "exit"
? Effect.succeed(npmFailure)
: Effect.fail(
new ProcessRunner.ProcessSpawnError({
command,
argumentCount: args.length,
cause: PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
description: "private diagnostic text",
}),
}),
),
}),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("Updating a tool makes no hub requests")),
),
);

const error = yield* service.updateTool("hub").pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "DeviceOperationError",
reason: "command_failed",
exitCode: 243,
});
expect(error.message).toBe(
"Device update device tool failed: The device command failed (exit code 243).",
);
}).pipe(
Effect.provide(
Layer.mergeAll(
ServerSettings.layerTest({ enableDeviceSupport: true }),
layerSecretResolver,
ServerConfig.layerTest(process.cwd(), { prefix: "t3-device-update-" }),
NodeCrypto.layer,
NetService.layer,
).pipe(Layer.provideMerge(NodeServices.layer)),
const error = yield* service.updateTool("hub").pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "DeviceOperationError",
reason: failure === "exit" ? "command_failed" : "package_manager_missing",
});
expect(error.message).toBe(
failure === "exit"
? "Device update device tool failed: The device command failed (exit code 243)."
: "Device update device tool failed: Device support needs npm, which is not on the T3 server's PATH. Install npm (it comes with Node.js from nodejs.org), restart T3 Code, then retry.",
);
}).pipe(
Effect.provide(
Layer.mergeAll(
ServerSettings.layerTest({ enableDeviceSupport: true }),
layerSecretResolver,
ServerConfig.layerTest(process.cwd(), { prefix: "t3-device-update-" }),
NodeCrypto.layer,
NetService.layer,
).pipe(Layer.provideMerge(NodeServices.layer)),
),
),
),
);

describe("agent session release on close", () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/device/DeviceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1430,8 +1430,9 @@ export const make = Effect.gen(function* () {
(cause) =>
new DeviceOperationError({
operation: "update device tool",
reason: "command_failed",
// Only the exit code goes into the shown message; npm's stderr can carry registry credentials.
reason:
cause.reason === "npm_not_found" ? "package_manager_missing" : "command_failed",
// npm's stderr can carry registry credentials; expose only the reason and exit code.
...(cause.exitCode !== undefined ? { exitCode: cause.exitCode } : {}),
cause,
}),
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ export class DeviceOperationError extends Schema.TaggedError<DeviceOperationErro
"invalid_payload",
"settings_failed",
"hub_rejected",
"package_manager_missing",
]),
exitCode: Schema.optional(Schema.Number),
cause: Schema.Defect(),
Expand All @@ -600,6 +601,8 @@ export class DeviceOperationError extends Schema.TaggedError<DeviceOperationErro
invalid_payload: "The device request could not be encoded.",
settings_failed: "Could not read or save device settings.",
hub_rejected: "The device hub could not complete the request.",
package_manager_missing:
"Device support needs npm, which is not on the T3 server's PATH. Install npm (it comes with Node.js from nodejs.org), restart T3 Code, then retry.",
}[this.reason];
return `Device ${this.operation} failed: ${explanation}`;
}
Expand Down
Loading