Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions scripts/cowork-vm-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ function buildMountMap(additionalMounts, mountBinds) {
return map;
}

/**
* Find the primary user mount name in mountMap — the first key that
* is not a dotfile mount (e.g. .claude, .auto-memory) and not the
* uploads mount. Used by both resolveWorkDir (HostBackend) and
* BwrapBackend.spawn to derive a sensible cwd from the user-selected
* project folder when the Electron app sends a session-root path with
* no /mnt/{name} component to translate.
*
* Returns the mount name (string) or null if no user mount exists.
*/
function findPrimaryMount(mountMap) {
if (!mountMap) return null;
return Object.keys(mountMap).find(
n => !n.startsWith('.') && n !== 'uploads',
) || null;
}

/**
* Build a merged environment for a spawned process. Combines filtered
* daemon env with app-provided env, and translates guest paths in
Expand Down Expand Up @@ -392,8 +409,18 @@ function resolveWorkDir(cwd, sharedCwdPath, mountMap) {
log(`resolveWorkDir: translated "${cwd}" -> "${translated}"`);
workDir = translated;
} else {
log(`resolveWorkDir: cwd is VM guest path "${cwd}", using home dir`);
workDir = os.homedir();
// Session-root path (e.g. /sessions/bold-sharp-clarke) has no
// /mnt/ component, so translateGuestPath can't resolve it.
// Derive cwd from the primary user mount, mirroring what
// BwrapBackend does at spawn time.
const primaryMount = findPrimaryMount(mountMap);
if (primaryMount && mountMap[primaryMount]) {
log(`resolveWorkDir: session root "${cwd}", using primary mount "${primaryMount}" -> "${mountMap[primaryMount]}"`);
workDir = mountMap[primaryMount];
} else {
log(`resolveWorkDir: cwd is VM guest path "${cwd}", no primary mount found, using home dir`);
workDir = os.homedir();
}
}
}

Expand Down Expand Up @@ -1145,9 +1172,7 @@ class BwrapBackend extends LocalBackend {
);

// Use the primary user mount as cwd (first non-dotfile, non-uploads mount)
const primaryMount = Object.keys(mountMap).find(
n => !n.startsWith('.') && n !== 'uploads',
);
const primaryMount = findPrimaryMount(mountMap);
const guestWorkDir = primaryMount
? `${sessionMnt}/${primaryMount}`
: sessionMnt;
Expand Down
118 changes: 117 additions & 1 deletion tests/cowork-path-translation.bats
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ function cleanSpawnArgs(rawArgs, mountMap) {
return cleanArgs;
}

function findPrimaryMount(mountMap) {
if (!mountMap) return null;
return Object.keys(mountMap).find(
n => !n.startsWith(".") && n !== "uploads",
) || null;
}

function resolveWorkDir(cwd, sharedCwdPath, mountMap) {
let workDir = cwd || os.homedir();
if (sharedCwdPath) {
Expand All @@ -134,7 +141,12 @@ function resolveWorkDir(cwd, sharedCwdPath, mountMap) {
if (translated) {
workDir = translated;
} else {
workDir = os.homedir();
const primaryMount = findPrimaryMount(mountMap);
if (primaryMount && mountMap[primaryMount]) {
workDir = mountMap[primaryMount];
} else {
workDir = os.homedir();
}
}
}
if (!fs.existsSync(workDir)) {
Expand Down Expand Up @@ -711,6 +723,110 @@ assertEqual(
[[ "$status" -eq 0 ]]
}

@test "resolveWorkDir: session-root cwd uses primary user mount" {
mkdir -p "${TEST_TMP}/project"

run node -e "${NODE_PREAMBLE}
assertEqual(
resolveWorkDir(
'/sessions/bold-sharp-clarke',
null,
{'project': '${TEST_TMP}/project'}
),
'${TEST_TMP}/project',
'session-root falls through to primary mount');
"
[[ "$status" -eq 0 ]]
}

@test "resolveWorkDir: session-root cwd skips dotfile and uploads mounts" {
mkdir -p "${TEST_TMP}/project"

run node -e "${NODE_PREAMBLE}
assertEqual(
resolveWorkDir(
'/sessions/abc',
null,
{
'.claude': '${TEST_TMP}/dotclaude',
'.auto-memory': '${TEST_TMP}/automem',
'uploads': '${TEST_TMP}/uploads',
'project': '${TEST_TMP}/project'
}
),
'${TEST_TMP}/project',
'dotfile and uploads mounts are skipped');
"
[[ "$status" -eq 0 ]]
}

@test "resolveWorkDir: session-root cwd with no user mount falls back to home" {
run node -e "${NODE_PREAMBLE}
assertEqual(
resolveWorkDir(
'/sessions/abc',
null,
{
'.claude': '/host/dotclaude',
'uploads': '/host/uploads'
}
),
os.homedir(),
'no user mount -> homedir fallback');
"
[[ "$status" -eq 0 ]]
}

# =============================================================================
# findPrimaryMount
# =============================================================================

@test "findPrimaryMount: returns null for null mountMap" {
run node -e "${NODE_PREAMBLE}
assert(findPrimaryMount(null) === null, 'null mountMap');
assert(findPrimaryMount(undefined) === null, 'undefined mountMap');
assert(findPrimaryMount({}) === null, 'empty mountMap');
"
[[ "$status" -eq 0 ]]
}

@test "findPrimaryMount: returns first non-dotfile non-uploads key" {
run node -e "${NODE_PREAMBLE}
assertEqual(
findPrimaryMount({'project': '/h/p'}),
'project',
'single user mount');
assertEqual(
findPrimaryMount({
'.claude': '/h/c',
'uploads': '/h/u',
'project': '/h/p'
}),
'project',
'skips dotfiles and uploads');
"
[[ "$status" -eq 0 ]]
}

@test "findPrimaryMount: returns null when all mounts are dotfiles or uploads" {
run node -e "${NODE_PREAMBLE}
assert(
findPrimaryMount({'.claude': '/h/c', 'uploads': '/h/u'}) === null,
'no user mount -> null');
"
[[ "$status" -eq 0 ]]
}

@test "findPrimaryMount: insertion order determines primary when multiple exist" {
run node -e "${NODE_PREAMBLE}
assertEqual(
findPrimaryMount({'first': '/h/1', 'second': '/h/2'}),
'first',
'first inserted user mount wins');
"
[[ "$status" -eq 0 ]]
}

# =============================================================================
# buildSpawnEnv
# =============================================================================
Expand Down
Loading