Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
44036f4
feat(cli): add agent view supervisor runtime
Jul 22, 2026
1b994c1
feat(cli): add agent view pty workers
Jul 22, 2026
b469884
fix(cli): harden agent view supervisor runtime
Jul 28, 2026
f101d28
fix(cli): harden agent view pty workers
Jul 28, 2026
c84e1dc
fix(cli): surface supervisor startup failures
Jul 28, 2026
eca1fc4
fix(cli): handle supervisor subscription failures
Jul 28, 2026
5217c6f
Merge branch 'main' into agent/agent-view-supervisor-runtime
qwen-code-dev-bot Jul 28, 2026
ee693b3
fix(cli): address review feedback on agent-view supervisor (#7799)
Jul 28, 2026
d4c3f0f
Merge branch 'main' into agent/agent-view-supervisor-runtime
qwen-code-dev-bot Jul 28, 2026
214a1cf
fix(cli): address review feedback on agent-view supervisor (#7799)
qwen-code-ci-bot Jul 28, 2026
a901609
fix(cli): address review feedback on agent-view supervisor (#7799)
qwen-code-ci-bot Jul 28, 2026
e149bc1
fix(cli): address review feedback on agent-view supervisor (#7799)
Jul 28, 2026
069b78b
Merge branch 'main' into agent/agent-view-supervisor-runtime
qwen-code-dev-bot Jul 28, 2026
b3caf9b
Merge branch 'main' into agent/agent-view-supervisor-runtime
ZijianZhang989 Jul 29, 2026
4e9ac53
fix(cli): bind streaming supervisor handlers and keep attach stream 8…
qwen-code-ci-bot Jul 30, 2026
437522e
fix(cli): pause attach socket during leftover write and harden error …
Jul 30, 2026
f263b18
fix(cli): harden supervisor shutdown tests and clean up signal listen…
Jul 30, 2026
acef632
fix(cli): guard fire-and-forget promises and add supervisor timeout t…
qwen-code-ci-bot Jul 30, 2026
6302b8e
fix(cli): normalize socket path test prefix and force credential file…
qwen-code-ci-bot Jul 30, 2026
e5f150a
fix(cli): address agent view runtime review feedback
Jul 31, 2026
4a9aa30
test(cli): cover agent view bridge edge cases
Jul 31, 2026
09f6d44
test(core): isolate worktree symlink traversal fixture
Jul 31, 2026
678f884
Revert "test(core): isolate worktree symlink traversal fixture"
Jul 31, 2026
5b69eae
Merge branch 'main' into agent/agent-view-supervisor-runtime
qwen-code-dev-bot Jul 31, 2026
5776467
fix(cli): harden agent view supervisor store, auth, and streaming (#7…
qwen-code-dev-bot Jul 31, 2026
9203dbd
fix(cli): harden agent view store joins, writes, and coverage (#7799)
qwen-code-dev-bot Jul 31, 2026
1bcf793
fix(cli): harden agent view credential writes and subscription handsh…
qwen-code-dev-bot Jul 31, 2026
1ae6b75
fix(cli): harden agent view store normalization, roster safety, and b…
Aug 1, 2026
0264edb
fix(cli): guard response serialization in supervisor server (#7799)
qwen-code-ci-bot Aug 1, 2026
0dd7980
fix(cli): harden agent view pty host teardown
Aug 3, 2026
b10135f
fix(cli): fail fast on pty host auth rejection
Aug 3, 2026
f0ca335
Merge remote-tracking branch 'origin/agent/agent-view-supervisor-runt…
Aug 3, 2026
22280ee
test(cli): cover pty host remote exit polling
Aug 3, 2026
e811cb4
fix(cli): address agent view pty worker review nits
Aug 5, 2026
d1eb5d5
Merge remote-tracking branch 'origin/main' into agent/agent-view-pty-…
Aug 5, 2026
79df20e
fix(cli): harden pty host socket fallback
Aug 5, 2026
bcc08f4
Merge remote-tracking branch 'origin/main' into agent/agent-view-pty-…
Aug 6, 2026
016322c
fix(cli): harden agent view pty workers
Aug 6, 2026
c96c934
fix(cli): harden agent view pty workers
Aug 6, 2026
8c76fd2
test(cli): cover pty host spawn contract
Aug 6, 2026
c3a6016
fix(cli): guard agent view pty host socket takeover and races
qwen-code-ci-bot Aug 8, 2026
6b85924
fix(cli): fit agent view pty logs under wire cap, strip host token af…
qwen-code-ci-bot Aug 8, 2026
b2deec7
Merge branch 'main' into agent/agent-view-pty-workers
wenshao Aug 8, 2026
eca1f8f
fix(cli): harden pty host fallback selection
Aug 9, 2026
f002b19
fix(cli): address agent view worker review blockers
Aug 10, 2026
da92f43
fix(cli): bound attach lease ttl and surface pty host input/listen fa…
Aug 10, 2026
e94dde0
fix(cli): serialize pty host socket ownership and keep attach transpo…
Aug 10, 2026
012c4db
fix(cli): escalate pty host shutdown and harden socket path fallback
Aug 11, 2026
b61917a
fix(cli): harden win32 kill, decoder reset, and exit tracker
Aug 11, 2026
244d162
fix(cli): skip POSIX signal tests on Windows and reclaim empty lock f…
Aug 11, 2026
07513f9
fix(cli): remove abort listener on delay timeout path
Aug 12, 2026
93fad08
fix(cli): don't optimistically resolve exited on SIGINT for remote ha…
Aug 12, 2026
22f610c
Merge remote-tracking branch 'origin/main' into codex/qwen-code-pr-7800
Aug 12, 2026
c56cf4a
test(cli): add SIGINT carve-out test for remote handle kill
Aug 12, 2026
49fe189
fix(cli): Only resolve exited on SIGKILL for remote handle kill
Aug 13, 2026
19e1c3d
Merge remote-tracking branch 'origin/main' into codex/qwen-code-pr-7800
Aug 13, 2026
8a3f89f
fix(cli): harden agent view PTY host socket lock and kill default
Aug 13, 2026
99c493f
Merge remote-tracking branch 'origin/main' into codex/qwen-code-pr-7800
Aug 13, 2026
640d437
fix(cli): fix agent view PTY host dispose signal, ring trim, and lock…
Aug 14, 2026
38b1073
fix(cli): settle remote pty host exit only after the RPC lands
Aug 14, 2026
7e8d984
fix(cli): optional-call shutdown in the lost-RPC regression test
Aug 14, 2026
8572f36
Merge remote-tracking branch 'origin/main' into codex/qwen-code-pr-7800
Aug 17, 2026
3c63475
fix(cli): wait for PTY host endpoint shutdown
yiliang114 Aug 19, 2026
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
257 changes: 257 additions & 0 deletions packages/cli/src/agent-view/attach-lease.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import {
AgentViewAttachLeaseManager,
MAX_AGENT_VIEW_ATTACH_LEASE_TTL_MS,
} from './attach-lease.js';

describe('AgentViewAttachLeaseManager', () => {
it('acquires a lease for an unattached session', () => {
const clock = fakeClock('2026-07-17T00:00:00.000Z');
const manager = new AgentViewAttachLeaseManager({
now: clock.now,
createLeaseId: () => 'lease-1',
defaultTtlMs: 1000,
});

expect(manager.acquire('session-1', { clientId: 'terminal-1' })).toEqual({
ok: true,
lease: {
sessionId: 'session-1',
leaseId: 'lease-1',
clientId: 'terminal-1',
acquiredAt: '2026-07-17T00:00:00.000Z',
lastHeartbeatAt: '2026-07-17T00:00:00.000Z',
expiresAt: '2026-07-17T00:00:01.000Z',
},
});
});

it('uses a random lease id when no id factory is provided', () => {
const manager = new AgentViewAttachLeaseManager();
const result = manager.acquire('session-1');

expect(result).toMatchObject({
ok: true,
lease: {
sessionId: 'session-1',
},
});
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
if (result.ok) {
expect(result.lease.leaseId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
}
});

it('isolates leases across sessions', () => {
const clock = fakeClock('2026-07-17T00:00:00.000Z');
let idCounter = 0;
const manager = new AgentViewAttachLeaseManager({
now: clock.now,
createLeaseId: () => `lease-${++idCounter}`,
});

const first = manager.acquire('session-a', { clientId: 'terminal-a' });
const second = manager.acquire('session-b', { clientId: 'terminal-b' });

expect(first.ok && first.lease.leaseId).toBe('lease-1');
expect(second.ok && second.lease.leaseId).toBe('lease-2');

// Session B's lease must not disturb session A's.
expect(manager.get('session-a')?.leaseId).toBe('lease-1');
expect(manager.heartbeat('session-a', 'lease-1')?.leaseId).toBe('lease-1');

// Releasing session A must not affect session B.
expect(manager.release('session-a', 'lease-1')).toBe(true);
expect(manager.get('session-a')).toBeUndefined();
expect(manager.get('session-b')?.leaseId).toBe('lease-2');
});

it('rejects a second acquire while a lease is active', () => {
const clock = fakeClock('2026-07-17T00:00:00.000Z');
const manager = new AgentViewAttachLeaseManager({
now: clock.now,
createLeaseId: () => 'lease-1',
});
const first = manager.acquire('session-1');

expect(
manager.acquire('session-1', {
leaseId: 'lease-2',
}),
).toEqual({
ok: false,
reason: 'already_attached',
lease: {
sessionId: first.lease.sessionId,
acquiredAt: first.lease.acquiredAt,
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
lastHeartbeatAt: first.lease.lastHeartbeatAt,
expiresAt: first.lease.expiresAt,
},
});
});

it('does not disclose the active lease id on contested acquire', () => {
const manager = new AgentViewAttachLeaseManager({
createLeaseId: () => 'lease-1',
});
manager.acquire('session-1');

const result = manager.acquire('session-1', { leaseId: 'lease-2' });

expect(result.ok).toBe(false);
if (!result.ok) {
expect('leaseId' in result.lease).toBe(false);
}
});

it('generates a lease id when the provided id is empty', () => {
const manager = new AgentViewAttachLeaseManager({
createLeaseId: () => 'generated-lease',
});

expect(manager.acquire('session-1', { leaseId: '' })).toMatchObject({
ok: true,
lease: {
leaseId: 'generated-lease',
},
});
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
});

it('releases only the matching lease', () => {
const manager = new AgentViewAttachLeaseManager({
createLeaseId: () => 'lease-1',
});
manager.acquire('session-1');

expect(manager.release('session-1', 'other-lease')).toBe(false);
expect(manager.get('session-1')).toMatchObject({ leaseId: 'lease-1' });
expect(manager.release('session-1', 'lease-1')).toBe(true);
expect(manager.get('session-1')).toBeUndefined();
});

it('rejects non-positive, oversized, and non-finite ttls', () => {
const manager = new AgentViewAttachLeaseManager();

expect(() => manager.acquire('session-1', { ttlMs: 0 })).toThrow(
'Attach lease ttlMs must be positive.',
);
expect(() => manager.acquire('session-1', { ttlMs: Number.NaN })).toThrow(
RangeError,
);
expect(() =>
manager.acquire('session-1', {
ttlMs: MAX_AGENT_VIEW_ATTACH_LEASE_TTL_MS + 1,
}),
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
).toThrow('Attach lease ttlMs must not exceed');
expect(() => manager.acquire('')).toThrow(
'Agent View session id is required.',
);
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
expect(manager.get('session-1')).toBeUndefined();
});

it('honors per-call ttls and expires lazily through get and release', () => {
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
const clock = fakeClock('2026-07-17T00:00:00.000Z');
const manager = new AgentViewAttachLeaseManager({
now: clock.now,
createLeaseId: () => 'lease-1',
defaultTtlMs: 1000,
});
manager.acquire('session-1', { ttlMs: 2000 });

clock.advance(1500);
expect(manager.get('session-1')).toMatchObject({ leaseId: 'lease-1' });
expect(
manager.heartbeat('session-1', 'lease-1', { ttlMs: 3000 }),
).toMatchObject({ expiresAt: '2026-07-17T00:00:04.500Z' });

clock.advance(3000);
expect(manager.get('session-1')).toBeUndefined();
expect(manager.release('session-1', 'lease-1')).toBe(false);
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
});

it('expires stale leases and allows reacquire', () => {
const clock = fakeClock('2026-07-17T00:00:00.000Z');
const leaseIds = ['lease-1', 'lease-2'];
const manager = new AgentViewAttachLeaseManager({
now: clock.now,
createLeaseId: () => leaseIds.shift() ?? 'missing-lease',
defaultTtlMs: 1000,
});
const first = manager.acquire('session-1');

clock.advance(1000);

expect(manager.expire()).toEqual([first.lease]);
expect(manager.acquire('session-1')).toMatchObject({
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
ok: true,
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
lease: {
leaseId: 'lease-2',
acquiredAt: '2026-07-17T00:00:01.000Z',
},
});
});

it('expires corrupted leases with invalid timestamps', () => {
const manager = new AgentViewAttachLeaseManager({
createLeaseId: () => 'lease-1',
});
manager.acquire('session-1');
(
manager as unknown as {
leases: Map<string, { expiresAt: string }>;
}
).leases.get('session-1')!.expiresAt = 'not-a-date';

expect(manager.expire()).toEqual([
expect.objectContaining({
sessionId: 'session-1',
leaseId: 'lease-1',
expiresAt: 'not-a-date',
}),
]);
expect(manager.get('session-1')).toBeUndefined();
});

it('heartbeat extends the matching lease', () => {
const clock = fakeClock('2026-07-17T00:00:00.000Z');
const manager = new AgentViewAttachLeaseManager({
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
now: clock.now,
createLeaseId: () => 'lease-1',
defaultTtlMs: 1000,
});
manager.acquire('session-1');
clock.advance(500);

expect(manager.heartbeat('session-1', 'wrong-lease')).toBeUndefined();
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
expect(manager.heartbeat('session-1', 'lease-1')).toMatchObject({
sessionId: 'session-1',
leaseId: 'lease-1',
acquiredAt: '2026-07-17T00:00:00.000Z',
lastHeartbeatAt: '2026-07-17T00:00:00.500Z',
expiresAt: '2026-07-17T00:00:01.500Z',
});

clock.advance(999);
expect(manager.get('session-1')).toMatchObject({ leaseId: 'lease-1' });
});
});

function fakeClock(start: string): {
now: () => Date;
advance: (ms: number) => void;
} {
let nowMs = Date.parse(start);
return {
now: () => new Date(nowMs),
advance: (ms: number) => {
nowMs += ms;
},
};
}
Loading
Loading