feat(FN-5430): gate task update invalidation in scheduler
Scheduler now gates task update invalidation, preventing spurious invalidations when engine lifecycle changes (soft-delete, lease recovery) touch task metadata, with coverage via new scheduler invalidation tests and a small dashboard server test adjustment. Fusion-Task-Id: FN-5430
This commit is contained in:
committed by
gsxdsm
parent
5c15031416
commit
59bebf0806
@@ -27,14 +27,30 @@ function createStore() {
|
||||
return { store, emit };
|
||||
}
|
||||
|
||||
function createTask(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "FN-1",
|
||||
column: "todo",
|
||||
paused: false,
|
||||
assignedAgentId: null,
|
||||
checkedOutBy: null,
|
||||
deletedAt: null,
|
||||
dependencies: [],
|
||||
columnMovedAt: "2026-01-01T00:00:00.000Z",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Scheduler auto-claim snapshot invalidation", () => {
|
||||
it("invalidates on task:created, task:updated, and task:deleted", () => {
|
||||
it("invalidates on task:created, first-seen task:updated, and task:deleted", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:created", { task: { id: "FN-1" } });
|
||||
emit("task:updated", { id: "FN-1" });
|
||||
emit("task:created", createTask({ id: "FN-1" }));
|
||||
emit("task:updated", createTask({ id: "FN-99" }));
|
||||
emit("task:deleted", { id: "FN-1" });
|
||||
|
||||
expect(invalidate).toHaveBeenCalledWith("task:created");
|
||||
@@ -42,6 +58,86 @@ describe("Scheduler auto-claim snapshot invalidation", () => {
|
||||
expect(invalidate).toHaveBeenCalledWith("task:deleted");
|
||||
});
|
||||
|
||||
it("does not invalidate for candidacy-neutral task:updated events", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:created", createTask());
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
emit("task:updated", createTask({ updatedAt: `2026-01-01T00:00:0${i}.000Z`, title: `v${i}` }));
|
||||
}
|
||||
|
||||
expect(invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(invalidate).toHaveBeenNthCalledWith(1, "task:created");
|
||||
expect(invalidate).not.toHaveBeenCalledWith("task:updated");
|
||||
});
|
||||
|
||||
it("invalidates once per candidacy-changing task:updated mutation", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:created", createTask());
|
||||
|
||||
emit("task:updated", createTask({ paused: true }));
|
||||
expect(invalidate).toHaveBeenCalledTimes(2);
|
||||
expect(invalidate).toHaveBeenLastCalledWith("task:updated");
|
||||
|
||||
emit("task:updated", createTask({ paused: true, updatedAt: "2026-01-01T00:00:02.000Z" }));
|
||||
expect(invalidate).toHaveBeenCalledTimes(2);
|
||||
|
||||
emit("task:updated", createTask({ paused: false, updatedAt: "2026-01-01T00:00:03.000Z" }));
|
||||
expect(invalidate).toHaveBeenCalledTimes(3);
|
||||
expect(invalidate).toHaveBeenLastCalledWith("task:updated");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["column", { column: "in-progress" }],
|
||||
["paused", { paused: true }],
|
||||
["assignedAgentId", { assignedAgentId: "agent-1" }],
|
||||
["checkedOutBy", { checkedOutBy: "agent-2" }],
|
||||
["deletedAt", { deletedAt: "2026-01-02T00:00:00.000Z" }],
|
||||
["dependencies", { dependencies: ["FN-2"] }],
|
||||
["columnMovedAt", { columnMovedAt: "2026-01-03T00:00:00.000Z" }],
|
||||
])("invalidates when %s changes", (_field, mutation) => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:created", createTask());
|
||||
emit("task:updated", createTask(mutation));
|
||||
|
||||
expect(invalidate).toHaveBeenCalledTimes(2);
|
||||
expect(invalidate).toHaveBeenNthCalledWith(1, "task:created");
|
||||
expect(invalidate).toHaveBeenNthCalledWith(2, "task:updated");
|
||||
});
|
||||
|
||||
it("invalidates on first-sighting task:updated with no stored fingerprint", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:updated", createTask({ id: "FN-99" }));
|
||||
|
||||
expect(invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(invalidate).toHaveBeenCalledWith("task:updated");
|
||||
});
|
||||
|
||||
it("clears task fingerprint on task:deleted", () => {
|
||||
const invalidate = vi.fn();
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:created", createTask({ id: "FN-1" }));
|
||||
emit("task:deleted", { id: "FN-1" });
|
||||
emit("task:updated", createTask({ id: "FN-1" }));
|
||||
|
||||
expect(invalidate).toHaveBeenNthCalledWith(1, "task:created");
|
||||
expect(invalidate).toHaveBeenNthCalledWith(2, "task:deleted");
|
||||
expect(invalidate).toHaveBeenNthCalledWith(3, "task:updated");
|
||||
});
|
||||
|
||||
it("clears scheduler bookkeeping for deleted tasks", () => {
|
||||
const { store, emit } = createStore();
|
||||
const scheduler = new Scheduler(store, {});
|
||||
@@ -70,9 +166,9 @@ describe("Scheduler auto-claim snapshot invalidation", () => {
|
||||
const { store, emit } = createStore();
|
||||
new Scheduler(store, { snapshotManager: { invalidate } as any });
|
||||
|
||||
emit("task:moved", { task: { id: "FN-1" }, from: "todo", to: "in-progress" });
|
||||
emit("task:moved", { task: { id: "FN-2" }, from: "in-progress", to: "todo" });
|
||||
emit("task:moved", { task: { id: "FN-3" }, from: "in-review", to: "done" });
|
||||
emit("task:moved", { task: createTask({ id: "FN-1" }), from: "todo", to: "in-progress" });
|
||||
emit("task:moved", { task: createTask({ id: "FN-2" }), from: "in-progress", to: "todo" });
|
||||
emit("task:moved", { task: createTask({ id: "FN-3", column: "done" }), from: "in-review", to: "done" });
|
||||
|
||||
expect(invalidate).toHaveBeenCalledWith("task:moved:todo->in-progress");
|
||||
expect(invalidate).toHaveBeenCalledWith("task:moved:in-progress->todo");
|
||||
|
||||
Reference in New Issue
Block a user