test(cli): refresh task command store fixtures (#3459)

## Summary
- keep CLI task command store mocks aligned with required settings and
retry-budget capabilities
- preserve resolved-store identity while filling production-shaped test
capabilities
- update task-create assertions for synchronous post-create tracking
ownership

## Test plan
- `corepack pnpm --filter @runfusion/fusion exec vitest run
src/commands/__tests__/task.test.ts --silent=passed-only --reporter=dot`
- `corepack pnpm --filter @runfusion/fusion typecheck`
- `corepack pnpm lint`

## CI note
- Build, Gate, Typecheck, Desktop packaging, CodeRabbit, and Greptile
pass on head `6632fd7e4`.
- Lint reaches the lifecycle-column ratchet, then hits current-main
mailbox census drift already fixed by green PR #3440.

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
Phil Larson
2026-08-15 21:43:53 -07:00
committed by GitHub
parent 59dc5df5f4
commit aedcb681b9

View File

@@ -6,6 +6,13 @@ Task create/link success lines use `result()` so they survive quiet mode.
Capture that seam for assertions that previously spied console.log.
*/
const resultSpy = vi.hoisted(() => vi.fn());
const requiredTaskStoreCapabilities = vi.hoisted(() => () => ({
getSettings: vi.fn().mockResolvedValue({}),
getGlobalSettingsStore: vi.fn().mockReturnValue({
getSettings: vi.fn().mockResolvedValue({}),
}),
resetTerminalFailureAutoRecoveryBudget: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../output.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../output.js")>();
return {
@@ -67,7 +74,13 @@ vi.mock("@fusion/core", async (importActual) => {
const taskStoreMockImplementation = TaskStoreMock.mockImplementation.bind(TaskStoreMock);
TaskStoreMock.mockImplementation = ((impl: (...args: any[]) => unknown) =>
taskStoreMockImplementation(function (this: unknown, ...args: any[]) {
return impl(...args);
const instance = impl(...args);
if (typeof instance === "object" && instance !== null) {
for (const [name, capability] of Object.entries(requiredTaskStoreCapabilities())) {
if (!(name in instance)) Object.assign(instance, { [name]: capability });
}
}
return instance;
})) as typeof TaskStoreMock.mockImplementation;
const CentralCoreMock = vi.fn(function () {});
@@ -184,9 +197,25 @@ vi.mock("@fusion/core/gh-cli", () => ({
}));
// Mock project-context
vi.mock("../../project-context.js", () => ({
vi.mock("../../project-context.js", () => {
const resolveProjectMock = vi.fn();
const withRequiredStoreCapabilities = (context: any) => {
for (const [name, capability] of Object.entries(requiredTaskStoreCapabilities())) {
if (!(name in context.store)) Object.assign(context.store, { [name]: capability });
}
return context;
};
const mockResolvedValue = resolveProjectMock.mockResolvedValue.bind(resolveProjectMock);
const mockResolvedValueOnce = resolveProjectMock.mockResolvedValueOnce.bind(resolveProjectMock);
resolveProjectMock.mockResolvedValue = ((context: any) =>
mockResolvedValue(withRequiredStoreCapabilities(context))) as typeof resolveProjectMock.mockResolvedValue;
resolveProjectMock.mockResolvedValueOnce = ((context: any) =>
mockResolvedValueOnce(withRequiredStoreCapabilities(context))) as typeof resolveProjectMock.mockResolvedValueOnce;
resolveProjectMock.mockRejectedValue(new Error("No project context"));
return {
resolveProjectPathOnly: vi.fn(async () => process.cwd()),
resolveProject: vi.fn().mockRejectedValue(new Error("No project context")),
resolveProject: resolveProjectMock,
getStore: vi.fn().mockResolvedValue({}),
getDefaultProject: vi.fn().mockResolvedValue(undefined),
setDefaultProject: vi.fn().mockResolvedValue(undefined),
@@ -222,7 +251,8 @@ vi.mock("../../project-context.js", () => ({
isRegistered: false,
store,
})),
}));
};
});
import { createInterface } from "node:readline/promises";
import { TaskStore, CentralCore, extractIntentSignature, findNearDuplicates, runDeterministicDuplicateGuard, reconcileDeterministicDuplicate, TaskIsLiveError } from "@fusion/core";
@@ -538,11 +568,11 @@ vi.mock("node:fs/promises", () => ({
}));
/*
FNXC:OriginWorkflowSelection 2026-07-26-19:40:
Every partial TaskStore mock below stubs `resolveOriginWorkflowOverrideId`: `runTaskCreate`
consults it to honor the project `taskCreateWorkflowId` setting, and these mocks are structural
partials, so a missing method is a TypeError rather than a fallback. `undefined` is the
unconfigured answer — CLI create then takes its unchanged project-default path.
FNXC:OriginWorkflowSelection 2026-08-15-01:12:
Every partial TaskStore mock below stubs `resolveOriginWorkflowOverrideId`; the central constructor and project-context wrappers also preserve the global-settings and retry-budget capabilities now required by CLI creation and manual retry. `undefined` keeps the unchanged project-default workflow path.
FNXC:CliTests 2026-08-15-01:12:
CLI create fixture assertions must include `{ invokeTaskCreatedHook: false }`; the command synchronously owns post-create tracking work and suppresses the deferred store hook to avoid duplicate or lost issue creation.
*/
describe("project-aware task command behavior", () => {
afterEach(() => {
@@ -649,7 +679,7 @@ describe("project-aware task command behavior", () => {
description: "test task",
dependencies: undefined,
source: { sourceType: "cli", sourceMetadata: { contentFingerprint: "fp-1" } },
});
}, { invokeTaskCreatedHook: false });
expect(logSpy.mock.calls.some((call) => String(call[0]).includes("Project: demo-project"))).toBe(true);
logSpy.mockRestore();
@@ -671,7 +701,10 @@ describe("project-aware task command behavior", () => {
await runTaskCreate("default task");
expect(resolveProject).toHaveBeenCalledWith(undefined);
expect(mockCreateTask).toHaveBeenCalledWith({ description: "default task", dependencies: undefined, source: { sourceType: "cli", sourceMetadata: undefined } });
expect(mockCreateTask).toHaveBeenCalledWith(
{ description: "default task", dependencies: undefined, source: { sourceType: "cli", sourceMetadata: undefined } },
{ invokeTaskCreatedHook: false },
);
});
it("runTaskCreate without project flag falls back to TaskStore(process.cwd()) when resolution fails", async () => {
@@ -687,6 +720,7 @@ describe("project-aware task command behavior", () => {
);
vi.mocked(createLocalStore).mockResolvedValueOnce({
...requiredTaskStoreCapabilities(),
init,
resolveOriginWorkflowOverrideId: vi.fn().mockResolvedValue(undefined), createTask: mockCreateTask,
addAttachment: vi.fn(),
@@ -698,7 +732,10 @@ describe("project-aware task command behavior", () => {
expect(resolveProject).toHaveBeenCalledWith(undefined);
expect(createLocalStore).toHaveBeenCalledWith("/current/project");
expect(mockCreateTask).toHaveBeenCalledWith({ description: "local task", dependencies: undefined, source: { sourceType: "cli", sourceMetadata: { contentFingerprint: "fp-local" } } });
expect(mockCreateTask).toHaveBeenCalledWith(
{ description: "local task", dependencies: undefined, source: { sourceType: "cli", sourceMetadata: { contentFingerprint: "fp-local" } } },
{ invokeTaskCreatedHook: false },
);
cwdSpy.mockRestore();
});
@@ -829,13 +866,16 @@ describe("project-aware task command behavior", () => {
await runTaskCreate("Investigate /pr/options /pr/preflight flow");
expect(mockCreateTask).toHaveBeenCalledWith(expect.objectContaining({
source: expect.objectContaining({
sourceMetadata: expect.objectContaining({
intentSignature: expect.objectContaining({ routePaths: ["/pr/options", "/pr/preflight"] }),
expect(mockCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
source: expect.objectContaining({
sourceMetadata: expect.objectContaining({
intentSignature: expect.objectContaining({ routePaths: ["/pr/options", "/pr/preflight"] }),
}),
}),
}),
}));
{ invokeTaskCreatedHook: false },
);
expect(close).toHaveBeenCalled();
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: originalInTTY });
@@ -914,14 +954,17 @@ describe("project-aware task command behavior", () => {
expect(runDeterministicDuplicateGuard).toHaveBeenCalledWith(expect.anything(), { description: "same task" }, expect.objectContaining({ bypass: true }));
expect(findNearDuplicates).not.toHaveBeenCalled();
expect(extractIntentSignature).toHaveBeenCalledWith({ description: "same task" });
expect(mockCreateTask).toHaveBeenCalledWith(expect.objectContaining({
source: expect.objectContaining({
sourceMetadata: expect.objectContaining({
contentFingerprint: "fp-no-dedup",
intentSignature: expect.objectContaining({ routePaths: ["/pr/options", "/pr/preflight"] }),
expect(mockCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
source: expect.objectContaining({
sourceMetadata: expect.objectContaining({
contentFingerprint: "fp-no-dedup",
intentSignature: expect.objectContaining({ routePaths: ["/pr/options", "/pr/preflight"] }),
}),
}),
}),
}));
{ invokeTaskCreatedHook: false },
);
});
it("runTaskCreate fails open when listTasks throws during near-duplicate checking", async () => {
@@ -1729,8 +1772,8 @@ describe("runTaskCreate with --depends", () => {
expect(mockCreateTask).toHaveBeenCalledWith({
description: "test task",
dependencies: ["FN-124"],
source: { sourceType: "cli" },
});
source: { sourceType: "cli", sourceMetadata: undefined },
}, { invokeTaskCreatedHook: false });
});
it("passes multiple dependencies correctly", async () => {
@@ -1739,8 +1782,8 @@ describe("runTaskCreate with --depends", () => {
expect(mockCreateTask).toHaveBeenCalledWith({
description: "test task",
dependencies: ["FN-124", "FN-100"],
source: { sourceType: "cli" },
});
source: { sourceType: "cli", sourceMetadata: undefined },
}, { invokeTaskCreatedHook: false });
const depsLine = logSpy.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("Dependencies:"),
@@ -1756,8 +1799,8 @@ describe("runTaskCreate with --depends", () => {
expect(mockCreateTask).toHaveBeenCalledWith({
description: "test task",
dependencies: undefined,
source: { sourceType: "cli" },
});
source: { sourceType: "cli", sourceMetadata: undefined },
}, { invokeTaskCreatedHook: false });
});
});