8fdda66893
Issue #62: a tmux client whose LC_ALL/LC_CTYPE/LANG names no UTF-8 prints `_` for every non-ASCII character, and a macOS LaunchAgent starts the dashboard with no locale at all — Vietnamese text and Claude Code's logo came out as underscores in every browser terminal. - shell-path.js: ensureUtf8Locale() defaults LC_CTYPE (en_US.UTF-8 on macOS, C.UTF-8 elsewhere) at start, before any tmux call; an explicit LC_ALL and Windows are left alone - RunSetup.tsx: the `/` list now offers Claude Code's bundled skills (/code-review, /security-review, /simplify, /loop…) and newer built-ins, and every enabled plugin's commands and skills as /<plugin>:<name> - Claude login decided by the TOKEN's owner (GET /api/oauth/profile, cached per token), not by .claude.json: a running session can leave the credentials store holding one account's token under another account's name. The Profiles modal, its Check quota and the status-bar quota now show the account requests are really billed to and warn on the mismatch; a switch saves the outgoing token into its OWNER's profile (credentials only) instead of overwriting the profile .claude.json names; saving a profile while they disagree is refused (EMISMATCH); a saved profile holding another account's token reports EWRONGACCOUNT instead of that account's usage - Running Claude Code sessions follow a switch on their own (they re-read the credentials store when it changes), so the "close your sessions first" warning is gone
97 lines
3.3 KiB
JavaScript
97 lines
3.3 KiB
JavaScript
/**
|
|
* @file Tests for server/lib/shell-path.js — login-shell PATH hydration with a
|
|
* stubbed spawner (noisy rc output, errors, opt-out, dedupe) plus one real
|
|
* `/bin/sh` run to prove the argv works end to end, and the UTF-8 locale
|
|
* default that keeps tmux from printing `_` for every non-ASCII character.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it, beforeEach, afterEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { hydrateShellPath, shellPath, ensureUtf8Locale } = require("../lib/shell-path");
|
|
|
|
const M = "__AGENTHUB_PATH__";
|
|
const stub =
|
|
(stdout, err = null) =>
|
|
(_cmd, _args, _opts, cb) => {
|
|
setImmediate(() => cb(err, stdout));
|
|
return { stdin: { end() {} } };
|
|
};
|
|
|
|
describe("hydrateShellPath", () => {
|
|
let saved;
|
|
beforeEach(() => {
|
|
saved = { PATH: process.env.PATH, OPT: process.env.AGENTHUB_SHELL_PATH };
|
|
process.env.PATH = "/usr/bin:/bin";
|
|
delete process.env.AGENTHUB_SHELL_PATH;
|
|
});
|
|
afterEach(() => {
|
|
process.env.PATH = saved.PATH;
|
|
if (saved.OPT === undefined) delete process.env.AGENTHUB_SHELL_PATH;
|
|
else process.env.AGENTHUB_SHELL_PATH = saved.OPT;
|
|
});
|
|
|
|
it("appends new segments from between the delimiters, ignoring rc noise", async () => {
|
|
const ok = await hydrateShellPath({
|
|
spawn: stub(`welcome!\n${M}/home/u/.local/bin:/usr/bin:/opt/x${M}bye`),
|
|
});
|
|
assert.equal(ok, true);
|
|
assert.equal(process.env.PATH, "/usr/bin:/bin:/home/u/.local/bin:/opt/x");
|
|
});
|
|
|
|
it("remembers the shell's own PATH in the shell's order", async () => {
|
|
await hydrateShellPath({ spawn: stub(`${M}/home/u/.nvm/bin:/usr/bin${M}`) });
|
|
assert.equal(shellPath(), "/home/u/.nvm/bin:/usr/bin");
|
|
});
|
|
|
|
it("keeps PATH on error or missing delimiters", async () => {
|
|
assert.equal(await hydrateShellPath({ spawn: stub("", new Error("timeout")) }), false);
|
|
assert.equal(await hydrateShellPath({ spawn: stub("no markers") }), false);
|
|
assert.equal(
|
|
await hydrateShellPath({
|
|
spawn: () => {
|
|
throw new Error("ENOENT");
|
|
},
|
|
}),
|
|
false
|
|
);
|
|
assert.equal(process.env.PATH, "/usr/bin:/bin");
|
|
});
|
|
|
|
it("does nothing with AGENTHUB_SHELL_PATH=0", async () => {
|
|
process.env.AGENTHUB_SHELL_PATH = "0";
|
|
assert.equal(await hydrateShellPath({ spawn: stub(`${M}/x${M}`) }), false);
|
|
assert.equal(process.env.PATH, "/usr/bin:/bin");
|
|
});
|
|
|
|
it("reads a real shell's PATH", async () => {
|
|
const ok = await hydrateShellPath({ shell: "/bin/sh" });
|
|
assert.equal(ok, true);
|
|
assert.ok(process.env.PATH.startsWith("/usr/bin:/bin"));
|
|
});
|
|
});
|
|
|
|
describe("ensureUtf8Locale", () => {
|
|
it("sets LC_CTYPE when no locale is set (a macOS LaunchAgent)", () => {
|
|
const env = {};
|
|
ensureUtf8Locale(env, "darwin");
|
|
assert.equal(env.LC_CTYPE, "en_US.UTF-8");
|
|
const linux = { LANG: "C" };
|
|
ensureUtf8Locale(linux, "linux");
|
|
assert.equal(linux.LC_CTYPE, "C.UTF-8");
|
|
});
|
|
|
|
it("leaves a UTF-8 locale, an explicit LC_ALL and Windows alone", () => {
|
|
for (const [env, platform] of [
|
|
[{ LANG: "vi_VN.UTF-8" }, "darwin"],
|
|
[{ LC_CTYPE: "en_US.utf8" }, "linux"],
|
|
[{ LC_ALL: "C" }, "darwin"],
|
|
[{}, "win32"],
|
|
]) {
|
|
const before = { ...env };
|
|
ensureUtf8Locale(env, platform);
|
|
assert.deepEqual(env, before);
|
|
}
|
|
});
|
|
});
|