feat(direnv): apply devenv env across all bash backends + always revalidate
Extract applyDirenvPreflight() so the ACP client terminal and PTY backends get the same direnv/devenv overlay as executeBash (previously only the one-shot path did). The helper is a pure (command, env) transform — merge direnv's set under the caller's overlay, prepend a regex-gated unset -v for removed vars — so interactive backends keep their own env shape (live TERM) while executeBash still layers its non-interactive defaults on top. The three dispatch branches are mutually exclusive, so no command is preflighted twice. Also drop the content-hash export cache in loadDirenvEnv: always run direnv export json and let direnv's own watch/mtime invalidation decide freshness, so a changed watched file re-exports even when .envrc is unchanged. Co-Authored-By: seal <noreply@sealedsecurity.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { resetSettingsForTest, Settings, type ShellMinimizerSettings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { buildMinimizerOptions, executeBash } from "@oh-my-pi/pi-coding-agent/exec/bash-executor";
|
||||
import * as direnvModule from "@oh-my-pi/pi-coding-agent/exec/direnv";
|
||||
import { DEFAULT_MAX_BYTES } from "@oh-my-pi/pi-coding-agent/session/streaming-output";
|
||||
import * as shellSnapshot from "@oh-my-pi/pi-coding-agent/utils/shell-snapshot";
|
||||
import type { Shell } from "@oh-my-pi/pi-natives";
|
||||
@@ -137,6 +138,37 @@ describe("executeBash", () => {
|
||||
expect(result.output.trim()).toBe(tempDir);
|
||||
});
|
||||
|
||||
it("passes the full direnv-load budget when the command deadline is disabled (timeout: 0)", async () => {
|
||||
// A disabled command deadline (`timeout: 0`) must NOT collapse the direnv
|
||||
// export window to 0 ms — that would make AbortSignal.timeout(0) abort the
|
||||
// load instantly, silently dropping the repo's direnv env. The load keeps
|
||||
// its full `bash.direnvLoadTimeoutMs` budget. Spying on loadDirenvEnv both
|
||||
// captures the timeoutMs and short-circuits real direnv (null diff = no-op).
|
||||
const budget = (await Settings.init()).get("bash.direnvLoadTimeoutMs");
|
||||
const spy = vi.spyOn(direnvModule, "loadDirenvEnv").mockResolvedValue(null);
|
||||
|
||||
await executeBash("true", { cwd: tempDir, timeout: 0 });
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy.mock.calls[0][1]?.timeoutMs).toBe(budget);
|
||||
expect(spy.mock.calls[0][1]?.timeoutMs).not.toBe(0);
|
||||
});
|
||||
|
||||
it("clamps the direnv-load budget to a positive command timeout smaller than it", async () => {
|
||||
// A positive caller timeout below the budget DOES clamp the direnv window,
|
||||
// proving the fix only relaxes the `timeout: 0` case and did not disable
|
||||
// clamping wholesale. Setting and options.timeout are both milliseconds.
|
||||
const budget = (await Settings.init()).get("bash.direnvLoadTimeoutMs");
|
||||
const callerTimeout = 5;
|
||||
expect(callerTimeout).toBeLessThan(budget);
|
||||
const spy = vi.spyOn(direnvModule, "loadDirenvEnv").mockResolvedValue(null);
|
||||
|
||||
await executeBash("true", { cwd: tempDir, timeout: callerTimeout });
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy.mock.calls[0][1]?.timeoutMs).toBe(Math.min(budget, callerTimeout));
|
||||
});
|
||||
|
||||
it("honors symlinked cwd requests in persistent shells", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user