mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* fix(daemon): ensure node binary dir is on PATH for agent sub-processes on Windows
When pnpm tools-dev launches the daemon via a full path to node.exe the
nodejs directory may not appear in process.env.PATH. Agent .cmd shims on
Windows (e.g. the npm copilot shim) call `"node" script.js`, so cmd.exe
fails to find node and the run exits immediately with:
'"node"' is not recognized as an internal or external command
Fix: in createAgentRuntimeEnv, prepend path.dirname(nodeBin) to PATH
when that directory is not already present. This is a no-op on systems
where the node directory is already in PATH.
Also extend the stdin 'error' swallow to cover the Windows equivalent of
EPIPE: err.code === 'EOF' / err.message === 'write EOF' (libuv UV_EOF).
These fire when the child process exits before reading stdin and were
incorrectly surfaced to the user as AGENT_EXECUTION_FAILED.
Affected file: apps/daemon/src/server.ts
* fix(daemon): anchor PATH-prepend test assertions and add coverage for both branches
- Update the existing 'injects daemon URL and run-scoped tool token' test to
expect PATH = `/opt/open-design/bin${path.delimiter}/bin` after the prepend,
matching the behaviour introduced in the Windows PATH fix.
- Add 'prepends node binary directory to PATH when not already present': given
PATH='/bin' and nodeBin='/opt/node/node', asserts PATH becomes
`/opt/node${path.delimiter}/bin`.
- Add 'does not duplicate node binary directory when already present in PATH':
given PATH already contains the bin dir, asserts it is unchanged (idempotent).
- Replace parts.includes(nodeBinDir) with a normalised some() predicate that
strips trailing path separators and performs a case-insensitive comparison on
win32, producing true idempotence when PATH contains the same directory with a
different case or trailing slash.
* case-insensitive key path finding
* fix Case-insensitive key lookup and add test
* fix(daemon): inject Node binary dir into all agent launch/probe envs on Windows
applyAgentLaunchEnv() is the shared terminal step for every agent spawn
path — detectAgents(), connection tests, and chat runs. Add an optional
`nodeBinDir` parameter (default: path.dirname(process.execPath)) so the
running Node directory is prepended to PATH at every call site without
requiring changes to detection.ts or connectionTest.ts.
Add two regression tests: a cross-platform test covering the Windows-style
'Path' key-casing fix, and a Windows-only test using proper C::\ paths and
';' delimiter verifying the full end-to-end Windows behavior.
* test(daemon): fix applyAgentLaunchEnv tests broken by nodeBinDir default
The 4 tests added in agent-runtime-env.test.ts used the 2-argument form of
applyAgentLaunchEnv, picking up path.dirname(process.execPath) as the new
default and making assertions non-deterministic across CI runners. Also,
three of the fixtures mixed Windows C:\...;... paths with POSIX path.delimiter
(':'), which silently produced wrong splits on POSIX runners.
Pass '' as nodeBinDir to all 4 tests so the early-return and path-prepend
logic is exercised in isolation. Replace Windows-style fixtures with
POSIX paths + path.delimiter; the real C:\... end-to-end shape is already
covered by winTest in tests/runtimes/launch.test.ts.
151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
import path from 'node:path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { createAgentRuntimeEnv, createAgentRuntimeToolPrompt } from '../src/server.js';
|
|
import { applyAgentLaunchEnv } from '../src/runtimes/launch.js';
|
|
|
|
describe('agent runtime tool environment', () => {
|
|
it('injects daemon URL and run-scoped tool token into agent sessions', () => {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: '/bin', OD_TOOL_TOKEN: 'stale-token' },
|
|
'http://127.0.0.1:7456',
|
|
{ token: 'fresh-token' },
|
|
'/opt/open-design/bin/node',
|
|
);
|
|
|
|
expect(env).toMatchObject({
|
|
PATH: `/opt/open-design/bin${path.delimiter}/bin`,
|
|
OD_DAEMON_URL: 'http://127.0.0.1:7456',
|
|
OD_NODE_BIN: '/opt/open-design/bin/node',
|
|
OD_TOOL_TOKEN: 'fresh-token',
|
|
});
|
|
});
|
|
|
|
it('prepends node binary directory to PATH when not already present', () => {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: '/bin' },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/node/node',
|
|
);
|
|
|
|
expect(env.PATH).toBe(`/opt/node${path.delimiter}/bin`);
|
|
});
|
|
|
|
it('does not duplicate node binary directory when already present in PATH', () => {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: `/opt/node${path.delimiter}/bin` },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/node/node',
|
|
);
|
|
|
|
expect(env.PATH).toBe(`/opt/node${path.delimiter}/bin`);
|
|
});
|
|
|
|
it('updates the existing path key in place when the base env uses Windows-style Path casing', () => {
|
|
// Windows GUI launches commonly spread process.env where the search path is
|
|
// stored under 'Path' rather than 'PATH'. The function must read and update
|
|
// that same key so child_process.spawn (which de-duplicates env keys
|
|
// case-insensitively on Windows) does not discard the inherited directories.
|
|
const env = createAgentRuntimeEnv(
|
|
{ Path: `/usr/local/bin` },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/node/node',
|
|
);
|
|
|
|
// The original 'Path' key must be updated with the prepended node dir.
|
|
expect(env.Path).toBe(`/opt/node${path.delimiter}/usr/local/bin`);
|
|
// A competing uppercase 'PATH' key must NOT be created alongside it.
|
|
expect(env.PATH).toBeUndefined();
|
|
});
|
|
|
|
it('does not leak stale inherited tool tokens when no run token was minted', () => {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: '/bin', OD_TOOL_TOKEN: 'stale-token' },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/open-design/bin/node',
|
|
);
|
|
|
|
expect(env.OD_DAEMON_URL).toBe('http://127.0.0.1:7456');
|
|
expect(env.OD_NODE_BIN).toBe('/opt/open-design/bin/node');
|
|
expect(env.OD_TOOL_TOKEN).toBeUndefined();
|
|
});
|
|
|
|
it('pins the daemon runtime data dir into agent sessions', () => {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: '/bin' },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/open-design/bin/node',
|
|
);
|
|
|
|
expect(env.OD_DATA_DIR).toBe(process.env.OD_DATA_DIR);
|
|
});
|
|
|
|
it('describes daemon URL and token availability without exposing the token', () => {
|
|
const prompt = createAgentRuntimeToolPrompt('http://127.0.0.1:7456', {
|
|
token: 'secret-run-token',
|
|
});
|
|
|
|
expect(prompt).toContain('Daemon URL: `http://127.0.0.1:7456`');
|
|
expect(prompt).toContain('`OD_DAEMON_URL`');
|
|
expect(prompt).toContain('`OD_NODE_BIN`');
|
|
expect(prompt).toContain('`"$OD_NODE_BIN" "$OD_BIN" tools ...`');
|
|
expect(prompt).toContain('& $env:OD_NODE_BIN $env:OD_BIN tools ...');
|
|
expect(prompt).toContain('`OD_TOOL_TOKEN` is available');
|
|
expect(prompt).toContain('do not print, persist, or override it');
|
|
expect(prompt).not.toContain('secret-run-token');
|
|
});
|
|
|
|
it('describes missing token availability without exposing stale internals', () => {
|
|
const prompt = createAgentRuntimeToolPrompt('http://127.0.0.1:7456', null);
|
|
|
|
expect(prompt).toContain('Daemon URL: `http://127.0.0.1:7456`');
|
|
expect(prompt).toContain('`OD_TOOL_TOKEN` is not available');
|
|
expect(prompt).not.toContain('Bearer');
|
|
});
|
|
});
|
|
|
|
describe('applyAgentLaunchEnv', () => {
|
|
it('returns env unchanged when childPathPrepend is empty and no node dir is provided', () => {
|
|
const base = { Path: ['/usr/local/bin', '/usr/bin'].join(path.delimiter), OTHER: 'val' };
|
|
const result = applyAgentLaunchEnv(base, { childPathPrepend: [] }, '');
|
|
expect(result).toBe(base);
|
|
});
|
|
|
|
it('prepends childPathPrepend entries to PATH when key is uppercase', () => {
|
|
const base = { PATH: '/usr/bin' };
|
|
const result = applyAgentLaunchEnv(base, { childPathPrepend: ['/opt/copilot'] }, '');
|
|
expect(result.PATH).toBe(`/opt/copilot${path.delimiter}/usr/bin`);
|
|
expect(result.Path).toBeUndefined();
|
|
});
|
|
|
|
it('uses the existing Windows-style Path key instead of adding a competing PATH key', () => {
|
|
// This is the Windows GUI regression: env.PATH is undefined when the actual
|
|
// key is 'Path'. The old code created a fresh PATH = just childPathPrepend,
|
|
// discarding the system paths and the node directory prepended by
|
|
// createAgentRuntimeEnv, which caused '"node" is not recognized' errors.
|
|
// Pure POSIX paths + path.delimiter keep the assertion correct on all platforms;
|
|
// the real Windows C:\...;... shape is covered by winTest in launch.test.ts.
|
|
const base = { Path: ['/opt/nodejs', '/usr/bin'].join(path.delimiter) };
|
|
const result = applyAgentLaunchEnv(base, { childPathPrepend: ['/opt/agent/bin'] }, '');
|
|
|
|
// The existing 'Path' key must be updated in place.
|
|
expect(result.Path).toBe(
|
|
['/opt/agent/bin', '/opt/nodejs', '/usr/bin'].join(path.delimiter),
|
|
);
|
|
// A competing uppercase 'PATH' key must NOT be created.
|
|
expect(result.PATH).toBeUndefined();
|
|
});
|
|
|
|
it('deduplicates entries already present in Path', () => {
|
|
const existing = ['/opt/bin', '/usr/bin'].join(path.delimiter);
|
|
const base = { Path: existing };
|
|
const result = applyAgentLaunchEnv(base, { childPathPrepend: ['/opt/bin'] }, '');
|
|
expect(result.Path).toBe(existing);
|
|
});
|
|
});
|