Guard terminal recovery against cross-session data
This commit is contained in:
parent
7b401eb3f6
commit
2b2523d65f
@ -711,6 +711,7 @@ class TerminalSessionCoordinator extends ChangeNotifier {
|
|||||||
(item) =>
|
(item) =>
|
||||||
_JournalItem.fromJson(Map<String, dynamic>.from(item as Map)),
|
_JournalItem.fromJson(Map<String, dynamic>.from(item as Map)),
|
||||||
)
|
)
|
||||||
|
.where((item) => item.sessionId == session.sessionId)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -179,7 +179,9 @@ class TerminalSocketSession {
|
|||||||
bool _handleAttachedAck(String frame) {
|
bool _handleAttachedAck(String frame) {
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(frame);
|
final decoded = jsonDecode(frame);
|
||||||
if (decoded is Map && decoded['type'] == 'attached') {
|
if (decoded is Map &&
|
||||||
|
decoded['type'] == 'attached' &&
|
||||||
|
_matchesSessionId(decoded)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@ -193,7 +195,9 @@ class TerminalSocketSession {
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(frame);
|
final decoded = jsonDecode(frame);
|
||||||
if (decoded is Map && decoded['type'] == 'restore') {
|
if (decoded is Map &&
|
||||||
|
decoded['type'] == 'restore' &&
|
||||||
|
_matchesSessionId(decoded)) {
|
||||||
onRestore(
|
onRestore(
|
||||||
TerminalRestorePayload.fromJson(Map<String, dynamic>.from(decoded)),
|
TerminalRestorePayload.fromJson(Map<String, dynamic>.from(decoded)),
|
||||||
);
|
);
|
||||||
@ -207,7 +211,9 @@ class TerminalSocketSession {
|
|||||||
TerminalOutputPayload? _decodeOutputFrame(String frame) {
|
TerminalOutputPayload? _decodeOutputFrame(String frame) {
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(frame);
|
final decoded = jsonDecode(frame);
|
||||||
if (decoded is Map && decoded['type'] == 'output') {
|
if (decoded is Map &&
|
||||||
|
decoded['type'] == 'output' &&
|
||||||
|
_matchesSessionId(decoded)) {
|
||||||
return TerminalOutputPayload.fromJson(
|
return TerminalOutputPayload.fromJson(
|
||||||
Map<String, dynamic>.from(decoded),
|
Map<String, dynamic>.from(decoded),
|
||||||
);
|
);
|
||||||
@ -225,6 +231,11 @@ class TerminalSocketSession {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _matchesSessionId(Map decoded) {
|
||||||
|
final messageSessionId = decoded['sessionId'];
|
||||||
|
return messageSessionId is String && messageSessionId == sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTransportClosed(TerminalSocketTransport? transport) {
|
void _handleTransportClosed(TerminalSocketTransport? transport) {
|
||||||
if (transport == null) {
|
if (transport == null) {
|
||||||
_subscription = null;
|
_subscription = null;
|
||||||
|
|||||||
@ -784,6 +784,62 @@ void main() {
|
|||||||
expect(history!.outputSeedText, 'header\r\nmiddle\r\ntail');
|
expect(history!.outputSeedText, 'header\r\nmiddle\r\ntail');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'loadRecentHistoryWindow ignores journal items from a different session',
|
||||||
|
() async {
|
||||||
|
final controller = TerminalInteractionController();
|
||||||
|
controller.applyFrame('skip-initial-history-load');
|
||||||
|
final apiClient = _FakeAgentApiClient(
|
||||||
|
journalResponses: [
|
||||||
|
<String, dynamic>{
|
||||||
|
'sessionId': 'abc',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'sessionId': 'other-session',
|
||||||
|
'sequence': 5,
|
||||||
|
'kind': 'output',
|
||||||
|
'payload': 'wrong-output\r\n',
|
||||||
|
'timestampUtc': '2026-04-07T03:20:05Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'sessionId': 'abc',
|
||||||
|
'sequence': 6,
|
||||||
|
'kind': 'output',
|
||||||
|
'payload': 'right-output\r\n',
|
||||||
|
'timestampUtc': '2026-04-07T03:20:06Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'hasMoreBefore': false,
|
||||||
|
'hasMoreAfter': false,
|
||||||
|
'currentSequence': 6,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final sessionFactory = _FakeTerminalSessionFactory();
|
||||||
|
final session = Session(
|
||||||
|
sessionId: 'abc',
|
||||||
|
name: 'codex-main',
|
||||||
|
status: 'idle',
|
||||||
|
);
|
||||||
|
final coordinator = TerminalSessionCoordinator(
|
||||||
|
controller: controller,
|
||||||
|
apiClient: apiClient,
|
||||||
|
session: session,
|
||||||
|
sessionFactory: sessionFactory.create,
|
||||||
|
onFrame: (_) {},
|
||||||
|
onRestore: (_) {},
|
||||||
|
viewportProvider: () => const TerminalViewport(columns: 80, rows: 24),
|
||||||
|
);
|
||||||
|
|
||||||
|
await coordinator.start();
|
||||||
|
final history = await coordinator.loadRecentHistoryWindow();
|
||||||
|
|
||||||
|
expect(history, isNotNull);
|
||||||
|
expect(history!.outputSeedText, 'right-output\r\n');
|
||||||
|
expect(history.lines, ['[output] right-output']);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FakeAgentApiClient extends AgentApiClient {
|
class _FakeAgentApiClient extends AgentApiClient {
|
||||||
|
|||||||
@ -220,6 +220,37 @@ void main() {
|
|||||||
|
|
||||||
expect(outputs, isEmpty);
|
expect(outputs, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('connect ignores restore and output frames for a different session', () async {
|
||||||
|
final transport = _FakeTerminalSocketTransport();
|
||||||
|
final session = TerminalSocketSession(
|
||||||
|
sessionId: 'session-123',
|
||||||
|
socketClient: AgentSocketClient(Uri.parse('https://host:9443')),
|
||||||
|
transportFactory: (_) => transport,
|
||||||
|
);
|
||||||
|
|
||||||
|
final outputs = <TerminalOutputPayload>[];
|
||||||
|
final restores = <TerminalRestorePayload>[];
|
||||||
|
final connectFuture = session.connect(
|
||||||
|
onOutput: outputs.add,
|
||||||
|
onRestore: restores.add,
|
||||||
|
);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
transport.emit('{"type":"attached","sessionId":"session-123"}');
|
||||||
|
await connectFuture;
|
||||||
|
|
||||||
|
transport.emit(
|
||||||
|
'{"type":"restore","sessionId":"session-999","sequence":4,"screenText":"wrong","pendingInput":""}',
|
||||||
|
);
|
||||||
|
transport.emit(
|
||||||
|
'{"type":"output","sessionId":"session-999","sequence":5,"chunk":"wrong-output"}',
|
||||||
|
);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
expect(restores, isEmpty);
|
||||||
|
expect(outputs, isEmpty);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FakeTerminalSocketTransport implements TerminalSocketTransport {
|
class _FakeTerminalSocketTransport implements TerminalSocketTransport {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user