fix(agents/contracts): return cloned task snapshots from shared task board

This commit is contained in:
wing
2026-02-18 15:07:36 +08:00
parent 6c53e086f7
commit 3e173d1e5d
2 changed files with 47 additions and 13 deletions
+25 -11
View File
@@ -18,6 +18,7 @@
*/
import { TaskStatus } from './agent-message.js';
import { deepClone } from '../../shared/utils/value-utils.js';
// ─── 常量 ───────────────────────────────────────────────
@@ -43,6 +44,16 @@ function createTaskId() {
return `task_${Date.now().toString(36)}_${(++_counter).toString(36)}`;
}
/** @param {BoardTask} task @returns {BoardTask} */
function cloneTask(task) {
return deepClone(task);
}
/** @param {BoardTask[]} tasks @returns {BoardTask[]} */
function cloneTasks(tasks) {
return tasks.map((task) => cloneTask(task));
}
// ─── 类型定义 ───────────────────────────────────────────
/**
@@ -112,7 +123,7 @@ export class SharedTaskBoard {
this._tasks.set(task.id, task);
this._emit(TaskBoardEvents.TASK_ADDED, { task });
return { ok: true, task };
return { ok: true, task: cloneTask(task) };
}
/**
@@ -136,7 +147,7 @@ export class SharedTaskBoard {
task.claimedAt = Date.now();
this._emit(TaskBoardEvents.TASK_CLAIMED, { task });
return { ok: true, task };
return { ok: true, task: cloneTask(task) };
}
/**
@@ -159,7 +170,7 @@ export class SharedTaskBoard {
task.completedAt = Date.now();
this._emit(TaskBoardEvents.TASK_COMPLETED, { task });
return { ok: true, task };
return { ok: true, task: cloneTask(task) };
}
/**
@@ -182,7 +193,7 @@ export class SharedTaskBoard {
task.completedAt = Date.now();
this._emit(TaskBoardEvents.TASK_FAILED, { task });
return { ok: true, task };
return { ok: true, task: cloneTask(task) };
}
/**
@@ -207,7 +218,7 @@ export class SharedTaskBoard {
task.completedAt = Date.now();
this._emit(TaskBoardEvents.TASK_CANCELLED, { task });
return { ok: true, task };
return { ok: true, task: cloneTask(task) };
}
/**
@@ -219,7 +230,8 @@ export class SharedTaskBoard {
for (const task of this._tasks.values()) {
if (task.status === 'pending') pending.push(task);
}
return pending.sort((a, b) => a.priority - b.priority || a.createdAt - b.createdAt);
pending.sort((a, b) => a.priority - b.priority || a.createdAt - b.createdAt);
return cloneTasks(pending);
}
/**
@@ -234,7 +246,7 @@ export class SharedTaskBoard {
for (const task of this._tasks.values()) {
if (task.claimedBy === agent) result.push(task);
}
return result;
return cloneTasks(result);
}
/**
@@ -243,7 +255,8 @@ export class SharedTaskBoard {
* @returns {BoardTask | null}
*/
getTask(taskId) {
return this._tasks.get(taskId) ?? null;
const task = this._tasks.get(taskId);
return task ? cloneTask(task) : null;
}
/** @returns {number} */
@@ -256,7 +269,7 @@ export class SharedTaskBoard {
getSnapshot() {
return {
boardId: this.boardId,
tasks: Array.from(this._tasks.values()),
tasks: cloneTasks(Array.from(this._tasks.values())),
ts: Date.now(),
};
}
@@ -274,7 +287,7 @@ export class SharedTaskBoard {
let count = 0;
for (const task of snapshot.tasks) {
if (task && typeof task === 'object' && str(task.id)) {
this._tasks.set(task.id, { ...task });
this._tasks.set(task.id, cloneTask(task));
count++;
}
}
@@ -289,10 +302,11 @@ export class SharedTaskBoard {
_emit(eventName, payload) {
if (!this._eventBus || typeof this._eventBus.emit !== 'function') return;
try {
const safePayload = deepClone(payload);
this._eventBus.emit(eventName, {
actor: 'taskboard',
status: 'info',
payload: { ...payload, boardId: this.boardId },
payload: { ...safePayload, boardId: this.boardId },
});
} catch {
// best-effort
@@ -201,14 +201,30 @@ describe('SharedTaskBoard', () => {
});
describe('getTask', () => {
it('returns task by id', () => {
it('returns cloned task by id', () => {
const { task } = board.addTask({ taskType: 'a:b', createdBy: 'x' });
expect(board.getTask(task.id)).toBe(task);
const stored = board.getTask(task.id);
expect(stored).toEqual(task);
expect(stored).not.toBe(task);
});
it('returns null for unknown id', () => {
expect(board.getTask('nope')).toBeNull();
});
it('does not allow external mutation through returned task objects', () => {
const { task } = board.addTask({
taskType: 'a:b',
createdBy: 'x',
payload: { nested: { value: 1 } },
});
const view = board.getTask(task.id);
view.payload.nested.value = 999;
const fresh = board.getTask(task.id);
expect(fresh.payload.nested.value).toBe(1);
});
});
describe('snapshot and restore', () => {
@@ -221,6 +237,10 @@ describe('SharedTaskBoard', () => {
expect(snapshot.tasks).toHaveLength(2);
expect(snapshot.ts).toBeGreaterThan(0);
snapshot.tasks[0].status = 'failed';
const fresh = board.getTask(snapshot.tasks[0].id);
expect(fresh.status).toBe('pending');
const board2 = new SharedTaskBoard();
const result = board2.restore(snapshot);
expect(result.ok).toBe(true);