Configure a persisted output-language setting and apply it across AI-authored task text and workflow summaries. - Add project/global output-language settings with defaults, validation, UI controls, translations, and documentation. - Propagate the selected language through planning, execution, review, summaries, and task creation. - Add focused coverage for language resolution and update affected dashboard, engine, and localization tests. Files changed: .changeset/fn-037-task-output-language.md | 7 ++ docs/settings-reference.md | 4 + docs/task-management.md | 4 + .../core/src/__tests__/ai-output-language.test.ts | 21 +++++ packages/core/src/ai/ai-output-language.ts | 42 +++++++++ packages/core/src/ai/ai-summarize.ts | 16 +++- packages/core/src/config/settings-schema.ts | 2 + packages/core/src/index.gate.ts | 2 + packages/core/src/index.ts | 2 + packages/core/src/task-store/settings-ops.ts | 7 ++ packages/core/src/task-store/task-creation.ts | 4 + packages/core/src/types/settings/settings-scope.ts | 2 + .../app/__tests__/settings-sections.test.tsx | 21 +++-- .../__tests__/SettingsModal.models-auth.test.tsx | 10 +- .../app/components/settings/section-keys.ts | 2 +- .../sections/ProjectModelsSection.search.ts | 13 ++- .../settings/sections/ProjectModelsSection.tsx | 28 ++++-- .../settings-default-descriptions.test.tsx | 4 +- .../src/__tests__/github-tracking.test.ts | 4 +- .../__tests__/planning-prompt-resolution.test.ts | 29 ++++-- .../src/__tests__/routes-planning.test.ts | 32 ++++++- packages/dashboard/src/ai-session-store.ts | 15 ++- packages/dashboard/src/github-tracking.ts | 3 + packages/dashboard/src/planning.ts | 75 +++++++++++++-- .../routes/register-ai-text-assistant-routes.ts | 11 ++- .../src/routes/register-planning-subtask-routes.ts | 6 +- .../src/routes/register-task-workflow-routes.ts | 9 +- packages/dashboard/src/shared/settings-sections.ts | 2 +- .../__tests__/ce-workflow-step-executor.test.ts | 45 ++++++++- .../__tests__/executor-review-artifacts.test.ts | 65 +++++++++++++ packages/engine/src/__tests__/triage.test.ts | 7 +- .../src/__tests__/workflow-task-runtime.test.ts | 23 +++++ .../create-authoritative-workflow-primitives.ts | 14 ++- .../create-authoritative-workflow-seams.ts | 17 +++- .../engine/src/executor/execute-workflow-graph.ts | 19 +++- .../engine/src/executor/execute-workflow-step.ts | 13 ++- packages/engine/src/executor/execution-prompt.ts | 10 +- .../engine/src/executor/handoff-task-to-review.ts | 10 +- .../engine/src/executor/run-graph-custom-node.ts | 6 +- .../src/executor/task-executor-graph-facades.ts | 8 +- packages/engine/src/triage.ts | 35 ++----- .../src/workflows/workflow-completion-summary.ts | 101 ++++++++++++++++++--- .../engine/src/workflows/workflow-task-runtime.ts | 12 +++ packages/i18n/locales/en/app.json | 7 +- packages/i18n/locales/es/app.json | 7 +- packages/i18n/locales/fr/app.json | 7 +- packages/i18n/locales/ko/app.json | 7 +- packages/i18n/locales/pt-BR/app.json | 14 ++- packages/i18n/locales/zh-CN/app.json | 7 +- packages/i18n/locales/zh-TW/app.json | 7 +- packages/i18n/src/resources.d.ts | 13 ++- 51 files changed, 680 insertions(+), 151 deletions(-) Fusion-Task-Id: FN-037 Fusion-Task-Lineage: 968a707e-7ffe-43ba-b983-d679b031f7a5 Co-authored-by: Fusion <noreply@runfusion.ai>
110 lines
6.0 KiB
TypeScript
110 lines
6.0 KiB
TypeScript
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { resolveTaskOutputLanguage } from "@fusion/core";
|
|
import { generateFeatureVideo, type FeatureVideoBrowserClient } from "../review-artifacts/feature-video.js";
|
|
import { TaskExecutor } from "../executor.js";
|
|
import { handoffTaskToReview } from "../executor/handoff-task-to-review.js";
|
|
import { createAuthoritativeWorkflowPrimitivesFromExecutor } from "../executor/create-authoritative-workflow-primitives.js";
|
|
import { createAuthoritativeWorkflowSeams } from "../executor/create-authoritative-workflow-seams.js";
|
|
|
|
const cleanup: string[] = [];
|
|
afterEach(async () => { await Promise.all(cleanup.splice(0).map((dir) => import("node:fs/promises").then(({ rm }) => rm(dir, { recursive: true, force: true })))); });
|
|
|
|
describe("TaskExecutor feature-video completion handoff", () => {
|
|
const task = { id: "FN-video", description: "user-facing", column: "in-progress", dependencies: [], steps: [], currentStep: 0 } as any;
|
|
|
|
function makeStore() {
|
|
return {
|
|
on: vi.fn(), getSettings: vi.fn().mockResolvedValue({ reviewArtifacts: "on" }), getTask: vi.fn().mockResolvedValue({ ...task, prompt: "**Review Artifacts:** on" }),
|
|
getTaskDocument: vi.fn().mockResolvedValue({ content: JSON.stringify({ baseUrl: "http://127.0.0.1:5173", targetRoute: "/" }) }),
|
|
registerArtifact: vi.fn().mockResolvedValue({ id: "video-1" }),
|
|
handoffToReview: vi.fn().mockResolvedValue({ ...task, column: "in-review" }),
|
|
} as any;
|
|
}
|
|
|
|
it("runs the injectable capture seam before handing completed work to review", async () => {
|
|
const store = makeStore();
|
|
const dir = await mkdtemp(join(tmpdir(), "executor-video-test-"));
|
|
cleanup.push(dir);
|
|
const videoPath = join(dir, "recording.webm");
|
|
await writeFile(videoPath, "webm");
|
|
const client: FeatureVideoBrowserClient = { launch: vi.fn().mockResolvedValue({ newContext: async () => ({ newPage: async () => ({ goto: async () => undefined, video: () => ({ path: async () => videoPath }) }), close: async () => undefined }), close: async () => undefined }) };
|
|
const capture = vi.fn((options) => generateFeatureVideo({ ...options, client, sleep: async () => undefined }));
|
|
const executor = new TaskExecutor(store, "/repo", { reviewArtifactGenerator: capture });
|
|
await (executor as any).handoffTaskToReview(task, "fn_task_done");
|
|
expect(capture).toHaveBeenCalledWith(expect.objectContaining({ task: expect.objectContaining({ id: task.id }), settings: { reviewArtifacts: "on" } }));
|
|
expect(store.registerArtifact).toHaveBeenCalledWith(expect.objectContaining({ type: "video", taskId: task.id }));
|
|
expect(store.handoffToReview).toHaveBeenCalledWith(task.id, expect.any(Object));
|
|
});
|
|
|
|
it("preserves completion handoff when capture rejects", async () => {
|
|
const store = makeStore();
|
|
const executor = new TaskExecutor(store, "/repo", { reviewArtifactGenerator: vi.fn().mockRejectedValue(new Error("browser crashed")) });
|
|
await expect((executor as any).handoffTaskToReview(task, "fn_task_done")).resolves.toMatchObject({ column: "in-review" });
|
|
expect(store.handoffToReview).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("uses the graph-start language snapshot for a missing review-handoff summary", async () => {
|
|
const updates: Array<{ taskId: string; summary: string }> = [];
|
|
const store = {
|
|
/* FNXC:TaskOutputLanguage 2026-08-19-16:14: This is the later selector state; the fallback must not read it. */
|
|
getSettings: vi.fn().mockResolvedValue({ taskOutputLanguage: "interface", language: "fr" }),
|
|
updateTask: vi.fn(async (taskId, update) => updates.push({ taskId, summary: update.summary })),
|
|
logEntry: vi.fn(),
|
|
handoffToReview: vi.fn().mockResolvedValue({ ...task, column: "in-review" }),
|
|
} as any;
|
|
const outputLanguage = resolveTaskOutputLanguage(
|
|
{ taskOutputLanguage: "english" },
|
|
"Déployer le flux.",
|
|
);
|
|
|
|
await handoffTaskToReview({
|
|
store,
|
|
getRunContextFor: () => undefined,
|
|
generateCompletionFeatureVideo: vi.fn(),
|
|
}, {
|
|
...task,
|
|
description: "Déployer le flux.",
|
|
title: "Deploy workflow",
|
|
}, "workflow-graph-review", undefined, outputLanguage);
|
|
|
|
expect(updates).toEqual([{
|
|
taskId: task.id,
|
|
summary: expect.stringContaining("Workflow completed: Deploy workflow."),
|
|
}]);
|
|
expect(store.getSettings).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("preserves graph-start input language when review handoff sees edited task settings and description", async () => {
|
|
const started = resolveTaskOutputLanguage(
|
|
{ taskOutputLanguage: "input" },
|
|
"Bonjour, ceci est une demande détaillée pour déployer le flux de validation du projet.",
|
|
);
|
|
const live = { ...task, description: "Necesito desplegar el flujo de validación del proyecto en español.", title: "Deploy validation" };
|
|
const handoffTaskToReview = vi.fn().mockResolvedValue({ ...live, column: "in-review" });
|
|
const deps = {
|
|
store: { getTask: vi.fn().mockResolvedValue(live) },
|
|
persistTokenUsage: vi.fn(),
|
|
handoffTaskToReview,
|
|
getRunContextFor: () => undefined,
|
|
} as any;
|
|
// This emulates a graph yielding after its French input snapshot while an operator changes
|
|
// both description and selector to the later Spanish/interface-French state.
|
|
const laterSettings = { taskOutputLanguage: "interface", language: "fr" } as any;
|
|
|
|
const primitives = createAuthoritativeWorkflowPrimitivesFromExecutor(deps, laterSettings, started);
|
|
await primitives.runReview({ node: { context: {} } } as any, live, { type: "code" } as any);
|
|
const seams = createAuthoritativeWorkflowSeams(deps, laterSettings, started);
|
|
await seams.review?.(live);
|
|
await seams["review-handoff"]?.(live);
|
|
|
|
expect(handoffTaskToReview).toHaveBeenCalledTimes(3);
|
|
for (const args of handoffTaskToReview.mock.calls) {
|
|
expect(args[3]).toBe(started);
|
|
expect(args[3]).toMatchObject({ mode: "input", locale: "fr" });
|
|
}
|
|
});
|
|
});
|