feat(FN-4018): isolate split merger temp workspaces with settings persisten
Merged FN-4014 to fix dual-scope GitHub tracking settings persistence and validation (mission vs task scope), scoped repo saves to the relevant section, and documented the dual defaults. Also landed FN-4018 to isolate split merger temp workspaces and add regression checks for split-suite test isolat Fusion-Task-Id: FN-4018
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
DEFAULT_PROJECT_SETTINGS,
|
||||
GLOBAL_SETTINGS_KEYS,
|
||||
PROJECT_SETTINGS_KEYS,
|
||||
isGlobalOnlySettingsKey,
|
||||
isGlobalSettingsKey,
|
||||
isProjectSettingsKey,
|
||||
} from "../types.js";
|
||||
@@ -95,6 +96,8 @@ describe("settings key parity", () => {
|
||||
expect(isGlobalSettingsKey("githubAuthToken")).toBe(false);
|
||||
expect(isProjectSettingsKey("githubTrackingDefaultRepo")).toBe(true);
|
||||
expect(isGlobalSettingsKey("githubTrackingDefaultRepo")).toBe(true);
|
||||
expect(isGlobalOnlySettingsKey("githubTrackingDefaultRepo")).toBe(false);
|
||||
expect(isGlobalOnlySettingsKey("themeMode")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps remoteAccess scoped to global settings only", () => {
|
||||
|
||||
@@ -554,6 +554,33 @@ describe("TaskStore", () => {
|
||||
// ── Scope Separation Regression Tests (FN-1729) ───────────────────────────
|
||||
|
||||
describe("scope separation regression", () => {
|
||||
it("keeps dual-scope githubTrackingDefaultRepo in project scope while excluding global-only keys", async () => {
|
||||
await harness.store().updateGlobalSettings({
|
||||
githubTrackingDefaultRepo: "global/default",
|
||||
themeMode: "light",
|
||||
});
|
||||
|
||||
await harness.store().updateSettings({
|
||||
githubTrackingDefaultRepo: "project/default",
|
||||
themeMode: "dark",
|
||||
});
|
||||
|
||||
const merged = await harness.store().getSettings();
|
||||
const mergedFast = await harness.store().getSettingsFast();
|
||||
const { global, project } = await harness.store().getSettingsByScope();
|
||||
const scopedFast = await harness.store().getSettingsByScopeFast();
|
||||
|
||||
expect(merged.githubTrackingDefaultRepo).toBe("project/default");
|
||||
expect(mergedFast.githubTrackingDefaultRepo).toBe("project/default");
|
||||
expect(project.githubTrackingDefaultRepo).toBe("project/default");
|
||||
expect(scopedFast.project.githubTrackingDefaultRepo).toBe("project/default");
|
||||
expect(global.githubTrackingDefaultRepo).toBe("global/default");
|
||||
expect(scopedFast.global.githubTrackingDefaultRepo).toBe("global/default");
|
||||
|
||||
expect((project as Record<string, unknown>).themeMode).toBeUndefined();
|
||||
expect((scopedFast.project as Record<string, unknown>).themeMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("getSettingsByScope: global lane keys never appear in project scope", async () => {
|
||||
await harness.store().updateGlobalSettings({
|
||||
executionGlobalProvider: "anthropic",
|
||||
|
||||
@@ -346,3 +346,7 @@ export function isGlobalSettingsKey(key: string): key is keyof GlobalSettings {
|
||||
export function isProjectSettingsKey(key: string): key is keyof ProjectSettings {
|
||||
return (PROJECT_SETTINGS_KEYS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
export function isGlobalOnlySettingsKey(key: string): key is keyof GlobalSettings {
|
||||
return isGlobalSettingsKey(key) && !isProjectSettingsKey(key);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
||||
import { existsSync, watch, type FSWatcher } from "node:fs";
|
||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType, WorkflowStepTemplate, Agent, AutostashOrphanRecord, TaskCommitAssociation, TaskCommitAssociationMatchSource, TaskCommitAssociationConfidence } from "./types.js";
|
||||
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
|
||||
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalSettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
|
||||
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalOnlySettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
|
||||
import { normalizeTaskPriority } from "./task-priority.js";
|
||||
import { canAgentTakeImplementationTask } from "./agent-role-policy.js";
|
||||
import { GlobalSettingsStore } from "./global-settings.js";
|
||||
@@ -1783,7 +1783,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// Strip global-only keys from project-level settings so stale project-scoped
|
||||
// values don't override the correct global value during the spread merge.
|
||||
const projectSettings = Object.fromEntries(
|
||||
Object.entries(config.settings ?? {}).filter(([key]) => !isGlobalSettingsKey(key)),
|
||||
Object.entries(config.settings ?? {}).filter(([key]) => !isGlobalOnlySettingsKey(key)),
|
||||
);
|
||||
return canonicalizeSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
@@ -1818,7 +1818,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// always done this; getSettingsFast() was missing the filter.
|
||||
const projectSettings: Partial<Settings> | undefined = raw
|
||||
? (Object.fromEntries(
|
||||
Object.entries(raw).filter(([key]) => !isGlobalSettingsKey(key)),
|
||||
Object.entries(raw).filter(([key]) => !isGlobalOnlySettingsKey(key)),
|
||||
) as Partial<Settings>)
|
||||
: undefined;
|
||||
|
||||
@@ -1846,7 +1846,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const projectSettings: Partial<ProjectSettings> = {};
|
||||
if (config.settings) {
|
||||
for (const key of Object.keys(config.settings)) {
|
||||
if (!isGlobalSettingsKey(key)) {
|
||||
if (!isGlobalOnlySettingsKey(key)) {
|
||||
(projectSettings as Record<string, unknown>)[key] = (config.settings as Record<string, unknown>)[key];
|
||||
}
|
||||
}
|
||||
@@ -1881,7 +1881,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const projectScoped: Partial<ProjectSettings> = {};
|
||||
if (projectSettings) {
|
||||
for (const key of Object.keys(projectSettings)) {
|
||||
if (!isGlobalSettingsKey(key)) {
|
||||
if (!isGlobalOnlySettingsKey(key)) {
|
||||
(projectScoped as Record<string, unknown>)[key] = (projectSettings as Record<string, unknown>)[key];
|
||||
}
|
||||
}
|
||||
@@ -1904,7 +1904,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// Filter out global-only fields — they should go through updateGlobalSettings()
|
||||
const projectPatch: Partial<Settings> = {};
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (!isGlobalSettingsKey(key)) {
|
||||
if (!isGlobalOnlySettingsKey(key)) {
|
||||
(projectPatch as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2469,6 +2469,7 @@ export {
|
||||
DEFAULT_SETTINGS,
|
||||
GLOBAL_SETTINGS_KEYS,
|
||||
PROJECT_SETTINGS_KEYS,
|
||||
isGlobalOnlySettingsKey,
|
||||
isGlobalSettingsKey,
|
||||
isProjectSettingsKey,
|
||||
} from "./settings-schema.js";
|
||||
|
||||
Reference in New Issue
Block a user