diff --git a/package/ego-browser/src/env.test.mjs b/package/ego-browser/src/env.test.mjs new file mode 100644 index 00000000..a27107c2 --- /dev/null +++ b/package/ego-browser/src/env.test.mjs @@ -0,0 +1,73 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { resolve } from "node:path"; + +import { agentWorkspace, REPO_ROOT, SRC_DIR } from "../dist/src/env.js"; + +const BUNDLED_SKILL = resolve(SRC_DIR, "ego-browser"); +const REPO_SKILL = resolve(REPO_ROOT, "..", "..", "skills", "ego-browser"); +const HOME = resolve("/home/agent"); +const INSTALL_ROOT = resolve(HOME, ".local", "share", "ego", "ego-skills"); + +function existsIn(paths) { + return (path) => paths.includes(path); +} + +test("agentWorkspace prefers the EGO_BROWSER_AGENT_WORKSPACE override", () => { + const override = resolve("/workspaces/custom"); + const result = agentWorkspace({ + env: { EGO_BROWSER_AGENT_WORKSPACE: override }, + exists: () => true, + }); + assert.equal(result, override); +}); + +test("agentWorkspace uses the skill bundled next to the build output", () => { + const result = agentWorkspace({ env: {}, exists: existsIn([BUNDLED_SKILL]) }); + assert.equal(result, BUNDLED_SKILL); +}); + +test("agentWorkspace uses the repo-layout skill when it exists", () => { + const result = agentWorkspace({ env: {}, exists: existsIn([REPO_SKILL]) }); + assert.equal(result, REPO_SKILL); +}); + +test("agentWorkspace falls back to the installed skill registered by onboarding", () => { + const result = agentWorkspace({ + env: { HOME }, + exists: existsIn([resolve(INSTALL_ROOT, "learnings")]), + }); + assert.equal(result, INSTALL_ROOT); +}); + +test("agentWorkspace prefers an ego-browser subdirectory inside the install root", () => { + const result = agentWorkspace({ + env: { HOME }, + exists: existsIn([ + resolve(INSTALL_ROOT, "ego-browser", "learnings"), + resolve(INSTALL_ROOT, "learnings"), + ]), + }); + assert.equal(result, resolve(INSTALL_ROOT, "ego-browser")); +}); + +test("agentWorkspace ignores an installed directory without learnings", () => { + const result = agentWorkspace({ + env: { HOME }, + exists: existsIn([INSTALL_ROOT]), + }); + assert.equal(result, REPO_SKILL); +}); + +test("agentWorkspace resolves the home directory from USERPROFILE", () => { + const result = agentWorkspace({ + env: { USERPROFILE: HOME }, + exists: existsIn([resolve(INSTALL_ROOT, "learnings")]), + }); + assert.equal(result, INSTALL_ROOT); +}); + +test("agentWorkspace keeps the repo-layout path when nothing exists", () => { + const result = agentWorkspace({ env: {}, exists: () => false }); + assert.equal(result, REPO_SKILL); +}); diff --git a/package/ego-browser/src/env.ts b/package/ego-browser/src/env.ts index 3954413a..ea54c963 100644 --- a/package/ego-browser/src/env.ts +++ b/package/ego-browser/src/env.ts @@ -5,22 +5,57 @@ import { fileURLToPath } from "node:url"; export const SRC_DIR = dirname(fileURLToPath(import.meta.url)); export const REPO_ROOT = resolve(SRC_DIR, ".."); -export function agentWorkspace() { - if (process.env.EGO_BROWSER_AGENT_WORKSPACE) { - return resolvePath(process.env.EGO_BROWSER_AGENT_WORKSPACE); +export function agentWorkspace({ + env = process.env, + exists = existsSync, +} = {}) { + if (env.EGO_BROWSER_AGENT_WORKSPACE) { + return resolvePath(env.EGO_BROWSER_AGENT_WORKSPACE); } const bundledSkill = resolve(SRC_DIR, "ego-browser"); - if (existsSync(bundledSkill)) { + if (exists(bundledSkill)) { return bundledSkill; } - return resolve(REPO_ROOT, "..", "..", "skills", "ego-browser"); + const repoSkill = resolve(REPO_ROOT, "..", "..", "skills", "ego-browser"); + if (exists(repoSkill)) { + return repoSkill; + } + + // In the app-bundled runtime neither module-relative candidate exists on + // disk (the skill payload ships elsewhere in the bundle), so the repo-layout + // path resolves to a directory that is not there. Onboarding registers the + // shipped skill at a user-level location; accept it only when it actually + // holds learnings, so a stale leftover directory cannot claim the workspace. + for (const installedSkill of installedSkillCandidates(env)) { + if (exists(resolve(installedSkill, "learnings"))) { + return installedSkill; + } + } + + return repoSkill; +} + +// Onboarding links the shipped skill payload to ~/.local/share/ego/ego-skills; +// depending on the link target that path is the skill directory itself or a +// directory holding an ego-browser/ subdirectory. Resolved against the home +// directory directly so behavior is identical on POSIX and Windows. +function installedSkillCandidates(env) { + const home = env.HOME || env.USERPROFILE; + if (!home) { + return []; + } + const installRoot = resolve(home, ".local", "share", "ego", "ego-skills"); + return [resolve(installRoot, "ego-browser"), installRoot]; } export function resolvePath(path) { if (path.startsWith("~")) { - return resolve(process.env.HOME || process.env.USERPROFILE || ".", path.slice(1)); + return resolve( + process.env.HOME || process.env.USERPROFILE || ".", + path.slice(1), + ); } return resolve(path); } @@ -36,7 +71,10 @@ export function loadEnvFile(path) { } const index = line.indexOf("="); const key = line.slice(0, index).trim(); - const value = line.slice(index + 1).trim().replace(/^['"]|['"]$/g, ""); + const value = line + .slice(index + 1) + .trim() + .replace(/^['"]|['"]$/g, ""); if (key && process.env[key] === undefined) { process.env[key] = value; }