// @effect-diagnostics nodeBuiltinImport:off - exercises concurrent real CLI subprocesses.import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { describe, expect, it } from "@effect/vitest";import * as NodeChildProcess from "node:child_process";import * as NodeUtil from "node:util";import * as NodeServices from "@effect/platform-node/NodeServices";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path";import { ensureAgentDeviceShim } from "./AgentDeviceShim.ts";import { agentDeviceConfigPath, agentDeviceSession, writeAgentDeviceConfig,} from "./AgentDeviceTarget.ts"; const exec = NodeUtil.promisify(NodeChildProcess.execFile); describe("host-bound agent commands", () => { it.effect("runs two hosts concurrently and only updates the reconnected host", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const temp = yield* fs.makeTempDirectoryScoped({ prefix: "t3-device-target-" }); const platform = yield* HostProcessPlatform; const dir = path.join( temp, platform === "win32" ? "paths with spaces" : "quotes '\" $HOME `literal`", ); yield* fs.makeDirectory(dir); const entryPath = path.join(dir, "cli.mjs"); yield* fs.writeFileString( entryPath, `import { readFileSync } from 'node:fs';const args = process.argv.slice(2);console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8'));if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, ); const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir }); const files = ["mini", "android"].map((host) => agentDeviceConfigPath(dir, host, path)); for (const [index, file] of files.entries()) yield* writeAgentDeviceConfig(file, { baseUrl: `http://127.0.0.1:${1000 + index}`, token: `token-${index}`, entryPath, }); const invoke = (file: string) => exec( platform === "win32" ? process.execPath : path.join(shim, "agent-device"), [ ...(platform === "win32" ? [path.join(shim, "agent-device-launcher.mjs")] : []), "snapshot", "--config", file, "--session", "test-session", ], { env: { ...process.env, AGENT_DEVICE_DAEMON_BASE_URL: "http://wrong-host" } }, ).then((result) => JSON.parse(result.stdout)); expect(yield* Effect.promise(() => Promise.all(files.map(invoke)))).toEqual([ { daemonBaseUrl: "http://127.0.0.1:1000", daemonAuthToken: "token-0" }, { daemonBaseUrl: "http://127.0.0.1:1001", daemonAuthToken: "token-1" }, ]); const second = yield* fs.readFileString(files[1]!); yield* writeAgentDeviceConfig(files[0]!, { baseUrl: "http://127.0.0.1:2000", token: "new", entryPath, }); expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new"); expect(yield* fs.readFileString(files[1]!)).toBe(second); expect(agentDeviceSession("thread", "mini", "same-id")).not.toBe( agentDeviceSession("thread", "android", "same-id"), ); for (const args of [ ["snapshot"], ["snapshot", "--config", files[0]!], ["snapshot", "--config", "help"], ["snapshot", "--config", files[0]!, "--session"], ]) { yield* Effect.promise(() => expect( exec(process.execPath, [path.join(shim, "agent-device-launcher.mjs"), ...args]), ).rejects.toThrow("Call device_open first"), ); } }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), );});
// @effect-diagnostics nodeBuiltinImport:off - exercises concurrent real CLI subprocesses.
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { describe, expect, it } from "@effect/vitest";
import * as NodeChildProcess from "node:child_process";
import * as NodeUtil from "node:util";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { ensureAgentDeviceShim } from "./AgentDeviceShim.ts";
import {
agentDeviceConfigPath,
agentDeviceSession,
writeAgentDeviceConfig,
} from "./AgentDeviceTarget.ts";
const exec = NodeUtil.promisify(NodeChildProcess.execFile);
describe("host-bound agent commands", () => {
it.effect("runs two hosts concurrently and only updates the reconnected host", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const temp = yield* fs.makeTempDirectoryScoped({ prefix: "t3-device-target-" });
const platform = yield* HostProcessPlatform;
const dir = path.join(
temp,
platform === "win32" ? "paths with spaces" : "quotes '\" $HOME `literal`",
);
yield* fs.makeDirectory(dir);
const entryPath = path.join(dir, "cli.mjs");
yield* fs.writeFileString(
entryPath,
`import { readFileSync } from 'node:fs';
const args = process.argv.slice(2);
console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8'));
if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`,
);
const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir });
const files = ["mini", "android"].map((host) => agentDeviceConfigPath(dir, host, path));
for (const [index, file] of files.entries())
yield* writeAgentDeviceConfig(file, {
baseUrl: `http://127.0.0.1:${1000 + index}`,
token: `token-${index}`,
entryPath,
});
const invoke = (file: string) =>
exec(
platform === "win32" ? process.execPath : path.join(shim, "agent-device"),
[
...(platform === "win32" ? [path.join(shim, "agent-device-launcher.mjs")] : []),
"snapshot",
"--config",
file,
"--session",
"test-session",
],
{ env: { ...process.env, AGENT_DEVICE_DAEMON_BASE_URL: "http://wrong-host" } },
).then((result) => JSON.parse(result.stdout));
expect(yield* Effect.promise(() => Promise.all(files.map(invoke)))).toEqual([
{ daemonBaseUrl: "http://127.0.0.1:1000", daemonAuthToken: "token-0" },
{ daemonBaseUrl: "http://127.0.0.1:1001", daemonAuthToken: "token-1" },
]);
const second = yield* fs.readFileString(files[1]!);
yield* writeAgentDeviceConfig(files[0]!, {
baseUrl: "http://127.0.0.1:2000",
token: "new",
entryPath,
});
expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new");
expect(yield* fs.readFileString(files[1]!)).toBe(second);
expect(agentDeviceSession("thread", "mini", "same-id")).not.toBe(
agentDeviceSession("thread", "android", "same-id"),
);
for (const args of [
["snapshot"],
["snapshot", "--config", files[0]!],
["snapshot", "--config", "help"],
["snapshot", "--config", files[0]!, "--session"],
]) {
yield* Effect.promise(() =>
expect(
exec(process.execPath, [path.join(shim, "agent-device-launcher.mjs"), ...args]),
).rejects.toThrow("Call device_open first"),
);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
});