| 1 | // @effect-diagnostics nodeBuiltinImport:off - exercises concurrent real CLI subprocesses. |
| 2 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 3 | import { describe, expect, it } from "@effect/vitest"; |
| 4 | import * as NodeChildProcess from "node:child_process"; |
| 5 | import * as NodeUtil from "node:util"; |
| 6 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Path from "effect/Path"; |
| 10 | import { ensureAgentDeviceShim } from "./AgentDeviceShim.ts"; |
| 11 | import { |
| 12 | agentDeviceConfigPath, |
| 13 | agentDeviceSession, |
| 14 | writeAgentDeviceConfig, |
| 15 | } from "./AgentDeviceTarget.ts"; |
| 16 | |
| 17 | const exec = NodeUtil.promisify(NodeChildProcess.execFile); |
| 18 | |
| 19 | describe("host-bound agent commands", () => { |
| 20 | it.effect("runs two hosts concurrently and only updates the reconnected host", () => |
| 21 | Effect.gen(function* () { |
| 22 | const fs = yield* FileSystem.FileSystem; |
| 23 | const path = yield* Path.Path; |
| 24 | const temp = yield* fs.makeTempDirectoryScoped({ prefix: "t3-device-target-" }); |
| 25 | const platform = yield* HostProcess.Platform; |
| 26 | const dir = path.join( |
| 27 | temp, |
| 28 | platform === "win32" ? "paths with spaces" : "quotes '\" $HOME `literal`", |
| 29 | ); |
| 30 | yield* fs.makeDirectory(dir); |
| 31 | const entryPath = path.join(dir, "cli.mjs"); |
| 32 | yield* fs.writeFileString( |
| 33 | entryPath, |
| 34 | `import { readFileSync } from 'node:fs'; |
| 35 | const args = process.argv.slice(2); |
| 36 | console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8')); |
| 37 | if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, |
| 38 | ); |
| 39 | const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir }); |
| 40 | const files = yield* Effect.forEach(["mini", "android"], (host) => |
| 41 | agentDeviceConfigPath(dir, host, path), |
| 42 | ); |
| 43 | for (const [index, file] of files.entries()) |
| 44 | yield* writeAgentDeviceConfig(file, { |
| 45 | baseUrl: `http://127.0.0.1:${1000 + index}`, |
| 46 | token: `token-${index}`, |
| 47 | entryPath, |
| 48 | }); |
| 49 | const invoke = (file: string) => |
| 50 | exec( |
| 51 | platform === "win32" ? process.execPath : path.join(shim, "agent-device"), |
| 52 | [ |
| 53 | ...(platform === "win32" ? [path.join(shim, "agent-device-launcher.mjs")] : []), |
| 54 | "snapshot", |
| 55 | "--config", |
| 56 | file, |
| 57 | "--session", |
| 58 | "test-session", |
| 59 | ], |
| 60 | { env: { ...process.env, AGENT_DEVICE_DAEMON_BASE_URL: "http://wrong-host" } }, |
| 61 | ).then((result) => JSON.parse(result.stdout)); |
| 62 | expect(yield* Effect.promise(() => Promise.all(files.map(invoke)))).toEqual([ |
| 63 | { daemonBaseUrl: "http://127.0.0.1:1000", daemonAuthToken: "token-0" }, |
| 64 | { daemonBaseUrl: "http://127.0.0.1:1001", daemonAuthToken: "token-1" }, |
| 65 | ]); |
| 66 | const second = yield* fs.readFileString(files[1]!); |
| 67 | yield* writeAgentDeviceConfig(files[0]!, { |
| 68 | baseUrl: "http://127.0.0.1:2000", |
| 69 | token: "new", |
| 70 | entryPath, |
| 71 | }); |
| 72 | expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new"); |
| 73 | expect(yield* fs.readFileString(files[1]!)).toBe(second); |
| 74 | expect(yield* agentDeviceSession("thread", "mini", "same-id")).not.toBe( |
| 75 | yield* agentDeviceSession("thread", "android", "same-id"), |
| 76 | ); |
| 77 | for (const args of [ |
| 78 | ["snapshot"], |
| 79 | ["snapshot", "--config", files[0]!], |
| 80 | ["snapshot", "--config", "help"], |
| 81 | ["snapshot", "--config", files[0]!, "--session"], |
| 82 | ]) { |
| 83 | yield* Effect.promise(() => |
| 84 | expect( |
| 85 | exec(process.execPath, [path.join(shim, "agent-device-launcher.mjs"), ...args]), |
| 86 | ).rejects.toThrow("Call device_open first"), |
| 87 | ); |
| 88 | } |
| 89 | }).pipe( |
| 90 | Effect.scoped, |
| 91 | Effect.provideService(HostProcess.IsExecutable, true), |
| 92 | Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), |
| 93 | Effect.provide(NodeServices.layer), |
| 94 | ), |
| 95 | ); |
| 96 | }); |