This commit is contained in:
@@ -164,6 +164,22 @@ describe('ConsoleSession', () => {
|
||||
expect(audit.beginAndComplete.mock.calls[0]![0].detail.reason).toBe('host_disconnect');
|
||||
});
|
||||
|
||||
it('tracks last output / ai-input / human-input timestamps separately', () => {
|
||||
const ch = new StubChannel();
|
||||
const { session } = mkSession(ch);
|
||||
const t0 = session.lastOutputAt;
|
||||
// simulate output
|
||||
(session as any).handleOutput(Buffer.from('hi'));
|
||||
expect(session.lastOutputAt).toBeGreaterThanOrEqual(t0);
|
||||
session.write(Buffer.from('a'), 'ai');
|
||||
expect(session.lastAiInputAt).toBeGreaterThan(0);
|
||||
const aiAt = session.lastAiInputAt;
|
||||
session.write(Buffer.from('b'), 'human');
|
||||
expect(session.lastHumanInputAt).toBeGreaterThan(0);
|
||||
expect(session.lastAiInputAt).toBe(aiAt); // human write must not move ai timestamp
|
||||
expect(typeof session.idleMs()).toBe('number');
|
||||
});
|
||||
|
||||
it('client "close" tears the session down once', async () => {
|
||||
const ch = new StubChannel();
|
||||
const { session, audit, client } = mkSession(ch);
|
||||
|
||||
@@ -121,6 +121,9 @@ export class ConsoleSession {
|
||||
private readonly auditRepo: SshAuditRepo;
|
||||
|
||||
private _lastActivityAt: number;
|
||||
private _lastOutputAt: number;
|
||||
private _lastAiInputAt = 0;
|
||||
private _lastHumanInputAt = 0;
|
||||
private _totalInputBytes = 0;
|
||||
private _totalOutputBytes = 0;
|
||||
|
||||
@@ -136,6 +139,7 @@ export class ConsoleSession {
|
||||
this.startedByUserId = args.startedByUserId;
|
||||
this.startedAt = Date.now();
|
||||
this._lastActivityAt = this.startedAt;
|
||||
this._lastOutputAt = this.startedAt;
|
||||
this.cols = args.cols;
|
||||
this.rows = args.rows;
|
||||
this.channel = args.channel;
|
||||
@@ -181,6 +185,18 @@ export class ConsoleSession {
|
||||
get lastActivityAt(): number {
|
||||
return this._lastActivityAt;
|
||||
}
|
||||
get lastOutputAt(): number {
|
||||
return this._lastOutputAt;
|
||||
}
|
||||
get lastAiInputAt(): number {
|
||||
return this._lastAiInputAt;
|
||||
}
|
||||
get lastHumanInputAt(): number {
|
||||
return this._lastHumanInputAt;
|
||||
}
|
||||
idleMs(): number {
|
||||
return Date.now() - this._lastOutputAt;
|
||||
}
|
||||
get totalInputBytes(): number {
|
||||
return this._totalInputBytes;
|
||||
}
|
||||
@@ -239,6 +255,8 @@ export class ConsoleSession {
|
||||
// Partial input (no newline) is forwarded so the shell can echo each
|
||||
// character back, matching the live terminal experience the user
|
||||
// expects in either role.
|
||||
if (source === 'ai') this._lastAiInputAt = Date.now();
|
||||
else this._lastHumanInputAt = Date.now();
|
||||
const out = source === 'ai' ? normalizeLfToCr(buf) : buf;
|
||||
this._totalInputBytes += out.length;
|
||||
const ok = this.channel.write(out);
|
||||
@@ -338,6 +356,7 @@ export class ConsoleSession {
|
||||
private handleOutput(data: Buffer): void {
|
||||
this._totalOutputBytes += data.length;
|
||||
this._lastActivityAt = Date.now();
|
||||
this._lastOutputAt = Date.now();
|
||||
this.scrollback.append(data);
|
||||
this.writeToHeadlessSync(data);
|
||||
for (const l of this.outputListeners) {
|
||||
|
||||
Reference in New Issue
Block a user