|
|
@@ -1,4 +1,7 @@
|
|
|
-import { describe, expect, mock, test } from 'bun:test';
|
|
|
+import * as fs from 'node:fs';
|
|
|
+import * as os from 'node:os';
|
|
|
+import * as path from 'node:path';
|
|
|
+import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
|
import { SLIM_INTERNAL_INITIATOR_MARKER } from '../utils';
|
|
|
import { BackgroundTaskManager } from './background-manager';
|
|
|
|
|
|
@@ -283,6 +286,106 @@ describe('BackgroundTaskManager', () => {
|
|
|
expect(result).toBeDefined();
|
|
|
expect(result?.id).toBe(task.id);
|
|
|
});
|
|
|
+
|
|
|
+ describe('disk persistence (survives manager reinitialization)', () => {
|
|
|
+ let testDir: string;
|
|
|
+ const origEnv = process.env.OPENCODE_LOG_DIR;
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omo-bg-test-'));
|
|
|
+ process.env.OPENCODE_LOG_DIR = testDir;
|
|
|
+ });
|
|
|
+
|
|
|
+ afterEach(() => {
|
|
|
+ fs.rmSync(testDir, { recursive: true, force: true });
|
|
|
+ if (origEnv === undefined) {
|
|
|
+ delete process.env.OPENCODE_LOG_DIR;
|
|
|
+ } else {
|
|
|
+ process.env.OPENCODE_LOG_DIR = origEnv;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ test('completed task is retrievable by a new manager instance after reinitialization', async () => {
|
|
|
+ const ctx = createMockContext({
|
|
|
+ sessionMessagesResult: {
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ info: { role: 'assistant' },
|
|
|
+ parts: [{ type: 'text', text: 'Task result here' }],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // First manager: completes the task
|
|
|
+ const manager1 = new BackgroundTaskManager(ctx);
|
|
|
+ const task = manager1.launch({
|
|
|
+ agent: 'explorer',
|
|
|
+ prompt: 'test',
|
|
|
+ description: 'Persistence test task',
|
|
|
+ parentSessionId: 'parent-session',
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.resolve();
|
|
|
+ await Promise.resolve();
|
|
|
+
|
|
|
+ await manager1.handleSessionStatus({
|
|
|
+ type: 'session.status',
|
|
|
+ properties: {
|
|
|
+ sessionID: task.sessionId,
|
|
|
+ status: { type: 'idle' },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(task.status).toBe('completed');
|
|
|
+ expect(task.result).toBe('Task result here');
|
|
|
+
|
|
|
+ // Simulate reinitialization: new manager with empty in-memory state
|
|
|
+ const manager2 = new BackgroundTaskManager(ctx);
|
|
|
+
|
|
|
+ // Should recover from disk
|
|
|
+ const recovered = manager2.getResult(task.id);
|
|
|
+ expect(recovered).not.toBeNull();
|
|
|
+ expect(recovered?.id).toBe(task.id);
|
|
|
+ expect(recovered?.status).toBe('completed');
|
|
|
+ expect(recovered?.result).toBe('Task result here');
|
|
|
+ expect(recovered?.description).toBe('Persistence test task');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('failed task is also recoverable after reinitialization', async () => {
|
|
|
+ const ctx = createMockContext({
|
|
|
+ sessionCreateResult: { data: {} }, // causes launch failure
|
|
|
+ });
|
|
|
+
|
|
|
+ const manager1 = new BackgroundTaskManager(ctx);
|
|
|
+ const task = manager1.launch({
|
|
|
+ agent: 'explorer',
|
|
|
+ prompt: 'test',
|
|
|
+ description: 'Failing task',
|
|
|
+ parentSessionId: 'parent-session',
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.resolve();
|
|
|
+ await Promise.resolve();
|
|
|
+
|
|
|
+ expect(task.status).toBe('failed');
|
|
|
+
|
|
|
+ const manager2 = new BackgroundTaskManager(ctx);
|
|
|
+ const recovered = manager2.getResult(task.id);
|
|
|
+ expect(recovered).not.toBeNull();
|
|
|
+ expect(recovered?.status).toBe('failed');
|
|
|
+ expect(recovered?.error).toBe('Failed to create background session');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('returns null for task that never completed (no disk file)', () => {
|
|
|
+ const ctx = createMockContext();
|
|
|
+ const manager = new BackgroundTaskManager(ctx);
|
|
|
+
|
|
|
+ // Task that was only launched on a previous (lost) manager
|
|
|
+ const result = manager.getResult('bg_nonexistent99');
|
|
|
+ expect(result).toBeNull();
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
describe('waitForCompletion', () => {
|