Skip to content
Merged
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
72 changes: 72 additions & 0 deletions packages/config/local/inbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,78 @@ describe("local inbox store", () => {
expect(summary?.taskTitle).toBe("Check deploy status after 1h");
});

it("preserves task/cron source_kind when a later ensureMessageThread call defaults to user", () => {
// Simulates the bug where a synthetic task thread was later re-ensured
// through a path (e.g. runtime-facade) that hardcodes sourceKind: "user".
const taskThreadKey = buildThreadKey("C-preserve", "task:task-99");
ensureMessageThread({
platform: "slack",
channelId: "C-preserve",
threadId: "task:task-99",
replyThreadId: "task:task-99",
sourceKind: "task",
taskId: "task-99",
taskTitle: "do the thing later",
});

// Second call without explicit sourceKind (defaults to "user") must NOT
// clobber the more specific "task" source already persisted.
ensureMessageThread({
platform: "slack",
channelId: "C-preserve",
threadId: "task:task-99",
replyThreadId: "task:task-99",
});

const taskDetail = getMessageThreadById(taskThreadKey);
expect(taskDetail?.sourceKind).toBe("task");
expect(taskDetail?.taskId).toBe("task-99");
expect(taskDetail?.taskTitle).toBe("do the thing later");

// Same protection for cron_job.
const cronThreadKey = buildThreadKey("C-preserve", "cron-job:job-99");
ensureMessageThread({
platform: "slack",
channelId: "C-preserve",
threadId: "cron-job:job-99",
replyThreadId: "cron-job:job-99",
sourceKind: "cron_job",
cronJobId: "job-99",
cronJobTitle: "nightly cron",
});
ensureMessageThread({
platform: "slack",
channelId: "C-preserve",
threadId: "cron-job:job-99",
replyThreadId: "cron-job:job-99",
sourceKind: "user",
});
const cronDetail = getMessageThreadById(cronThreadKey);
expect(cronDetail?.sourceKind).toBe("cron_job");
expect(cronDetail?.cronJobId).toBe("job-99");

// Sanity check: a plain user thread upgraded to task still reflects the
// more specific value (upgrades user -> task are allowed).
const upgradeKey = buildThreadKey("C-preserve", "T-upgrade");
ensureMessageThread({
platform: "slack",
channelId: "C-preserve",
threadId: "T-upgrade",
replyThreadId: "T-upgrade",
});
ensureMessageThread({
platform: "slack",
channelId: "C-preserve",
threadId: "T-upgrade",
replyThreadId: "T-upgrade",
sourceKind: "task",
taskId: "task-upgrade",
});
const upgraded = getMessageThreadById(upgradeKey);
expect(upgraded?.sourceKind).toBe("task");
expect(upgraded?.taskId).toBe("task-upgrade");
});

it("captures a full question → reply → answer batch timeline", () => {
const threadKey = buildThreadKey("C-q", "T-q");
ensureMessageThread({
Expand Down
5 changes: 4 additions & 1 deletion packages/config/local/inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,10 @@ export function ensureMessageThread(params: EnsureMessageThreadParams): string {
working_directory = COALESCE(excluded.working_directory, message_thread.working_directory),
thread_owner_user_id = COALESCE(excluded.thread_owner_user_id, message_thread.thread_owner_user_id),
branch_name = COALESCE(excluded.branch_name, message_thread.branch_name),
source_kind = excluded.source_kind,
source_kind = CASE
WHEN message_thread.source_kind IN ('task','cron_job') THEN message_thread.source_kind
ELSE excluded.source_kind
END,
cron_job_id = COALESCE(excluded.cron_job_id, message_thread.cron_job_id),
cron_job_title = COALESCE(excluded.cron_job_title, message_thread.cron_job_title),
task_id = COALESCE(excluded.task_id, message_thread.task_id),
Expand Down
Loading