fix: address 8-domain Oracle review findings (C1, C2, M1-M4)

- C1: thinking-prepend unique part IDs per message (global PK collision)
- C2: recover-thinking-disabled-violation try/catch guard on SDK call
- M1: remove non-schema truncated/originalSize fields from SDK interfaces
- M2: messageHasContentFromSDK treats thinking-only messages as non-empty
- M3: syncAllTasksToTodos persists finalTodos + no-id rename dedup guard
- M4: AbortSignal.timeout(30s) on HTTP fetch calls in opencode-http-api

All 2739 tests pass, typecheck clean.
This commit is contained in:
YeonGyu-Kim
2026-02-16 15:23:45 +09:00
parent 106cd5c8b1
commit c2012c6027
10 changed files with 148 additions and 46 deletions
+74 -4
View File
@@ -418,12 +418,16 @@ describe("syncAllTasksToTodos", () => {
},
];
mockCtx.client.session.todo.mockResolvedValue(currentTodos);
let writtenTodos: TodoInfo[] = [];
const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => {
writtenTodos = input.todos;
};
// when
await syncAllTasksToTodos(mockCtx, tasks, "session-1");
await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer);
// then
expect(mockCtx.client.session.todo).toHaveBeenCalled();
expect(writtenTodos.some((t: TodoInfo) => t.id === "T-1")).toBe(false);
});
it("preserves existing todos not in task list", async () => {
@@ -451,12 +455,17 @@ describe("syncAllTasksToTodos", () => {
},
];
mockCtx.client.session.todo.mockResolvedValue(currentTodos);
let writtenTodos: TodoInfo[] = [];
const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => {
writtenTodos = input.todos;
};
// when
await syncAllTasksToTodos(mockCtx, tasks, "session-1");
await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer);
// then
expect(mockCtx.client.session.todo).toHaveBeenCalled();
expect(writtenTodos.some((t: TodoInfo) => t.id === "T-existing")).toBe(true);
expect(writtenTodos.some((t: TodoInfo) => t.content === "Task 1")).toBe(true);
});
it("handles empty task list", async () => {
@@ -471,6 +480,67 @@ describe("syncAllTasksToTodos", () => {
expect(mockCtx.client.session.todo).toHaveBeenCalled();
});
it("calls writer with final todos", async () => {
// given
const tasks: Task[] = [
{
id: "T-1",
subject: "Task 1",
description: "Description 1",
status: "pending",
blocks: [],
blockedBy: [],
},
];
mockCtx.client.session.todo.mockResolvedValue([]);
let writerCalled = false;
const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => {
writerCalled = true;
expect(input.sessionID).toBe("session-1");
expect(input.todos.length).toBe(1);
expect(input.todos[0].content).toBe("Task 1");
};
// when
await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer);
// then
expect(writerCalled).toBe(true);
});
it("deduplicates no-id todos when task replaces existing content", async () => {
// given
const tasks: Task[] = [
{
id: "T-1",
subject: "Task 1 (updated)",
description: "Description 1",
status: "in_progress",
blocks: [],
blockedBy: [],
},
];
const currentTodos: TodoInfo[] = [
{
content: "Task 1 (updated)",
status: "pending",
},
];
mockCtx.client.session.todo.mockResolvedValue(currentTodos);
let writtenTodos: TodoInfo[] = [];
const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => {
writtenTodos = input.todos;
};
// when
await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer);
// then — no duplicates
const matching = writtenTodos.filter((t: TodoInfo) => t.content === "Task 1 (updated)");
expect(matching.length).toBe(1);
expect(matching[0].status).toBe("in_progress");
});
it("preserves todos without id field", async () => {
// given
const tasks: Task[] = [
+10 -1
View File
@@ -139,6 +139,7 @@ export async function syncAllTasksToTodos(
ctx: PluginInput,
tasks: Task[],
sessionID?: string,
writer?: TodoWriter,
): Promise<void> {
try {
let currentTodos: TodoInfo[] = [];
@@ -156,8 +157,10 @@ export async function syncAllTasksToTodos(
const newTodos: TodoInfo[] = [];
const tasksToRemove = new Set<string>();
const allTaskSubjects = new Set<string>();
for (const task of tasks) {
allTaskSubjects.add(task.subject);
const todo = syncTaskToTodo(task);
if (todo === null) {
tasksToRemove.add(task.id);
@@ -176,13 +179,19 @@ export async function syncAllTasksToTodos(
const isInNewTodos = newTodos.some((newTodo) => todosMatch(existing, newTodo));
const isRemovedById = existing.id ? tasksToRemove.has(existing.id) : false;
const isRemovedByContent = !existing.id && removedTaskSubjects.has(existing.content);
if (!isInNewTodos && !isRemovedById && !isRemovedByContent) {
const isReplacedByTask = !existing.id && allTaskSubjects.has(existing.content);
if (!isInNewTodos && !isRemovedById && !isRemovedByContent && !isReplacedByTask) {
finalTodos.push(existing);
}
}
finalTodos.push(...newTodos);
const resolvedWriter = writer ?? (await resolveTodoWriter());
if (resolvedWriter && sessionID) {
await resolvedWriter({ sessionID, todos: finalTodos });
}
log("[todo-sync] Synced todos", {
count: finalTodos.length,
sessionID,