Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

396 lines
12 KiB
JavaScript

/**
* Unit tests for layered .env resolution: pure parse/merge/upsert primitives,
* real-filesystem load/save against temp dirs (mode 600 asserted), and a real
* linked-worktree fixture proving an empty worktree .env falls through to the
* home-scoped layer.
*/
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import test from "node:test";
import {
applyLayeredEnvToProcess,
listPresent,
loadLayeredEnv,
mergeEnvLayers,
parseDotenv,
saveEnvVar,
upsertEnvContent,
writeSecret,
} from "./env-layers.mjs";
const ROOT = resolve(new URL("../..", import.meta.url).pathname);
// realpath so git-canonicalized paths (macOS /var -> /private/var) compare equal.
function tempDir(prefix) {
return realpathSync(mkdtempSync(join(tmpdir(), prefix)));
}
function git(cwd, args) {
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
assert.equal(
result.status,
0,
`git ${args.join(" ")} failed: ${result.stderr}`,
);
return result.stdout.trim();
}
// --- parseDotenv --------------------------------------------------------------
test("parseDotenv handles comments, export prefix, quotes, and CRLF", () => {
const parsed = parseDotenv(
[
"# comment",
"PLAIN=value",
"export EXPORTED=exported-value",
'DQ="double quoted"',
"SQ='single quoted'",
"SPACED = padded ",
"not a valid line",
"EMPTY=",
"1BAD=starts-with-digit",
].join("\r\n"),
);
assert.deepEqual(parsed, {
PLAIN: "value",
EXPORTED: "exported-value",
DQ: "double quoted",
SQ: "single quoted",
SPACED: "padded",
EMPTY: "",
});
});
// --- mergeEnvLayers -------------------------------------------------------------
test("mergeEnvLayers: first (highest-precedence) definition wins, sources attributed", () => {
const { values, sources } = mergeEnvLayers([
{ source: "process", values: { A: "proc", EMPTYWIN: "" } },
{ source: "repo", values: { A: "repo", B: "repo", EMPTYWIN: "file" } },
{ source: "home", values: { C: "home", D: "home", SKIPPED: undefined } },
]);
assert.deepEqual(values, {
A: "proc",
B: "repo",
C: "home",
D: "home",
EMPTYWIN: "",
});
assert.deepEqual(sources, {
A: "process",
B: "repo",
C: "home",
D: "home",
EMPTYWIN: "process",
});
});
// --- upsertEnvContent ------------------------------------------------------------
test("upsertEnvContent replaces in place, preserves comments, appends new keys", () => {
const before = [
"# keep me",
"KEEP=old-keep",
"REPLACE=old",
"",
"export ALSO=old-also",
].join("\n");
const after = upsertEnvContent(before, {
REPLACE: "new",
ALSO: "new-also",
ADDED: "fresh",
});
assert.equal(
after,
[
"# keep me",
"KEEP=old-keep",
"REPLACE=new",
"",
"ALSO=new-also",
"ADDED=fresh",
"",
].join("\n"),
);
});
test("upsertEnvContent on empty text emits just the entries", () => {
assert.equal(upsertEnvContent("", { A: "1" }), "A=1\n");
});
// --- loadLayeredEnv / listPresent ---------------------------------------------------
test("loadLayeredEnv merges process > repo > home and reports layers", () => {
const base = tempDir("env-layers-load-");
try {
const repoRoot = join(base, "repo");
const homeEnvPath = join(base, "home", ".eliza", ".env");
for (const dir of [repoRoot, join(base, "home", ".eliza")]) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(join(repoRoot, ".env"), "A=repo\nB=repo\n");
writeFileSync(homeEnvPath, "B=home\nC=home\n");
const { values, sources, layers } = loadLayeredEnv({
processEnv: { A: "proc" },
repoRoot,
homeEnvPath,
});
assert.equal(values.A, "proc");
assert.equal(values.B, "repo");
assert.equal(values.C, "home");
assert.deepEqual(sources, {
A: "process",
B: "repo",
C: "home",
});
assert.deepEqual(
layers.map((layer) => [layer.source, layer.exists]),
[
["process", true],
["repo", true],
["home", true],
],
);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("loadLayeredEnv: missing files are graceful", () => {
const base = tempDir("env-layers-absent-");
try {
const repoRoot = join(base, "repo");
mkdirSync(repoRoot, { recursive: true });
const { values, sources, layers } = loadLayeredEnv({
processEnv: {},
repoRoot,
homeEnvPath: join(base, "nonexistent", ".env"),
});
assert.deepEqual(values, {});
assert.deepEqual(sources, {});
assert.deepEqual(
layers.map((layer) => layer.source),
["process", "repo", "home"],
);
assert.equal(
layers.every((layer) => layer.source === "process" || !layer.exists),
true,
);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("applyLayeredEnvToProcess hydrates only keys the process does not define", () => {
const base = tempDir("env-layers-apply-");
try {
const repoRoot = join(base, "repo");
mkdirSync(repoRoot, { recursive: true });
writeFileSync(join(repoRoot, ".env"), "FROM_REPO=repo\nKEPT=shadowed\n");
const homeEnvPath = join(base, "home.env");
writeFileSync(homeEnvPath, "FROM_HOME=home\nFROM_REPO=home-loses\n");
const processEnv = { KEPT: "process-wins", EMPTY: "" };
const loaded = applyLayeredEnvToProcess({
processEnv,
repoRoot,
homeEnvPath,
});
assert.equal(processEnv.FROM_REPO, "repo");
assert.equal(processEnv.FROM_HOME, "home");
assert.equal(processEnv.KEPT, "process-wins");
assert.equal(processEnv.EMPTY, "", "empty-but-defined keys stay untouched");
assert.equal(loaded.sources.FROM_REPO, "repo");
assert.equal(loaded.sources.KEPT, "process");
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("fresh linked worktree with empty repo .env uses home-scoped secrets, not main checkout .env", () => {
const base = tempDir("env-layers-worktree-home-");
try {
const mainRoot = join(base, "main");
const wtRoot = join(base, "wt");
const homeEnvPath = join(base, "home", ".eliza", ".env");
git(base, ["init", "-b", "main", "main"]);
writeFileSync(join(mainRoot, "seed.txt"), "seed\n");
writeFileSync(join(mainRoot, ".env"), "TOKEN=stale-main\n");
git(mainRoot, ["add", "seed.txt"]);
git(mainRoot, [
"-c",
"user.email=test@example.com",
"-c",
"user.name=Test",
"commit",
"-m",
"seed",
]);
git(mainRoot, ["worktree", "add", wtRoot]);
mkdirSync(dirname(homeEnvPath), { recursive: true });
writeFileSync(homeEnvPath, "TOKEN=home-secret\n");
const loaded = loadLayeredEnv({
processEnv: {},
repoRoot: wtRoot,
homeEnvPath,
});
assert.equal(loaded.values.TOKEN, "home-secret");
assert.equal(loaded.sources.TOKEN, "home");
assert.deepEqual(
loaded.layers.map((layer) => layer.source),
["process", "repo", "home"],
);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("listPresent attributes each source and never returns values", () => {
const base = tempDir("env-layers-present-");
try {
const repoRoot = join(base, "repo");
const homeEnvPath = join(base, "home.env");
mkdirSync(repoRoot, { recursive: true });
writeFileSync(join(repoRoot, ".env"), "FROM_REPO=secret-repo\nEMPTYVAL=\n");
writeFileSync(homeEnvPath, "FROM_HOME=secret-home\n");
const rows = listPresent(
["FROM_PROC", "FROM_REPO", "FROM_HOME", "EMPTYVAL", "ABSENT"],
{
processEnv: { FROM_PROC: "secret-proc" },
repoRoot,
homeEnvPath,
},
);
assert.deepEqual(rows, [
{ name: "FROM_PROC", present: true, source: "process" },
{ name: "FROM_REPO", present: true, source: "repo" },
{ name: "FROM_HOME", present: true, source: "home" },
{ name: "EMPTYVAL", present: false, source: "repo" },
{ name: "ABSENT", present: false, source: null },
]);
assert.equal(JSON.stringify(rows).includes("secret-"), false);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
// --- saveEnvVar -----------------------------------------------------------------------
test("writeSecret creates the home file with mode 600 and upserts on re-save", () => {
const base = tempDir("env-layers-save-");
try {
const homeEnvPath = join(base, ".eliza", ".env");
const processEnv = {};
const first = writeSecret("NEW_TOKEN", "tok-1", {
scope: "home",
homeEnvPath,
processEnv,
});
assert.deepEqual(first, {
key: "NEW_TOKEN",
scope: "home",
path: homeEnvPath,
});
assert.equal(readFileSync(homeEnvPath, "utf8"), "NEW_TOKEN=tok-1\n");
assert.equal(statSync(homeEnvPath).mode & 0o777, 0o600);
assert.equal(processEnv.NEW_TOKEN, "tok-1");
writeFileSync(homeEnvPath, "# note\nNEW_TOKEN=tok-1\nOTHER=keep\n");
writeSecret("NEW_TOKEN", "tok-2", {
scope: "home",
homeEnvPath,
processEnv,
});
assert.equal(
readFileSync(homeEnvPath, "utf8"),
"# note\nNEW_TOKEN=tok-2\nOTHER=keep\n",
);
assert.equal(statSync(homeEnvPath).mode & 0o777, 0o600);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("writeSecret writes the repo layer when scoped", () => {
const base = tempDir("env-layers-save-repo-");
try {
const processEnv = {};
const result = writeSecret("REPO_ONLY", "x", {
scope: "repo",
repoRoot: base,
processEnv,
});
assert.equal(result.path, join(base, ".env"));
assert.equal(readFileSync(join(base, ".env"), "utf8"), "REPO_ONLY=x\n");
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("saveEnvVar remains a compatibility wrapper for existing dashboard callers", () => {
const base = tempDir("env-layers-save-wrapper-");
try {
const processEnv = {};
const result = saveEnvVar("WRAPPED", "x", "repo", {
repoRoot: base,
processEnv,
});
assert.deepEqual(result, {
key: "WRAPPED",
target: "repo",
path: join(base, ".env"),
});
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("writeSecret rejects invalid keys, multi-line values, and bad scopes", () => {
const base = tempDir("env-layers-save-bad-");
try {
const options = { homeEnvPath: join(base, ".env"), processEnv: {} };
assert.throws(
() => writeSecret("bad key", "v", options),
/invalid env key/,
);
assert.throws(
() => writeSecret("GOOD_KEY", "a\nb", options),
/single-line/,
);
assert.throws(
() => writeSecret("GOOD_KEY", "v", { ...options, scope: "elsewhere" }),
/scope/,
);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("HITL dashboard, lane driver, collector, and tests import the shared layered env module", () => {
const importers = [
"scripts/lifeops/hitl-credential-dashboard.mjs",
"scripts/lifeops/run-11632-live-lanes.mjs",
"scripts/lifeops/collect-11632-live-validation-status.mjs",
"scripts/lifeops/env-layers.test.mjs",
];
for (const relativePath of importers) {
const text = readFileSync(join(ROOT, relativePath), "utf8");
assert.match(
text,
/from "\.\/env-layers\.mjs"/,
`${relativePath} must import scripts/lifeops/env-layers.mjs`,
);
}
});