mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(settings): preserve conductor migration context
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user