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:
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user