Repository navigation
Expand file tree
/
Copy pathsshDeviceScript.test.ts
More file actions
134 lines (129 loc) · 5.93 KB
/
Copy pathsshDeviceScript.test.ts
File metadata and controls
134 lines (129 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - verifies generated remote scripts using real shell and Node processes.
import * as Effect from "effect/Effect";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { expect, it } from "@effect/vitest";
import * as NodeChildProcess from "node:child_process";
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeUtil from "node:util";
import { quoteRemoteArg, remoteDeviceEnvironment, remoteDeviceScript } from "./sshDeviceScript.ts";
const exec = NodeUtil.promisify(NodeChildProcess.execFile);
it.effect("finds Android Studio Java for a non-interactive SSH session", () =>
Effect.gen(function* () {
if ((yield* HostProcess.Platform) === "win32") return;
yield* Effect.promise(async () => {
const home = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-ssh-java-"));
try {
const javaHome = NodePath.join(home, ".local/opt/android-studio/jbr");
await NodeFSP.mkdir(NodePath.join(javaHome, "bin"), { recursive: true });
await NodeFSP.writeFile(
NodePath.join(javaHome, "bin/java"),
"#!/bin/sh\necho test-java\n",
{ mode: 0o755 },
);
const result = await exec("/bin/sh", ["-c", `${remoteDeviceEnvironment}\njava`], {
env: { HOME: home, PATH: "/nonexistent", JAVA_HOME: "" },
});
expect(result.stdout.trim()).toBe("test-java");
} finally {
await NodeFSP.rm(home, { recursive: true, force: true });
}
});
}),
);
it.effect("preserves shell metacharacters and newlines in remote arguments", () =>
Effect.gen(function* () {
if ((yield* HostProcess.Platform) === "win32") return;
yield* Effect.promise(async () => {
const value = "quotes ' \" ; $(echo expanded) $HOME\nnext line";
const result = await exec("sh", ["-c", `printf %s ${quoteRemoteArg(value)}`]);
expect(result.stdout).toBe(value);
});
}),
);
it.effect("stops a daemon left for its owner when starting without agent tools", () =>
Effect.gen(function* () {
if ((yield* HostProcess.Platform) === "win32") return;
yield* Effect.promise(async () => {
const home = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-ssh-start-"));
try {
const state = NodePath.join(home, ".t3/device/hosts/owner");
const daemonFile = NodePath.join(state, "daemon.json");
const calls = NodePath.join(home, "calls");
const failMarker = NodePath.join(home, "fail-stop");
const entry = NodePath.join(home, "agent-device.mjs");
await NodeFSP.mkdir(NodePath.join(home, "bin"));
await NodeFSP.mkdir(state, { recursive: true });
await NodeFSP.writeFile(
entry,
`import * as fs from "node:fs";
fs.appendFileSync(${JSON.stringify(calls)}, process.argv.slice(2).join(" ") + "\\n");
if (fs.existsSync(${JSON.stringify(failMarker)})) process.exitCode = 1;`,
);
await NodeFSP.writeFile(
NodePath.join(state, "agent.json"),
JSON.stringify({ entryPath: entry }),
);
await NodeFSP.writeFile(daemonFile, JSON.stringify({ httpPort: 1, token: "claims" }));
// An empty PATH leaves no platforms, so the script ends right after the daemon check.
const start = () =>
NodeChildProcess.spawnSync(process.execPath, [], {
input: remoteDeviceScript("owner", "start"),
env: { HOME: home, PATH: NodePath.join(home, "bin") },
encoding: "utf8",
});
await NodeFSP.writeFile(failMarker, "");
expect(start().stderr).toContain("Could not stop the agent-device daemon");
expect(await NodeFSP.readFile(daemonFile, "utf8")).toContain("claims");
await NodeFSP.rm(failMarker);
expect(start().stderr).toContain("Android SDK missing");
await expect(NodeFSP.access(daemonFile)).rejects.toThrow();
expect(await NodeFSP.readFile(calls, "utf8")).toBe(
`daemon stop --state-dir ${state}\ndaemon stop --state-dir ${state}\n`,
);
} finally {
await NodeFSP.rm(home, { recursive: true, force: true });
}
});
}),
);
it.effect("keeps a daemon record its missing agent CLI cannot stop", () =>
Effect.gen(function* () {
if ((yield* HostProcess.Platform) === "win32") return;
yield* Effect.promise(async () => {
const home = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-ssh-start-"));
try {
const state = NodePath.join(home, ".t3/device/hosts/owner");
const daemonFile = NodePath.join(state, "daemon.json");
await NodeFSP.mkdir(NodePath.join(home, "bin"));
await NodeFSP.mkdir(state, { recursive: true });
await NodeFSP.writeFile(
NodePath.join(state, "agent.json"),
JSON.stringify({ entryPath: NodePath.join(home, "missing/agent-device.mjs") }),
);
const start = async (pid: number) => {
await NodeFSP.writeFile(
daemonFile,
JSON.stringify({ pid, httpPort: 1, token: "claims" }),
);
return NodeChildProcess.spawnSync(process.execPath, [], {
input: remoteDeviceScript("owner", "start"),
env: { HOME: home, PATH: NodePath.join(home, "bin") },
encoding: "utf8",
});
};
// This test process stands in for a daemon that is still running.
expect((await start(process.pid)).stderr).toContain(
"Could not stop the agent-device daemon",
);
expect(await NodeFSP.readFile(daemonFile, "utf8")).toContain("claims");
const exited = NodeChildProcess.spawnSync(process.execPath, ["-e", ""]).pid;
expect((await start(exited)).stderr).toContain("Android SDK missing");
await expect(NodeFSP.access(daemonFile)).rejects.toThrow();
} finally {
await NodeFSP.rm(home, { recursive: true, force: true });
}
});
}),
);