fix(settings): report remaining import gaps

This commit is contained in:
Mohamed Boudra
2026-07-17 16:42:07 +02:00
parent 2f0e3451de
commit 9505e43925
4 changed files with 163 additions and 12 deletions
+4
View File
@@ -3664,6 +3664,8 @@ export const WriteProjectConfigResponseMessageSchema = z.object({
export const GetProjectConfigImportResponseMessageSchema = z.object({
type: z.literal("project.config.get_import.response"),
// zod-aot 0.2.0 miscompiles boolean discriminators as string options
// (`"true"`/`"false"`), so keep this sequential until upstream fixes it.
payload: z.union([
ProjectConfigImportPreviewSchema.extend({
requestId: z.string(),
@@ -3680,6 +3682,8 @@ export const GetProjectConfigImportResponseMessageSchema = z.object({
export const ApplyProjectConfigImportResponseMessageSchema = z.object({
type: z.literal("project.config.apply_import.response"),
// zod-aot 0.2.0 miscompiles boolean discriminators as string options
// (`"true"`/`"false"`), so keep this sequential until upstream fixes it.
payload: z.union([
z.object({
requestId: z.string(),
@@ -1,4 +1,5 @@
import type { PaseoConfigRaw } from "@getpaseo/protocol/messages";
import { normalizeServiceEnvName } from "../../../workspace-service-env.js";
import type { ProjectConfigImportCandidate, ProjectConfigImportPreview } from "./service.js";
interface MergeProjectConfigImportInput {
@@ -42,12 +43,26 @@ export function mergeProjectConfigImport(
}
const patchScripts = input.candidate.patch.scripts ?? {};
const serviceScriptByEnvName = collectServiceScriptsByEnvName(base.scripts ?? {});
for (const [scriptId, script] of Object.entries(patchScripts)) {
const key = `scripts.${scriptId}`;
if (base.scripts && Object.hasOwn(base.scripts, scriptId)) {
setOutcome(items, key, `Paseo already has a "${scriptId}" script.`);
continue;
}
if (isServiceScript(script)) {
const envName = normalizeServiceEnvName(scriptId);
const existingScriptId = serviceScriptByEnvName.get(envName);
if (existingScriptId) {
setOutcome(
items,
key,
`Service environment name collides with "${existingScriptId}" (${envName}).`,
);
continue;
}
serviceScriptByEnvName.set(envName, scriptId);
}
merged.scripts = { ...merged.scripts, [scriptId]: script };
importedCount += 1;
}
@@ -64,6 +79,24 @@ export function mergeProjectConfigImport(
};
}
function collectServiceScriptsByEnvName(
scripts: NonNullable<PaseoConfigRaw["scripts"]>,
): Map<string, string> {
const result = new Map<string, string>();
for (const [scriptId, script] of Object.entries(scripts)) {
if (isServiceScript(script)) {
result.set(normalizeServiceEnvName(scriptId), scriptId);
}
}
return result;
}
function isServiceScript(script: unknown): boolean {
return Boolean(
script && typeof script === "object" && "type" in script && script.type === "service",
);
}
function hasLifecycle(value: unknown): boolean {
if (typeof value === "string") {
return value.trim().length > 0;
@@ -476,6 +476,91 @@ hide = true
);
});
test("reports nested environment variables, prompts, and Git settings", () => {
const repo = makeRepo();
writeSharedToml(
repo,
`
[environment_variables.local]
LOCAL_TOKEN = "local-secret"
[environment_variables.cloud]
CLOUD_TOKEN = "cloud-secret"
[prompts]
system = "custom prompt"
[git]
default_branch = "develop"
`,
);
expect(inspect(repo).items).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "environment_variables",
outcome: "unsupported",
detail: "Environment variable values are not imported. Found: CLOUD_TOKEN, LOCAL_TOKEN.",
}),
expect.objectContaining({ key: "prompts", outcome: "unsupported" }),
expect.objectContaining({ key: "git", outcome: "unsupported" }),
]),
);
});
test("reports default and icon fields on imported run scripts", () => {
const repo = makeRepo();
writeSharedToml(
repo,
`
[scripts.run.dev]
command = "npm run dev"
default = true
icon = "play"
`,
);
const preview = inspect(repo);
expect(preview.preview).toMatchObject({ scripts: { dev: { command: "npm run dev" } } });
expect(preview.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: "scripts.dev.default", outcome: "unsupported" }),
expect.objectContaining({ key: "scripts.dev.icon", outcome: "unsupported" }),
]),
);
});
test("does not import services with colliding normalized environment names", () => {
const repo = makeRepo();
writeSharedToml(
repo,
`
[scripts.run.app-server]
command = "npm run app -- --port $CONDUCTOR_PORT"
[scripts.run."app.server"]
command = "npm run other -- --port $CONDUCTOR_PORT"
`,
);
const preview = inspect(repo);
expect(preview.preview?.scripts).toMatchObject({
"app-server": { type: "service", port: "$PASEO_PORT" },
});
expect(preview.preview?.scripts).not.toHaveProperty("app.server");
expect(preview.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "scripts.app.server",
outcome: "collision",
detail: 'Service environment name collides with "app-server" (APP_SERVER).',
}),
]),
);
});
test("malformed TOML identifies the safe relative source path", () => {
const repo = makeRepo();
writeSharedToml(repo, "[scripts\nsetup = nope");
@@ -30,6 +30,8 @@ interface ConductorSettings {
file_include_globs?: unknown;
environment_variables?: unknown;
environment_variables_forward?: unknown;
prompts?: unknown;
git?: unknown;
spotlight_testing?: unknown;
[key: string]: unknown;
}
@@ -37,7 +39,9 @@ interface ConductorSettings {
interface ConductorRunScript {
command: string;
args?: string[];
default?: boolean;
hide?: boolean;
icon?: string;
options?: {
cwd?: string;
};
@@ -240,7 +244,9 @@ function normalizeRunScript(entry: Record<string, unknown>, command: string): Co
return {
command,
...(args ? { args } : {}),
...(typeof entry.default === "boolean" ? { default: entry.default } : {}),
...(typeof entry.hide === "boolean" ? { hide: entry.hide } : {}),
...(typeof entry.icon === "string" ? { icon: entry.icon } : {}),
...(options && typeof options.cwd === "string" ? { options: { cwd: options.cwd } } : {}),
...(availableIn ? { available_in: availableIn } : {}),
};
@@ -272,6 +278,13 @@ function mapRunScript(
return;
}
if (script.default !== undefined) {
unsupported(items, `scripts.${scriptId}.default`, "Default script selection is not imported.");
}
if (script.icon !== undefined) {
unsupported(items, `scripts.${scriptId}.icon`, "Script icons are not imported.");
}
let command = appendArgs(script.command, script.args ?? []);
if (script.options?.cwd) {
const cwdPrefix = safeCwdPrefix(script.options.cwd);
@@ -349,27 +362,43 @@ function reportUnsupported(
"Paseo spotlight is a separate workflow, not project config.",
);
}
if (settings.prompts !== undefined) {
unsupported(items, "prompts", "Custom agent prompts are not imported.");
}
if (settings.git !== undefined) {
unsupported(items, "git", "Conductor Git settings are not imported.");
}
}
function collectEnvironmentVariableNames(settings: ConductorSettings): string[] {
const names = new Set<string>();
for (const key of ["environment_variables", "environment_variables_forward"] as const) {
const value = settings[key];
if (isRecord(value)) {
for (const name of Object.keys(value).sort()) {
names.add(name);
}
} else if (Array.isArray(value)) {
for (const name of value) {
if (typeof name === "string") {
names.add(name);
}
}
}
collectEnvironmentVariableNamesFromValue(settings[key], names);
}
return Array.from(names).sort();
}
function collectEnvironmentVariableNamesFromValue(value: unknown, names: Set<string>): void {
if (Array.isArray(value)) {
for (const name of value) {
if (typeof name === "string") {
names.add(name);
}
}
return;
}
if (!isRecord(value)) {
return;
}
for (const [name, nestedValue] of Object.entries(value)) {
if (isRecord(nestedValue)) {
collectEnvironmentVariableNamesFromValue(nestedValue, names);
} else {
names.add(name);
}
}
}
function unsupported(items: ProjectConfigImportItem[], key: string, detail: string): void {
items.push({ key, label: key, outcome: "unsupported", detail });
}