Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/opencode/src/background/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ export class BackgroundManager {
throwOnError: false,
});

const children = unwrapResponse<Array<unknown>>(childrenResponse) ?? [];
const rawChildren = unwrapResponse<Array<unknown>>(childrenResponse);
const children = Array.isArray(rawChildren) ? rawChildren : [];
for (const child of children) {
const childSession = child as { id?: string; status?: { type?: string } };
if (!childSession.id) continue;
Expand Down Expand Up @@ -240,7 +241,8 @@ export class BackgroundManager {
throwOnError: false,
});

const sessions = unwrapResponse<Array<unknown>>(sessionsResponse) ?? [];
const rawSessions = unwrapResponse<Array<unknown>>(sessionsResponse);
const sessions = Array.isArray(rawSessions) ? rawSessions : [];

for (const session of sessions) {
const sess = session as {
Expand Down
140 changes: 140 additions & 0 deletions packages/opencode/test/background.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it, beforeEach } from 'bun:test';
import { ConcurrencyManager } from '../src/background/concurrency';
import { BackgroundManager } from '../src/background/manager';
import type { BackgroundTask, BackgroundTaskStatus, TaskProgress } from '../src/background/types';
import type { PluginInput } from '@opencode-ai/plugin';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Import PluginInput from local src in tests.

Using the package import bypasses the local export and violates the test dependency rule. Prefer the local re-export in ../src.
As per coding guidelines: "Import test dependencies from ../src/ in test files".

Suggested change
-import type { PluginInput } from '@opencode-ai/plugin';
+import type { PluginInput } from '../src';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { PluginInput } from '@opencode-ai/plugin';
import type { PluginInput } from '../src';
🤖 Prompt for AI Agents
In `@packages/opencode/test/background.test.ts` at line 5, The test currently
imports the PluginInput type from the published package; change the import to
use the local test re-export so tests depend on ../src instead of the package.
Update the import statement that references "PluginInput" in
packages/opencode/test/background.test.ts to import the type from the local
../src re-export (import type { PluginInput } from '../src') so the test follows
the project rule of importing test dependencies from ../src.


describe('Background', () => {
describe('ConcurrencyManager', () => {
Expand Down Expand Up @@ -266,4 +268,142 @@ describe('Background', () => {
expect(progress.lastTool).toBe('bash');
});
});

describe('BackgroundManager', () => {
function createMockCtx(overrides?: {
sessionList?: () => Promise<unknown>;
sessionChildren?: () => Promise<unknown>;
}): PluginInput {
return {
client: {
session: {
list: overrides?.sessionList ?? (async () => ({ data: [] })),
children: overrides?.sessionChildren ?? (async () => ({ data: [] })),
get: async () => ({ data: {} }),
messages: async () => ({ data: [] }),
create: async () => ({ data: { id: 'sess_1' } }),
prompt: async () => ({}),
abort: async () => ({}),
status: async () => ({ data: {} }),
},
},
} as unknown as PluginInput;
}
Comment on lines +273 to +291
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use @agentuity/test-utils for mocks instead of hand-rolled stubs.

createMockCtx manually constructs a mock, but tests should use the shared mock utilities to keep behavior consistent.
As per coding guidelines: "Use @agentuity/test-utils for mocks in tests".

🤖 Prompt for AI Agents
In `@packages/opencode/test/background.test.ts` around lines 273 - 291, The tests
currently define a hand-rolled createMockCtx that returns a PluginInput stub;
replace this with the shared mock from `@agentuity/test-utils` (e.g., import the
library's PluginInput/client/session mock factory) and use its factory to
construct the PluginInput used in tests, wiring the optional overrides for
session.list and session.children into the factory instead of manually stubbing
in createMockCtx so the test reuses the canonical mocks and behavior provided by
`@agentuity/test-utils`.


describe('recoverTasks', () => {
it('does not throw when session.list returns an empty object instead of an array', async () => {
const ctx = createMockCtx({
sessionList: async () => ({ data: {} }),
});
const mgr = new BackgroundManager(ctx);
const recovered = await mgr.recoverTasks();
expect(recovered).toBe(0);
});

it('does not throw when session.list returns undefined', async () => {
const ctx = createMockCtx({
sessionList: async () => ({ data: undefined }),
});
const mgr = new BackgroundManager(ctx);
const recovered = await mgr.recoverTasks();
expect(recovered).toBe(0);
});

it('does not throw when session.list returns null', async () => {
const ctx = createMockCtx({
sessionList: async () => ({ data: null }),
});
const mgr = new BackgroundManager(ctx);
const recovered = await mgr.recoverTasks();
expect(recovered).toBe(0);
});

it('returns 0 when session.list returns an empty array', async () => {
const ctx = createMockCtx({
sessionList: async () => ({ data: [] }),
});
const mgr = new BackgroundManager(ctx);
const recovered = await mgr.recoverTasks();
expect(recovered).toBe(0);
});

it('recovers tasks from valid session data', async () => {
const taskMeta = JSON.stringify({
taskId: 'bg_abc123',
agent: 'scout',
description: 'Test task',
createdAt: new Date().toISOString(),
});
const ctx = createMockCtx({
sessionList: async () => ({
data: [
{
id: 'sess_1',
title: taskMeta,
parentID: 'parent_1',
status: { type: 'running' },
},
],
}),
});
const mgr = new BackgroundManager(ctx);
const recovered = await mgr.recoverTasks();
expect(recovered).toBe(1);
const task = mgr.getTask('bg_abc123');
expect(task).toBeDefined();
expect(task!.agent).toBe('scout');
expect(task!.sessionId).toBe('sess_1');
expect(task!.status).toBe('running');
});

it('skips sessions without bg_ prefixed taskId', async () => {
const ctx = createMockCtx({
sessionList: async () => ({
data: [
{
id: 'sess_1',
title: JSON.stringify({ taskId: 'not_a_bg_task' }),
status: { type: 'idle' },
},
],
}),
});
const mgr = new BackgroundManager(ctx);
const recovered = await mgr.recoverTasks();
expect(recovered).toBe(0);
});
});

describe('refreshStatuses', () => {
it('does not throw when session.children returns an empty object instead of an array', async () => {
const ctx = createMockCtx({
sessionChildren: async () => ({ data: {} }),
});
const mgr = new BackgroundManager(ctx);

const taskMeta = JSON.stringify({
taskId: 'bg_test1',
agent: 'scout',
description: 'test',
});
const listCtx = createMockCtx({
sessionList: async () => ({
data: [
{
id: 'sess_1',
title: taskMeta,
parentID: 'parent_1',
status: { type: 'running' },
},
],
}),
});
const listMgr = new BackgroundManager(listCtx);
await listMgr.recoverTasks();

const results = await mgr.refreshStatuses();
expect(results).toBeDefined();
Comment on lines +377 to +405
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

refreshStatuses test doesn’t exercise the non-array children path.

The manager that uses sessionChildren: () => ({ data: {} }) never recovers tasks, so session.children is likely never called. Use a single manager that both recovers tasks and returns a non-array children payload.

Suggested change
-			it('does not throw when session.children returns an empty object instead of an array', async () => {
-				const ctx = createMockCtx({
-					sessionChildren: async () => ({ data: {} }),
-				});
-				const mgr = new BackgroundManager(ctx);
-
-				const taskMeta = JSON.stringify({
+			it('does not throw when session.children returns an empty object instead of an array', async () => {
+				const taskMeta = JSON.stringify({
 					taskId: 'bg_test1',
 					agent: 'scout',
 					description: 'test',
 				});
-				const listCtx = createMockCtx({
+				const ctx = createMockCtx({
 					sessionList: async () => ({
 						data: [
 							{
 								id: 'sess_1',
 								title: taskMeta,
 								parentID: 'parent_1',
 								status: { type: 'running' },
 							},
 						],
 					}),
+					sessionChildren: async () => ({ data: {} }),
 				});
-				const listMgr = new BackgroundManager(listCtx);
-				await listMgr.recoverTasks();
+				const mgr = new BackgroundManager(ctx);
+				await mgr.recoverTasks();
 
 				const results = await mgr.refreshStatuses();
 				expect(results).toBeDefined();
 			});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('refreshStatuses', () => {
it('does not throw when session.children returns an empty object instead of an array', async () => {
const ctx = createMockCtx({
sessionChildren: async () => ({ data: {} }),
});
const mgr = new BackgroundManager(ctx);
const taskMeta = JSON.stringify({
taskId: 'bg_test1',
agent: 'scout',
description: 'test',
});
const listCtx = createMockCtx({
sessionList: async () => ({
data: [
{
id: 'sess_1',
title: taskMeta,
parentID: 'parent_1',
status: { type: 'running' },
},
],
}),
});
const listMgr = new BackgroundManager(listCtx);
await listMgr.recoverTasks();
const results = await mgr.refreshStatuses();
expect(results).toBeDefined();
describe('refreshStatuses', () => {
it('does not throw when session.children returns an empty object instead of an array', async () => {
const taskMeta = JSON.stringify({
taskId: 'bg_test1',
agent: 'scout',
description: 'test',
});
const ctx = createMockCtx({
sessionList: async () => ({
data: [
{
id: 'sess_1',
title: taskMeta,
parentID: 'parent_1',
status: { type: 'running' },
},
],
}),
sessionChildren: async () => ({ data: {} }),
});
const mgr = new BackgroundManager(ctx);
await mgr.recoverTasks();
const results = await mgr.refreshStatuses();
expect(results).toBeDefined();
});
🤖 Prompt for AI Agents
In `@packages/opencode/test/background.test.ts` around lines 377 - 405, The test
currently instantiates two BackgroundManager instances so the one configured
with sessionChildren: async () => ({ data: {} }) never runs recoverTasks; change
the test to create a single mock context that returns a sessionList (with the
task meta) and a non-array sessionChildren payload ({ data: {} }), instantiate
one BackgroundManager with that context (use BackgroundManager mgr), call
mgr.recoverTasks() and then await mgr.refreshStatuses(), and assert results;
this ensures BackgroundManager.recoverTasks and
BackgroundManager.refreshStatuses exercise the non-array children path.

});
});
});
});
Loading