mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(daemon): add phase 2c cli wrappers Co-authored-by: multica-agent <github@multica.ai> * fix: handle desktop-gated CLI imports Co-authored-by: multica-agent <github@multica.ai> * fix: pass sidecar ipc path to agent wrappers Co-authored-by: multica-agent <github@multica.ai> * fix: make agent wrapper env explicit Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): preserve CLI import and diff edge cases Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
179 lines
6.8 KiB
TypeScript
179 lines
6.8 KiB
TypeScript
import path from 'node:path';
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { SIDECAR_ENV } from '@open-design/sidecar-proto';
|
|
|
|
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('passes the daemon sidecar IPC path from the explicit base env into agent wrapper sessions', () => {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: '/bin', [SIDECAR_ENV.IPC_PATH]: '/tmp/open-design/ipc/daemon.sock' },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/open-design/bin/node',
|
|
);
|
|
|
|
expect(env[SIDECAR_ENV.IPC_PATH]).toBe('/tmp/open-design/ipc/daemon.sock');
|
|
});
|
|
|
|
it('does not pull the daemon sidecar IPC path from ambient process state', () => {
|
|
vi.stubEnv(SIDECAR_ENV.IPC_PATH, '/tmp/open-design/ipc/stale.sock');
|
|
try {
|
|
const env = createAgentRuntimeEnv(
|
|
{ PATH: '/bin' },
|
|
'http://127.0.0.1:7456',
|
|
null,
|
|
'/opt/open-design/bin/node',
|
|
);
|
|
|
|
expect(env[SIDECAR_ENV.IPC_PATH]).toBeUndefined();
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|