mirror of
https://github.com/Feather-2/paper-burner-x.git
synced 2026-09-21 04:37:06 +08:00
fix(agents/contracts): return cloned task snapshots from shared task board
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user