fix(settings): preserve conductor migration context

This commit is contained in:
Mohamed Boudra
2026-07-17 17:00:30 +02:00
parent 9505e43925
commit b93d326fb2
2 changed files with 152 additions and 13 deletions

View File

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

View File

@@ -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<string, unknown> = { ...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[] {