## Summary Keep the Mux module's launcher around after startup so it can append useful diagnostics when `mux server` is killed outside the Node runtime. ## Background The module previously forked `mux server` and returned immediately, which meant external kills (for example `SIGKILL` or an OOM kill) could leave users with only a stopped app and no launcher-side clue about what happened. ## Implementation - keep the existing module inputs and startup shape intact - launch `mux server` under a detached Bash watcher that waits for the child process to exit - append signal/exit-code diagnostics to `log_path` when the server dies unexpectedly - include a best-effort kernel OOM/SIGKILL hint in the log when the host exposes it - add Terraform and Bun tests that cover the new launcher diagnostics - bump the module examples from `1.3.1` to `1.4.0` ## Validation - `bun x prettier --check registry/coder/modules/mux/README.md registry/coder/modules/mux/main.test.ts registry/coder/modules/mux/mux.tftest.hcl registry/coder/modules/mux/run.sh` - `terraform fmt -check -recursive registry/coder/modules/mux` - `cd registry/coder/modules/mux && terraform validate` - `cd registry/coder/modules/mux && terraform test -verbose` - `cd registry/coder/modules/mux && bun test main.test.ts` - `bun run shellcheck -- registry/coder/modules/mux/run.sh` --- Generated with mux (exec mode) using openai:gpt-5.4.
173 lines
4.7 KiB
TypeScript
173 lines
4.7 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
executeScriptInContainer,
|
|
execContainer,
|
|
findResourceInstance,
|
|
readFileContainer,
|
|
removeContainer,
|
|
runContainer,
|
|
runTerraformApply,
|
|
runTerraformInit,
|
|
testRequiredVariables,
|
|
} from "~test";
|
|
|
|
describe("mux", async () => {
|
|
await runTerraformInit(import.meta.dir);
|
|
|
|
testRequiredVariables(import.meta.dir, {
|
|
agent_id: "foo",
|
|
});
|
|
|
|
it("runs with default", async () => {
|
|
const state = await runTerraformApply(import.meta.dir, {
|
|
agent_id: "foo",
|
|
});
|
|
|
|
const output = await executeScriptInContainer(
|
|
state,
|
|
"alpine/curl",
|
|
"sh",
|
|
"apk add --no-cache bash tar gzip ca-certificates findutils nodejs && update-ca-certificates",
|
|
);
|
|
if (output.exitCode !== 0) {
|
|
console.log("STDOUT:\n" + output.stdout.join("\n"));
|
|
console.log("STDERR:\n" + output.stderr.join("\n"));
|
|
}
|
|
expect(output.exitCode).toBe(0);
|
|
const expectedLines = [
|
|
"📥 No package manager found; downloading tarball from registry...",
|
|
"🥳 mux has been installed in /tmp/mux",
|
|
"🚀 Starting mux server on port 4000...",
|
|
"Check logs at /tmp/mux.log!",
|
|
];
|
|
for (const line of expectedLines) {
|
|
expect(output.stdout).toContain(line);
|
|
}
|
|
}, 60000);
|
|
|
|
it("parses custom additional_arguments", async () => {
|
|
const state = await runTerraformApply(import.meta.dir, {
|
|
agent_id: "foo",
|
|
install: false,
|
|
log_path: "/tmp/mux.log",
|
|
additional_arguments:
|
|
"--open-mode pinned --add-project '/workspaces/my repo'",
|
|
});
|
|
|
|
const instance = findResourceInstance(state, "coder_script");
|
|
const id = await runContainer("alpine/curl");
|
|
|
|
try {
|
|
const setup = await execContainer(id, [
|
|
"sh",
|
|
"-c",
|
|
`apk add --no-cache bash >/dev/null
|
|
mkdir -p /tmp/mux
|
|
cat <<'EOF' > /tmp/mux/mux
|
|
#!/usr/bin/env sh
|
|
i=1
|
|
for arg in "$@"; do
|
|
echo "arg$i=$arg"
|
|
i=$((i + 1))
|
|
done
|
|
EOF
|
|
chmod +x /tmp/mux/mux`,
|
|
]);
|
|
expect(setup.exitCode).toBe(0);
|
|
|
|
const output = await execContainer(id, ["sh", "-c", instance.script]);
|
|
if (output.exitCode !== 0) {
|
|
console.log("STDOUT:\n" + output.stdout);
|
|
console.log("STDERR:\n" + output.stderr);
|
|
}
|
|
expect(output.exitCode).toBe(0);
|
|
|
|
await execContainer(id, ["sh", "-c", "sleep 1"]);
|
|
const log = await readFileContainer(id, "/tmp/mux.log");
|
|
expect(log).toContain("arg1=server");
|
|
expect(log).toContain("arg2=--port");
|
|
expect(log).toContain("arg3=4000");
|
|
expect(log).toContain("arg4=--open-mode");
|
|
expect(log).toContain("arg5=pinned");
|
|
expect(log).toContain("arg6=--add-project");
|
|
expect(log).toContain("arg7=/workspaces/my repo");
|
|
} finally {
|
|
await removeContainer(id);
|
|
}
|
|
}, 60000);
|
|
|
|
it("logs signal-based exits after startup", async () => {
|
|
const state = await runTerraformApply(import.meta.dir, {
|
|
agent_id: "foo",
|
|
install: false,
|
|
log_path: "/tmp/mux.log",
|
|
});
|
|
|
|
const instance = findResourceInstance(state, "coder_script");
|
|
const id = await runContainer("alpine/curl");
|
|
|
|
try {
|
|
const setup = await execContainer(id, [
|
|
"sh",
|
|
"-c",
|
|
`apk add --no-cache bash >/dev/null
|
|
mkdir -p /tmp/mux
|
|
cat <<'EOF' > /tmp/mux/mux
|
|
#!/usr/bin/env sh
|
|
target_pid="$$"
|
|
(
|
|
sleep 1
|
|
kill -9 "$target_pid"
|
|
) &
|
|
while true; do
|
|
sleep 1
|
|
done
|
|
EOF
|
|
chmod +x /tmp/mux/mux`,
|
|
]);
|
|
expect(setup.exitCode).toBe(0);
|
|
|
|
const output = await execContainer(id, ["sh", "-c", instance.script]);
|
|
if (output.exitCode !== 0) {
|
|
console.log("STDOUT:\n" + output.stdout);
|
|
console.log("STDERR:\n" + output.stderr);
|
|
}
|
|
expect(output.exitCode).toBe(0);
|
|
|
|
await execContainer(id, ["sh", "-c", "sleep 2"]);
|
|
const log = await readFileContainer(id, "/tmp/mux.log");
|
|
expect(log).toContain("shell exit code 137");
|
|
expect(log).toContain(
|
|
"SIGKILL usually means the process was killed externally or by the OOM killer.",
|
|
);
|
|
} finally {
|
|
await removeContainer(id);
|
|
}
|
|
}, 60000);
|
|
|
|
it("runs with npm present", async () => {
|
|
const state = await runTerraformApply(import.meta.dir, {
|
|
agent_id: "foo",
|
|
});
|
|
|
|
const output = await executeScriptInContainer(
|
|
state,
|
|
"node:20-alpine",
|
|
"sh",
|
|
"apk add bash",
|
|
);
|
|
|
|
expect(output.exitCode).toBe(0);
|
|
const expectedLines = [
|
|
"📦 Installing mux via npm into /tmp/mux...",
|
|
"⏭️ Skipping lifecycle scripts with --ignore-scripts",
|
|
"🥳 mux has been installed in /tmp/mux",
|
|
"🚀 Starting mux server on port 4000...",
|
|
"Check logs at /tmp/mux.log!",
|
|
];
|
|
for (const line of expectedLines) {
|
|
expect(output.stdout).toContain(line);
|
|
}
|
|
}, 180000);
|
|
});
|