feat(agents): 完善恢复费用与云资源交互
This commit is contained in:
@@ -3,21 +3,23 @@ import { mkdtemp, readFile, writeFile, rm, readdir } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { CloudAgentsModule } from '@electron/services/cloud-agents';
|
||||
import { CloudAgentJournal } from '@electron/services/cloud-agent-journal';
|
||||
import type { CloudRecoveryState } from '../../shared/cloud-agents';
|
||||
import { EMPTY_CLOUD_CONFIGURATION } from '../../shared/cloud-agents';
|
||||
import { clearWorksSquareSession, storeWorksSquareSession } from '@electron/services/works-square-session';
|
||||
|
||||
const desktop = vi.hoisted(() => ({ pick: vi.fn(), save: vi.fn(), show: vi.fn(), click: vi.fn() }));
|
||||
vi.mock('electron', () => ({
|
||||
vi.mock('electron', () => { const electron = {
|
||||
app: { getPath: () => '/tmp/cloud-agents-test', getVersion: () => 'test', isPackaged: false },
|
||||
dialog: { showOpenDialog: desktop.pick, showSaveDialog: desktop.save },
|
||||
Notification: class { static isSupported() { return true; } on = desktop.click; show = desktop.show; },
|
||||
BrowserWindow: { getAllWindows: () => [] },
|
||||
}));
|
||||
}; return { ...electron, default: electron }; });
|
||||
|
||||
const draft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: '写作搭档', purpose: '帮助写作', system_prompt: '简明表达',
|
||||
draft_revision: 1, updated_at: '2026-09-10T00:00:00Z',
|
||||
configuration: EMPTY_CLOUD_CONFIGURATION, published_version: null, enabled: true,
|
||||
configuration: EMPTY_CLOUD_CONFIGURATION, published_version: null, enabled: true, archived: false,
|
||||
};
|
||||
const input = { operation_id: '12345678-1234-1234-1234-123456789012', name: draft.name, purpose: draft.purpose };
|
||||
const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status });
|
||||
@@ -29,13 +31,63 @@ function login(key = 'a'.repeat(64)) {
|
||||
storeWorksSquareSession({ accessToken: 'ws-secret', expiresAt: Date.now() + 600000, accountPartitionKey: key });
|
||||
}
|
||||
const instances: CloudAgentsModule[] = [];
|
||||
const journalRecords = new Map<string, CloudRecoveryState>();
|
||||
const journal = () => new CloudAgentJournal(async () => ({
|
||||
get: (key, fallback) => journalRecords.get(key) ?? fallback,
|
||||
set: (key, value) => { journalRecords.set(key, value); },
|
||||
}));
|
||||
function moduleFor(fetchImpl: typeof fetch) {
|
||||
const module = new CloudAgentsModule(fetchImpl); instances.push(module); return module;
|
||||
const module = new CloudAgentsModule(fetchImpl, journal()); instances.push(module); return module;
|
||||
}
|
||||
beforeEach(() => { vi.clearAllMocks(); clearWorksSquareSession(); login(); });
|
||||
beforeEach(() => { vi.clearAllMocks(); journalRecords.clear(); clearWorksSquareSession(); login(); });
|
||||
afterEach(() => { instances.splice(0).forEach((module) => module.dispose()); clearWorksSquareSession(); vi.useRealTimers(); });
|
||||
|
||||
describe('Main cloud Agents boundary', () => {
|
||||
it('persists recovery in the real electron-store file and reloads it after replacing Main', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'makelore-cloud-journal-'));
|
||||
try {
|
||||
const { default: Store } = await import('electron-store');
|
||||
const diskJournal = () => new CloudAgentJournal(async () => new Store<Record<string, CloudRecoveryState>>({
|
||||
cwd: directory, name: 'recovery', projectVersion: '2.0.0',
|
||||
}));
|
||||
const transport = vi.fn().mockResolvedValueOnce(session()).mockRejectedValueOnce(new Error('lost response'));
|
||||
const first = new CloudAgentsModule(transport, diskJournal()); instances.push(first);
|
||||
await first.rememberRecent({ slug: draft.slug, mode: 'published', thread_id: 'durable-thread' });
|
||||
await expect(first.execute({ operation: 'submit', input: { slug: draft.slug, request_id: 'durable-request', thread_id: 'durable-thread', query: '待确认任务' } })).rejects.toThrow();
|
||||
first.dispose();
|
||||
const raw = await readFile(join(directory, 'recovery.json'), 'utf8');
|
||||
expect(raw).toContain('durable-request');
|
||||
expect(raw).not.toContain('ws-secret'); expect(raw).not.toContain('yuxi-secret');
|
||||
const restored = new CloudAgentsModule(vi.fn(), diskJournal()); instances.push(restored);
|
||||
const state = await restored.recovery();
|
||||
expect(state.recent?.thread_id).toBe('durable-thread');
|
||||
expect(state.pending[0].input.query).toBe('待确认任务');
|
||||
login('b'.repeat(64));
|
||||
expect(await restored.recovery()).toEqual({ recent: null, pending: [] });
|
||||
} finally { await rm(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
it('restores an uncertain operation and recent location in a new Main instance without crossing accounts', async () => {
|
||||
const transport = vi.fn().mockResolvedValueOnce(session()).mockRejectedValueOnce(new Error('lost'))
|
||||
.mockResolvedValueOnce(session()).mockResolvedValueOnce(json({
|
||||
request_id: 'server-request', thread_id: 'server-thread', run_id: null, status: 'queued', version: '1',
|
||||
}));
|
||||
const first = moduleFor(transport);
|
||||
const input = { slug: draft.slug, request_id: 'stable-request', thread_id: 'stable-thread', query: '原始任务' };
|
||||
await first.rememberRecent({ slug: draft.slug, mode: 'published', thread_id: 'stable-thread' });
|
||||
await expect(first.execute({ operation: 'submit', input })).rejects.toMatchObject({ code: 'cloud_service_unavailable' });
|
||||
first.dispose();
|
||||
const restored = moduleFor(transport);
|
||||
login('b'.repeat(64));
|
||||
expect(await restored.recovery()).toEqual({ recent: null, pending: [] });
|
||||
login();
|
||||
const state = await restored.recovery();
|
||||
expect(state.recent?.slug).toBe(draft.slug);
|
||||
expect(state.pending).toHaveLength(1);
|
||||
expect(transport).toHaveBeenCalledTimes(2);
|
||||
expect(await restored.resolvePending({ id: state.pending[0].id })).toMatchObject({ result: { request_id: 'server-request' } });
|
||||
expect(JSON.parse(transport.mock.calls[3][1].body)).toEqual(JSON.parse(transport.mock.calls[1][1].body));
|
||||
expect((await restored.recovery()).pending).toEqual([]);
|
||||
});
|
||||
it('reads upload bytes and writes downloaded bytes only through Main dialogs', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'makelore-cloud-files-'));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user