Files
fusion/packages/engine/src/__tests__/executor-review-artifacts.test.ts
Fusion Agent 7563fcfe1b FN-037: configure AI-authored task output language
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>
2026-08-20 03:57:23 +00:00

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" });
}
});
});