mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(terminal): clear the pane directory when a read says the session has none
applyPaneExits() only applied the manager's directory when it returned a
value, so after a successful read a session that was split into several
panes, or was no longer on the socket, kept its stale cwd. The manager
already keeps its map across a failed or empty read, so the server now
applies its answer as given, undefined included, and setCwd() broadcasts
the clear once.
A blank #{pane_current_path} on a single-pane session (tmux could not read
the process cwd that tick) now carries the previous entry forward in
derivePaneCurrentPaths(), so only a split or unlisted session clears.
The server tests now drive the real TmuxManager with only the tmux read
faked. docs/api-reference.md documents SessionState.cwd.
This commit is contained in:
1 parent
1607b1e488
commit
54c1692350
5 files changed
+146
-21
No files matched your search
+113
-10
@@ -12,8 +12,9 @@
|
||||
* carries `#{pane_current_path}`; `derivePaneCurrentPaths()` turns it into
|
||||
* one directory per session (single-pane sessions only, absolute paths
|
||||
* only).
|
||||
* 2. The manager keeps the last authoritative answer, so a failed read never
|
||||
* blanks a known directory.
|
||||
* 2. The manager keeps the last authoritative answer, so a failed read or a
|
||||
* blank field never blanks a known directory, while a read that lists the
|
||||
* session as split, or not at all, clears it.
|
||||
* 3. Scoping: remote and Docker sessions never carry one (their local pane
|
||||
* runs the ssh / docker-exec CLIENT), and neither does a direct-PTY or a
|
||||
* rediscovered session.
|
||||
@@ -63,6 +64,28 @@ describe('derivePaneCurrentPaths', () => {
|
||||
const rows = parsePaneRows(['codeman-a|100|0|||', 'codeman-b|200|0|||rel/ative'].join('\n'));
|
||||
expect(derivePaneCurrentPaths(rows).size).toBe(0);
|
||||
});
|
||||
|
||||
it('carries the previous directory forward for a blank field on a single-pane session', () => {
|
||||
// A blank `#{pane_current_path}` is a tmux that could not read the process
|
||||
// cwd that tick, not a pane that has no directory.
|
||||
const previous = new Map([['codeman-a', '/work/proj/sub']]);
|
||||
const paths = derivePaneCurrentPaths(parsePaneRows('codeman-a|100|0|||'), previous);
|
||||
expect(paths.get('codeman-a')).toBe('/work/proj/sub');
|
||||
});
|
||||
|
||||
it('carries nothing forward for a split session, an unlisted one or a relative field', () => {
|
||||
// Those are authoritative absences: the read answered, and its answer is
|
||||
// "no single directory".
|
||||
const previous = new Map([
|
||||
['codeman-split', '/one'],
|
||||
['codeman-gone', '/two'],
|
||||
['codeman-rel', '/three'],
|
||||
]);
|
||||
const rows = parsePaneRows(
|
||||
['codeman-split|100|0|||', 'codeman-split|101|0|||', 'codeman-rel|300|0|||rel/ative'].join('\n')
|
||||
);
|
||||
expect(derivePaneCurrentPaths(rows, previous).size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TmuxManager pane directory bookkeeping', () => {
|
||||
@@ -102,6 +125,26 @@ describe('TmuxManager pane directory bookkeeping', () => {
|
||||
expect(manager.getPaneCurrentPath('codeman-s1')).toBe('/work/proj/sub');
|
||||
});
|
||||
|
||||
it('keeps the last directory across a blank field on a single-pane read', async () => {
|
||||
const manager = new TestManager();
|
||||
manager.registerSession(localSession('s1'));
|
||||
manager.rows = parsePaneRows('codeman-s1|100|0|||/work/proj/sub');
|
||||
await manager.refreshPaneExits();
|
||||
manager.rows = parsePaneRows('codeman-s1|100|0|||');
|
||||
await manager.refreshPaneExits();
|
||||
expect(manager.getPaneCurrentPath('codeman-s1')).toBe('/work/proj/sub');
|
||||
});
|
||||
|
||||
it('forgets the directory once the session is split into several panes', async () => {
|
||||
const manager = new TestManager();
|
||||
manager.registerSession(localSession('s1'));
|
||||
manager.rows = parsePaneRows('codeman-s1|100|0|||/work/proj/sub');
|
||||
await manager.refreshPaneExits();
|
||||
manager.rows = parsePaneRows(['codeman-s1|100|0|||/work/proj/sub', 'codeman-s1|101|0|||/work/proj'].join('\n'));
|
||||
await manager.refreshPaneExits();
|
||||
expect(manager.getPaneCurrentPath('codeman-s1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('records the directory even when a respawn races the read', async () => {
|
||||
// A respawn bumps the generation and discards the read's EXIT observation;
|
||||
// the directory the pane was in at read time is still the best answer.
|
||||
@@ -252,17 +295,77 @@ describe('a pane read reaching the session record', () => {
|
||||
expect(persist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the last directory when the read has no answer for the session', () => {
|
||||
const { mux, sessions } = build();
|
||||
/**
|
||||
* Drive the server's REAL TmuxManager: its parsing, its derivation and its
|
||||
* `getPaneCurrentPath()`. Only the tmux exec seam (`readPaneRows`) is faked,
|
||||
* with raw `list-panes -a` output, as the bookkeeping tests above do.
|
||||
*/
|
||||
const buildWithRealManager = () => {
|
||||
const { sessions } = build();
|
||||
const mux = (server as unknown as { mux: TmuxManager }).mux;
|
||||
expect(mux).toBeInstanceOf(TmuxManager);
|
||||
let output = '';
|
||||
(mux as unknown as { readPaneRows: () => Promise<PaneRow[]> }).readPaneRows = async () => parsePaneRows(output);
|
||||
mux.registerSession({
|
||||
sessionId: 'aaaa',
|
||||
muxName: 'codeman-aaaa',
|
||||
pid: 100,
|
||||
createdAt: 0,
|
||||
workingDir: '/work/proj',
|
||||
mode: 'shell',
|
||||
attached: true,
|
||||
} as MuxSession);
|
||||
const session = localMuxSession();
|
||||
sessions.set(session.id, session);
|
||||
mux.getPaneExit = () => undefined;
|
||||
let dir: string | undefined = '/work/proj/sub';
|
||||
mux.getPaneCurrentPath = () => dir;
|
||||
mux.emit('paneExitsUpdated');
|
||||
dir = undefined;
|
||||
mux.emit('paneExitsUpdated');
|
||||
const broadcast = vi.fn();
|
||||
(server as unknown as { broadcastSessionStateDebounced: (id: string) => void }).broadcastSessionStateDebounced =
|
||||
broadcast;
|
||||
const tick = async (listPanes: string) => {
|
||||
output = listPanes;
|
||||
await mux.refreshPaneExits();
|
||||
mux.emit('paneExitsUpdated');
|
||||
};
|
||||
return { session, broadcast, tick };
|
||||
};
|
||||
|
||||
it('clears the directory once the session is split into several panes', async () => {
|
||||
const { session, broadcast, tick } = buildWithRealManager();
|
||||
await tick('codeman-aaaa|100|0|||/work/proj/sub');
|
||||
expect(session.toState().cwd).toBe('/work/proj/sub');
|
||||
expect(broadcast).toHaveBeenCalledTimes(1);
|
||||
|
||||
await tick(['codeman-aaaa|100|0|||/work/proj/sub', 'codeman-aaaa|101|0|||/work/proj'].join('\n'));
|
||||
// An absent cwd is what makes the browser fall back to workingDir.
|
||||
expect(session.toState().cwd).toBeUndefined();
|
||||
expect(broadcast).toHaveBeenCalledTimes(2);
|
||||
|
||||
// The clear is announced once, not on every later tick that agrees.
|
||||
await tick(['codeman-aaaa|100|0|||/work/proj/sub', 'codeman-aaaa|101|0|||/work/proj'].join('\n'));
|
||||
expect(broadcast).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('clears the directory once a read no longer lists the session', async () => {
|
||||
const { session, broadcast, tick } = buildWithRealManager();
|
||||
await tick('codeman-aaaa|100|0|||/work/proj/sub');
|
||||
expect(session.toState().cwd).toBe('/work/proj/sub');
|
||||
|
||||
await tick('codeman-other|300|0|||/elsewhere');
|
||||
expect(session.toState().cwd).toBeUndefined();
|
||||
expect(broadcast).toHaveBeenCalledTimes(2);
|
||||
await tick('codeman-other|300|0|||/elsewhere');
|
||||
expect(broadcast).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('keeps the directory across a blank field and across a failed read', async () => {
|
||||
const { session, broadcast, tick } = buildWithRealManager();
|
||||
await tick('codeman-aaaa|100|0|||/work/proj/sub');
|
||||
// tmux could not read the process cwd for one tick.
|
||||
await tick('codeman-aaaa|100|0|||');
|
||||
expect(session.toState().cwd).toBe('/work/proj/sub');
|
||||
// A failed read arrives as no rows at all.
|
||||
await tick('');
|
||||
expect(session.toState().cwd).toBe('/work/proj/sub');
|
||||
expect(broadcast).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('publishes nothing for a docker case', () => {
|
||||
|
||||
Reference in new issue
Block a user