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) =>
|
||||
_JournalItem.fromJson(Map<String, dynamic>.from(item as Map)),
|
||||
)
|
||||
.where((item) => item.sessionId == session.sessionId)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
|
||||
@ -179,7 +179,9 @@ class TerminalSocketSession {
|
||||
bool _handleAttachedAck(String frame) {
|
||||
try {
|
||||
final decoded = jsonDecode(frame);
|
||||
if (decoded is Map && decoded['type'] == 'attached') {
|
||||
if (decoded is Map &&
|
||||
decoded['type'] == 'attached' &&
|
||||
_matchesSessionId(decoded)) {
|
||||
return true;
|
||||
}
|
||||
} catch (_) {}
|
||||
@ -193,7 +195,9 @@ class TerminalSocketSession {
|
||||
) {
|
||||
try {
|
||||
final decoded = jsonDecode(frame);
|
||||
if (decoded is Map && decoded['type'] == 'restore') {
|
||||
if (decoded is Map &&
|
||||
decoded['type'] == 'restore' &&
|
||||
_matchesSessionId(decoded)) {
|
||||
onRestore(
|
||||
TerminalRestorePayload.fromJson(Map<String, dynamic>.from(decoded)),
|
||||
);
|
||||
@ -207,7 +211,9 @@ class TerminalSocketSession {
|
||||
TerminalOutputPayload? _decodeOutputFrame(String frame) {
|
||||
try {
|
||||
final decoded = jsonDecode(frame);
|
||||
if (decoded is Map && decoded['type'] == 'output') {
|
||||
if (decoded is Map &&
|
||||
decoded['type'] == 'output' &&
|
||||
_matchesSessionId(decoded)) {
|
||||
return TerminalOutputPayload.fromJson(
|
||||
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) {
|
||||
if (transport == null) {
|
||||
_subscription = null;
|
||||
|
||||
@ -784,6 +784,62 @@ void main() {
|
||||
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 {
|
||||
|
||||
@ -220,6 +220,37 @@ void main() {
|
||||
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user