Skip to content

Commit 3c29926

Browse files
committed
fix workspace bug
1 parent 472d5ae commit 3c29926

File tree

3 files changed

+628
-2
lines changed

3 files changed

+628
-2
lines changed

packages/sandbox-container/src/session.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
import { randomUUID } from 'node:crypto';
2727
import { watch } from 'node:fs';
28-
import { mkdir, rm } from 'node:fs/promises';
28+
import { mkdir, rm, stat } from 'node:fs/promises';
2929
import { tmpdir } from 'node:os';
3030
import { basename, dirname, join } from 'node:path';
3131
import type { ExecEvent, Logger } from '@repo/shared';
@@ -137,10 +137,29 @@ export class Session {
137137
this.sessionDir = join(tmpdir(), `session-${this.id}-${Date.now()}`);
138138
await mkdir(this.sessionDir, { recursive: true });
139139

140+
// Determine working directory and verify it exists to avoid spawn failures
141+
// If /workspace or specified cwd doesn't exist, fall back to /
142+
// This handles the case where /workspace is deleted
143+
let cwd = this.options.cwd || CONFIG.DEFAULT_CWD;
144+
try {
145+
await stat(cwd);
146+
} catch {
147+
// Directory doesn't exist, fall back to root
148+
this.logger.warn(
149+
`Working directory '${cwd}' does not exist, falling back to '/'`,
150+
{
151+
sessionId: this.id,
152+
requestedCwd: cwd,
153+
fallbackCwd: '/'
154+
}
155+
);
156+
cwd = '/';
157+
}
158+
140159
// Spawn persistent bash with stdin pipe - no IPC or wrapper needed!
141160
this.shell = Bun.spawn({
142161
cmd: ['bash', '--norc'],
143-
cwd: this.options.cwd || CONFIG.DEFAULT_CWD,
162+
cwd,
144163
env: {
145164
...process.env,
146165
...this.options.env,

packages/sandbox-container/tests/session.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,52 @@ describe('Session', () => {
7575
// Session directory should be created (we can't easily check without accessing private fields)
7676
expect(session.isReady()).toBe(true);
7777
});
78+
79+
it('should initialize with non-existent cwd by falling back to / (issue #288)', async () => {
80+
// This tests the fix for issue #288 where session creation failed
81+
// if /workspace was deleted before a new session was created
82+
session = new Session({
83+
id: 'test-session-nonexistent-cwd',
84+
cwd: '/nonexistent/path/that/does/not/exist'
85+
});
86+
87+
// This should NOT throw - instead it should fall back to /
88+
await session.initialize();
89+
90+
expect(session.isReady()).toBe(true);
91+
92+
// Verify we can execute commands
93+
const result = await session.exec('pwd');
94+
expect(result.exitCode).toBe(0);
95+
// The shell should have started in / since the requested cwd doesn't exist
96+
expect(result.stdout.trim()).toBe('/');
97+
});
98+
99+
it('should initialize with missing /workspace by falling back to / (issue #288)', async () => {
100+
// Simulate the exact scenario from issue #288
101+
// Create a workspace, then delete it, then try to create a session with it
102+
const workspaceDir = join(testDir, 'workspace');
103+
await mkdir(workspaceDir, { recursive: true });
104+
105+
// Delete the workspace
106+
await rm(workspaceDir, { recursive: true, force: true });
107+
108+
// Now try to create a session with the deleted workspace as cwd
109+
session = new Session({
110+
id: 'test-session-deleted-workspace',
111+
cwd: workspaceDir
112+
});
113+
114+
// This should NOT throw - instead it should fall back to /
115+
await session.initialize();
116+
117+
expect(session.isReady()).toBe(true);
118+
119+
// Verify we can execute commands
120+
const result = await session.exec('echo "session works"');
121+
expect(result.exitCode).toBe(0);
122+
expect(result.stdout.trim()).toBe('session works');
123+
});
78124
});
79125

80126
describe('exec', () => {
@@ -496,6 +542,92 @@ describe('Session', () => {
496542
expect(result.exitCode).toBe(1);
497543
expect(result.stderr).toContain('Failed to change directory');
498544
});
545+
546+
it('should continue working after session cwd is deleted (issue #288)', async () => {
547+
// Create a working directory for the session
548+
const workspaceDir = join(testDir, 'workspace');
549+
await mkdir(workspaceDir, { recursive: true });
550+
551+
session = new Session({
552+
id: 'test-cwd-deletion',
553+
cwd: workspaceDir
554+
});
555+
556+
await session.initialize();
557+
558+
// Verify baseline works
559+
const baseline = await session.exec('echo "baseline"');
560+
expect(baseline.exitCode).toBe(0);
561+
expect(baseline.stdout.trim()).toBe('baseline');
562+
563+
// Delete the workspace directory (this is the bug scenario)
564+
await session.exec(`rm -rf ${workspaceDir}`);
565+
566+
// Try a subsequent command - this should NOT fail with an obscure error
567+
// It should either work (falling back to /) or give a clear error message
568+
const afterRemoval = await session.exec('echo "after removal"');
569+
570+
// The command should succeed - bash can still run commands even if cwd is deleted
571+
// It will use the deleted directory's inode until a cd happens
572+
expect(afterRemoval.exitCode).toBe(0);
573+
expect(afterRemoval.stdout.trim()).toBe('after removal');
574+
});
575+
576+
it('should handle cwd being replaced with symlink (issue #288)', async () => {
577+
// Create directories for the test
578+
const workspaceDir = join(testDir, 'workspace');
579+
const backupDir = join(testDir, 'backup');
580+
await mkdir(workspaceDir, { recursive: true });
581+
await mkdir(backupDir, { recursive: true });
582+
583+
session = new Session({
584+
id: 'test-cwd-symlink',
585+
cwd: workspaceDir
586+
});
587+
588+
await session.initialize();
589+
590+
// Verify baseline works
591+
const baseline = await session.exec('echo "baseline"');
592+
expect(baseline.exitCode).toBe(0);
593+
expect(baseline.stdout.trim()).toBe('baseline');
594+
595+
// Replace workspace with a symlink to backup directory
596+
await session.exec(
597+
`rm -rf ${workspaceDir} && ln -sf ${backupDir} ${workspaceDir}`
598+
);
599+
600+
// Try a subsequent command - should continue working
601+
const afterSymlink = await session.exec('echo "after symlink"');
602+
expect(afterSymlink.exitCode).toBe(0);
603+
expect(afterSymlink.stdout.trim()).toBe('after symlink');
604+
});
605+
606+
it('should be recoverable by cd-ing to valid directory after cwd deletion', async () => {
607+
// Create a working directory for the session
608+
const workspaceDir = join(testDir, 'workspace');
609+
await mkdir(workspaceDir, { recursive: true });
610+
611+
session = new Session({
612+
id: 'test-cwd-recovery',
613+
cwd: workspaceDir
614+
});
615+
616+
await session.initialize();
617+
618+
// Delete the workspace directory
619+
await session.exec(`rm -rf ${workspaceDir}`);
620+
621+
// Change to a valid directory - this should work and recover the session
622+
const cdResult = await session.exec('cd /tmp && pwd');
623+
expect(cdResult.exitCode).toBe(0);
624+
expect(cdResult.stdout.trim()).toBe('/tmp');
625+
626+
// Subsequent commands should work
627+
const afterCd = await session.exec('echo "recovered"');
628+
expect(afterCd.exitCode).toBe(0);
629+
expect(afterCd.stdout.trim()).toBe('recovered');
630+
});
499631
});
500632

501633
describe('FIFO cleanup', () => {

0 commit comments

Comments
 (0)