mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-24 18:52:29 -07:00
feat(cli): add /teleport command for portable session management
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import {
|
||||
TeleportService,
|
||||
Config,
|
||||
ChatRecordingService,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
describe('Teleport E2E Integration', () => {
|
||||
let tmpDir: string;
|
||||
let machineA_Home: string;
|
||||
let machineB_Home: string;
|
||||
let projectDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-teleport-e2e-'));
|
||||
machineA_Home = path.join(tmpDir, 'machineA');
|
||||
machineB_Home = path.join(tmpDir, 'machineB');
|
||||
projectDir = path.join(tmpDir, 'my-project');
|
||||
|
||||
await fs.mkdir(machineA_Home, { recursive: true });
|
||||
await fs.mkdir(machineB_Home, { recursive: true });
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should round-trip a session between two simulated machines', async () => {
|
||||
// --- STEP 1: Setup Machine A and create a session ---
|
||||
const configA = new Config({
|
||||
sessionId: 'session-123',
|
||||
targetDir: projectDir,
|
||||
cwd: projectDir,
|
||||
model: 'test-model',
|
||||
});
|
||||
const machineA_TempDir = path.join(machineA_Home, 'tmp');
|
||||
await fs.mkdir(machineA_TempDir, { recursive: true });
|
||||
vi.spyOn(configA.storage, 'getProjectTempDir').mockReturnValue(
|
||||
machineA_TempDir,
|
||||
);
|
||||
|
||||
const recordingServiceA = new ChatRecordingService(configA);
|
||||
recordingServiceA.initialize();
|
||||
|
||||
recordingServiceA.recordMessage({
|
||||
model: 'test-model',
|
||||
type: 'user',
|
||||
content: [{ text: 'Hello from Machine A' }],
|
||||
});
|
||||
|
||||
const realSessionId = configA.getSessionId();
|
||||
const chatFilePathA = recordingServiceA.getConversationFilePath();
|
||||
expect(chatFilePathA).not.toBeNull();
|
||||
|
||||
// --- STEP 2: Export from Machine A ---
|
||||
const teleportServiceA = new TeleportService(configA);
|
||||
const tarballPath = path.join(tmpDir, 'teleport.tar.gz');
|
||||
await teleportServiceA.exportSession(realSessionId, tarballPath);
|
||||
|
||||
// --- STEP 3: Setup Machine B and Import ---
|
||||
const configB = new Config({
|
||||
sessionId: 'new-session',
|
||||
targetDir: projectDir,
|
||||
cwd: projectDir,
|
||||
model: 'test-model',
|
||||
});
|
||||
const machineB_TempDir = path.join(machineB_Home, 'tmp');
|
||||
await fs.mkdir(machineB_TempDir, { recursive: true });
|
||||
vi.spyOn(configB.storage, 'getProjectTempDir').mockReturnValue(
|
||||
machineB_TempDir,
|
||||
);
|
||||
|
||||
const teleportServiceB = new TeleportService(configB);
|
||||
const importResult = await teleportServiceB.importSession(tarballPath);
|
||||
|
||||
expect(importResult.sessionId).toBe(realSessionId);
|
||||
|
||||
// --- STEP 4: Verify Machine B can "see" the session ---
|
||||
const chatFiles = await configB.storage.listProjectChatFiles();
|
||||
expect(chatFiles.length).toBe(1);
|
||||
|
||||
// storage.listProjectChatFiles returns relative paths
|
||||
const importedFile = path.join(machineB_TempDir, chatFiles[0].filePath);
|
||||
const conversationData = JSON.parse(
|
||||
await fs.readFile(importedFile, 'utf8'),
|
||||
);
|
||||
|
||||
const recordingServiceB = new ChatRecordingService(configB);
|
||||
recordingServiceB.initialize({
|
||||
filePath: importedFile,
|
||||
conversation: conversationData,
|
||||
});
|
||||
|
||||
const conversation = recordingServiceB.getConversation();
|
||||
expect(conversation).not.toBeNull();
|
||||
expect(conversation?.messages[0].content[0].text).toBe(
|
||||
'Hello from Machine A',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle encrypted sessions in E2E', async () => {
|
||||
const secret = 'password123';
|
||||
const configA = new Config({
|
||||
sessionId: 'enc-session',
|
||||
targetDir: projectDir,
|
||||
cwd: projectDir,
|
||||
model: 'm',
|
||||
});
|
||||
const machineA_TempDir = path.join(machineA_Home, 'tmp');
|
||||
await fs.mkdir(machineA_TempDir, { recursive: true });
|
||||
vi.spyOn(configA.storage, 'getProjectTempDir').mockReturnValue(
|
||||
machineA_TempDir,
|
||||
);
|
||||
|
||||
const recordingServiceA = new ChatRecordingService(configA);
|
||||
recordingServiceA.initialize();
|
||||
recordingServiceA.recordMessage({
|
||||
model: 'm',
|
||||
type: 'user',
|
||||
content: [{ text: 'Encrypted message' }],
|
||||
});
|
||||
|
||||
const realSessionId = configA.getSessionId();
|
||||
const teleportServiceA = new TeleportService(configA);
|
||||
const tarballPath = path.join(tmpDir, 'encrypted.tar.gz');
|
||||
await teleportServiceA.exportSession(realSessionId, tarballPath, secret);
|
||||
|
||||
// Machine B
|
||||
const configB = new Config({
|
||||
sessionId: 'b',
|
||||
targetDir: projectDir,
|
||||
cwd: projectDir,
|
||||
model: 'm',
|
||||
});
|
||||
const machineB_TempDir = path.join(machineB_Home, 'tmp');
|
||||
await fs.mkdir(machineB_TempDir, { recursive: true });
|
||||
vi.spyOn(configB.storage, 'getProjectTempDir').mockReturnValue(
|
||||
machineB_TempDir,
|
||||
);
|
||||
|
||||
const teleportServiceB = new TeleportService(configB);
|
||||
await teleportServiceB.importSession(tarballPath, secret);
|
||||
|
||||
const chatFiles = await configB.storage.listProjectChatFiles();
|
||||
const importedFile = path.join(machineB_TempDir, chatFiles[0].filePath);
|
||||
const conversationData = JSON.parse(
|
||||
await fs.readFile(importedFile, 'utf8'),
|
||||
);
|
||||
|
||||
const recordingServiceB = new ChatRecordingService(configB);
|
||||
recordingServiceB.initialize({
|
||||
filePath: importedFile,
|
||||
conversation: conversationData,
|
||||
});
|
||||
|
||||
const conversation = recordingServiceB.getConversation();
|
||||
expect(conversation?.messages[0].content[0].text).toBe('Encrypted message');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user