From b93d326fb29932ea1e82d8ad6252eccdd8d0bdc1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 17:00:30 +0200 Subject: [PATCH] fix(settings): preserve conductor migration context --- .../import/sources/conductor/importer.test.ts | 88 ++++++++++++++++++- .../import/sources/conductor/importer.ts | 77 ++++++++++++++-- 2 files changed, 152 insertions(+), 13 deletions(-) diff --git a/packages/server/src/server/session/project-config/import/sources/conductor/importer.test.ts b/packages/server/src/server/session/project-config/import/sources/conductor/importer.test.ts index c76ff7de4..402198cfc 100644 --- a/packages/server/src/server/session/project-config/import/sources/conductor/importer.test.ts +++ b/packages/server/src/server/session/project-config/import/sources/conductor/importer.test.ts @@ -141,7 +141,7 @@ enabled = true expect(JSON.stringify(preview.items)).not.toContain("do-not-return"); }); - test("local TOML overrides shared scripts and legacy JSON is ignored when shared TOML exists", () => { + test("merges legacy scoped JSON before TOML and lets local TOML win", () => { const repo = makeRepo(); writeSharedToml( repo, @@ -161,7 +161,9 @@ setup = "local setup" command = "local dev" `, ); - writeSharedJson(repo, { scripts: { setup: "scoped legacy shared setup" } }); + writeSharedJson(repo, { + scripts: { setup: "scoped legacy shared setup", archive: "scoped legacy archive" }, + }); writeLocalJson(repo, { scripts: { setup: "scoped legacy local setup" } }); writeFileSync( join(repo, "conductor.json"), @@ -171,11 +173,13 @@ command = "local dev" const preview = inspect(repo); expect(preview.inputs).toEqual([ + { role: "shared", relativePath: ".conductor/settings.json" }, { role: "shared", relativePath: ".conductor/settings.toml" }, + { role: "local", relativePath: ".conductor/settings.local.json" }, { role: "local", relativePath: ".conductor/settings.local.toml" }, ]); expect(preview.preview).toMatchObject({ - worktree: { setup: "local setup" }, + worktree: { setup: "local setup", teardown: "scoped legacy archive" }, scripts: { dev: { command: "local dev" } }, }); }); @@ -508,6 +512,75 @@ default_branch = "develop" ); }); + test("preserves shared environment variable names under local scoped overrides", () => { + const repo = makeRepo(); + writeSharedToml( + repo, + ` +[environment_variables.local] +SHARED_TOKEN = "shared-secret" +`, + ); + writeLocalToml( + repo, + ` +[environment_variables.cloud] +CLOUD_TOKEN = "cloud-secret" +`, + ); + + expect(inspect(repo).items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: "environment_variables", + detail: "Environment variable values are not imported. Found: CLOUD_TOKEN, SHARED_TOKEN.", + }), + ]), + ); + }); + + test("reports legacy run mode, privacy, and harness settings", () => { + const repo = makeRepo(); + writeFileSync( + join(repo, "conductor.json"), + JSON.stringify({ + scripts: { setup: "npm ci" }, + runScriptMode: "nonconcurrent", + enterpriseDataPrivacy: true, + claude_code_executable_path: "/opt/claude", + codex_executable_path: "/opt/codex", + claude_provider: "bedrock", + bedrock_region: "eu-west-1", + vertex_project_id: "project", + ssh_key_path: "~/.ssh/id_ed25519", + }), + ); + + expect(inspect(repo).items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: "runScriptMode", outcome: "unsupported" }), + expect.objectContaining({ key: "enterpriseDataPrivacy", outcome: "unsupported" }), + expect.objectContaining({ key: "claude_code_executable_path", outcome: "unsupported" }), + expect.objectContaining({ key: "codex_executable_path", outcome: "unsupported" }), + expect.objectContaining({ key: "claude_provider", outcome: "unsupported" }), + expect.objectContaining({ key: "bedrock_region", outcome: "unsupported" }), + expect.objectContaining({ key: "vertex_project_id", outcome: "unsupported" }), + expect.objectContaining({ key: "ssh_key_path", outcome: "unsupported" }), + ]), + ); + }); + + test("reports snake-case enterprise data privacy settings", () => { + const repo = makeRepo(); + writeSharedToml(repo, "enterprise_data_privacy = true\n"); + + expect(inspect(repo).items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: "enterprise_data_privacy", outcome: "unsupported" }), + ]), + ); + }); + test("reports default and icon fields on imported run scripts", () => { const repo = makeRepo(); writeSharedToml( @@ -588,7 +661,12 @@ command = "npm run other -- --port $CONDUCTOR_PORT" writeSharedToml(repo, '[scripts]\nsetup = "npm ci"\n'); writeFileSync(join(repo, ".worktreeinclude"), "config/*.local\n"); - expect(inspect(repo).items).toEqual( + const initialPreview = inspect(repo); + expect(initialPreview.inputs).toEqual([ + { role: "shared", relativePath: ".conductor/settings.toml" }, + { role: "include", relativePath: ".worktreeinclude" }, + ]); + expect(initialPreview.items).toEqual( expect.arrayContaining([ expect.objectContaining({ key: ".worktreeinclude", @@ -597,5 +675,7 @@ command = "npm run other -- --port $CONDUCTOR_PORT" }), ]), ); + writeFileSync(join(repo, ".worktreeinclude"), "config/*.local\nsecrets/*.local\n"); + expect(inspect(repo).sourceRevision).not.toBe(initialPreview.sourceRevision); }); }); diff --git a/packages/server/src/server/session/project-config/import/sources/conductor/importer.ts b/packages/server/src/server/session/project-config/import/sources/conductor/importer.ts index 82f662496..3fd68b555 100644 --- a/packages/server/src/server/session/project-config/import/sources/conductor/importer.ts +++ b/packages/server/src/server/session/project-config/import/sources/conductor/importer.ts @@ -17,6 +17,7 @@ interface SourceFile { relativePath: string; path: string; bytes: string; + containsSettings: boolean; } interface ConductorSettings { @@ -30,6 +31,9 @@ interface ConductorSettings { file_include_globs?: unknown; environment_variables?: unknown; environment_variables_forward?: unknown; + runScriptMode?: unknown; + enterprise_data_privacy?: unknown; + enterpriseDataPrivacy?: unknown; prompts?: unknown; git?: unknown; spotlight_testing?: unknown; @@ -103,29 +107,42 @@ function discoverConductorSources(repoRoot: string): SourceFile[] { const sharedTomlPath = join(repoRoot, ".conductor", "settings.toml"); const sharedJsonPath = join(repoRoot, ".conductor", "settings.json"); const rootLegacyPath = join(repoRoot, "conductor.json"); + const worktreeIncludePath = join(repoRoot, ".worktreeinclude"); const files: SourceFile[] = []; + if (existsSync(sharedJsonPath)) { + files.push(readSourceFile(repoRoot, sharedJsonPath, "shared", true)); + } if (existsSync(sharedTomlPath)) { - files.push(readSourceFile(repoRoot, sharedTomlPath, "shared")); - } else if (existsSync(sharedJsonPath)) { - files.push(readSourceFile(repoRoot, sharedJsonPath, "shared")); - } else if (existsSync(rootLegacyPath)) { - files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy")); + files.push(readSourceFile(repoRoot, sharedTomlPath, "shared", true)); + } + if (!existsSync(sharedJsonPath) && !existsSync(sharedTomlPath) && existsSync(rootLegacyPath)) { + files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy", true)); + } + if (existsSync(localJsonPath)) { + files.push(readSourceFile(repoRoot, localJsonPath, "local", true)); } if (existsSync(localTomlPath)) { - files.push(readSourceFile(repoRoot, localTomlPath, "local")); - } else if (existsSync(localJsonPath)) { - files.push(readSourceFile(repoRoot, localJsonPath, "local")); + files.push(readSourceFile(repoRoot, localTomlPath, "local", true)); + } + if (existsSync(worktreeIncludePath)) { + files.push(readSourceFile(repoRoot, worktreeIncludePath, "include", false)); } return files; } -function readSourceFile(repoRoot: string, path: string, role: string): SourceFile { +function readSourceFile( + repoRoot: string, + path: string, + role: string, + containsSettings: boolean, +): SourceFile { return { role, relativePath: relative(repoRoot, path).replaceAll("\\", "/"), path, bytes: readFileSync(path, "utf8"), + containsSettings, }; } @@ -135,6 +152,9 @@ function loadConductorSettings( ): ConductorSettings { let merged: ConductorSettings = {}; for (const file of sourceFiles) { + if (!file.containsSettings) { + continue; + } let parsed: unknown; try { parsed = file.relativePath.endsWith(".json") ? JSON.parse(file.bytes) : parseToml(file.bytes); @@ -158,9 +178,28 @@ function mergeSettings(base: ConductorSettings, override: ConductorSettings): Co ...(isRecord(override.scripts) ? override.scripts : {}), run: mergeRunScripts(base.scripts?.run, override.scripts?.run), }, + environment_variables: mergeNestedSettings( + base.environment_variables, + override.environment_variables, + ), + environment_variables_forward: mergeNestedSettings( + base.environment_variables_forward, + override.environment_variables_forward, + ), }; } +function mergeNestedSettings(base: unknown, override: unknown): unknown { + if (!isRecord(base) || !isRecord(override)) { + return override ?? base; + } + const merged: Record = { ...base }; + for (const [key, value] of Object.entries(override)) { + merged[key] = mergeNestedSettings(base[key], value); + } + return merged; +} + function mergeRunScripts(base: unknown, override: unknown): unknown { if (typeof override === "string") { return override; @@ -326,6 +365,9 @@ function reportUnsupported( if (scripts?.run_mode !== undefined) { unsupported(items, "scripts.run_mode", "Paseo has no project-wide run mode."); } + if (settings.runScriptMode !== undefined) { + unsupported(items, "runScriptMode", "Paseo has no project-wide run mode."); + } if (scripts?.auto_run_after_setup !== undefined) { unsupported( items, @@ -368,6 +410,23 @@ function reportUnsupported( if (settings.git !== undefined) { unsupported(items, "git", "Conductor Git settings are not imported."); } + for (const key of ["enterprise_data_privacy", "enterpriseDataPrivacy"] as const) { + if (settings[key] !== undefined) { + unsupported(items, key, "Conductor enterprise data privacy settings are not imported."); + } + } + for (const key of [ + "claude_code_executable_path", + "codex_executable_path", + "claude_provider", + "bedrock_region", + "vertex_project_id", + "ssh_key_path", + ] as const) { + if (settings[key] !== undefined) { + unsupported(items, key, "Conductor harness and provider settings are not imported."); + } + } } function collectEnvironmentVariableNames(settings: ConductorSettings): string[] {