feat(FN-2840): strengthen node routing and task diff visibility
- Expand core test coverage for task node overrides, unavailable node policies, and defaultNodeId settings contracts - Restrict dashboard task change views to files actually modified by the task, with updated API route handling and tests - Improve quick chat UX and styling with extended component behavior and hook updates - Harden memory dream extraction by reading assistant text from session state and covering undefined-output cases - Enhance merger commit summarization and update architecture/settings/multi-project/task-management docs Fusion-Task-Id: FN-2840
This commit is contained in:
@@ -50,6 +50,30 @@ describe("validateNodeOverrideChange", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("returns exact task-in-progress reason and actionable guidance in message", () => {
|
||||
const result = validateNodeOverrideChange(
|
||||
{ id: "FN-999", column: "in-progress", nodeId: "node-a" },
|
||||
"node-b",
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
allowed: false,
|
||||
reason: "task-in-progress",
|
||||
});
|
||||
expect(result.message).toContain("FN-999");
|
||||
expect(result.message?.toLowerCase()).toContain("wait");
|
||||
expect(result.message?.toLowerCase()).toContain("pause");
|
||||
expect(result.message?.toLowerCase()).toContain("stop");
|
||||
});
|
||||
|
||||
it("blocks setting nodeId on in-progress task even when existing nodeId is undefined", () => {
|
||||
const result = validateNodeOverrideChange({ id: "FN-404", column: "in-progress" }, "node-new");
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.reason).toBe("task-in-progress");
|
||||
expect(result.message).toContain("FN-404");
|
||||
});
|
||||
|
||||
it("allows nodeId change on in-progress task when newNodeId is undefined (no-op)", () => {
|
||||
const result = validateNodeOverrideChange(
|
||||
{ id: "FN-2", column: "in-progress", nodeId: "node-a" },
|
||||
|
||||
@@ -557,6 +557,33 @@ describe("TaskStore", () => {
|
||||
const updated = await store.updateTask(task.id, { nodeId: null });
|
||||
expect(updated.nodeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not throw when nodeId update is undefined on an in-progress task", async () => {
|
||||
const task = await store.createTask({ description: "In progress no-op", nodeId: "node-stable" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
|
||||
const updated = await store.updateTask(task.id, { nodeId: undefined });
|
||||
expect(updated.nodeId).toBe("node-stable");
|
||||
});
|
||||
|
||||
it("includes task ID in nodeId override blocking error", async () => {
|
||||
const task = await store.createTask({ description: "In progress blocked id" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
|
||||
await expect(store.updateTask(task.id, { nodeId: "node-abc" })).rejects.toThrow(task.id);
|
||||
});
|
||||
|
||||
it("allows priority updates on in-progress tasks without changing existing nodeId", async () => {
|
||||
const task = await store.createTask({ description: "In progress priority", nodeId: "node-keep" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
|
||||
const updated = await store.updateTask(task.id, { priority: "high" });
|
||||
expect(updated.priority).toBe("high");
|
||||
expect(updated.nodeId).toBe("node-keep");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectNextTaskForAgent", () => {
|
||||
|
||||
@@ -56,6 +56,21 @@ describe("task node override persistence", () => {
|
||||
expect(fetched.nodeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("treats updateTask nodeId undefined as a no-op", async () => {
|
||||
const created = await store.createTask({ description: "Task to keep node", nodeId: "node-stable" });
|
||||
await store.updateTask(created.id, { nodeId: undefined });
|
||||
|
||||
const fetched = await store.getTask(created.id);
|
||||
expect(fetched.nodeId).toBe("node-stable");
|
||||
});
|
||||
|
||||
it("normalizes createTask nodeId null to undefined", async () => {
|
||||
const created = await store.createTask({ description: "Task with null node", nodeId: null });
|
||||
|
||||
const fetched = await store.getTask(created.id);
|
||||
expect(fetched.nodeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("persists nodeId across store reload", async () => {
|
||||
const diskRoot = makeTmpDir();
|
||||
const diskGlobal = makeTmpDir();
|
||||
@@ -102,4 +117,14 @@ describe("task node override persistence", () => {
|
||||
expect(tasks.find((task) => task.id === second.id)?.nodeId).toBe("node-two");
|
||||
expect(tasks.find((task) => task.id === third.id)?.nodeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("persists different nodeId values independently across multiple tasks", async () => {
|
||||
const first = await store.createTask({ description: "Node alpha", nodeId: "node-alpha" });
|
||||
const second = await store.createTask({ description: "Node beta", nodeId: "node-beta" });
|
||||
const third = await store.createTask({ description: "No override" });
|
||||
|
||||
expect((await store.getTask(first.id)).nodeId).toBe("node-alpha");
|
||||
expect((await store.getTask(second.id)).nodeId).toBe("node-beta");
|
||||
expect((await store.getTask(third.id)).nodeId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,49 @@ describe("unavailableNodePolicy settings contract", () => {
|
||||
expect(validateUnavailableNodePolicy("block")).toBe("block");
|
||||
expect(validateUnavailableNodePolicy("fallback-local")).toBe("fallback-local");
|
||||
expect(validateUnavailableNodePolicy(undefined)).toBeUndefined();
|
||||
expect(validateUnavailableNodePolicy("")).toBeUndefined();
|
||||
expect(validateUnavailableNodePolicy(null)).toBeUndefined();
|
||||
expect(validateUnavailableNodePolicy("fallback-remote")).toBeUndefined();
|
||||
expect(validateUnavailableNodePolicy(42)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultNodeId settings contract", () => {
|
||||
it("defaults defaultNodeId to undefined", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.defaultNodeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("classifies defaultNodeId as project-only scope", () => {
|
||||
expect(isProjectSettingsKey("defaultNodeId")).toBe(true);
|
||||
expect(isGlobalSettingsKey("defaultNodeId")).toBe(false);
|
||||
});
|
||||
|
||||
it("includes defaultNodeId in PROJECT_SETTINGS_KEYS", () => {
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("defaultNodeId");
|
||||
});
|
||||
|
||||
it("allows defaultNodeId to be a string or undefined", () => {
|
||||
const withDefaultNode = {
|
||||
...DEFAULT_PROJECT_SETTINGS,
|
||||
defaultNodeId: "node-primary",
|
||||
};
|
||||
const withoutDefaultNode = {
|
||||
...DEFAULT_PROJECT_SETTINGS,
|
||||
defaultNodeId: undefined,
|
||||
};
|
||||
|
||||
expect(withDefaultNode.defaultNodeId).toBe("node-primary");
|
||||
expect(withoutDefaultNode.defaultNodeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("round-trips defaultNodeId via object serialization", () => {
|
||||
const settings = {
|
||||
...DEFAULT_PROJECT_SETTINGS,
|
||||
defaultNodeId: "node-persisted",
|
||||
};
|
||||
|
||||
const roundTripped = JSON.parse(JSON.stringify(settings)) as typeof settings;
|
||||
|
||||
expect(roundTripped.defaultNodeId).toBe("node-persisted");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user