fix(settings): keep project imports current and complete

This commit is contained in:
Mohamed Boudra
2026-07-17 15:56:04 +02:00
parent 59d9a25a0d
commit 28cac5859a
6 changed files with 127 additions and 17 deletions
@@ -52,13 +52,15 @@ export function projectConfigImportPreviewQueryKey(
source: ProjectConfigImportSourceDescriptor | null,
): ProjectConfigImportPreviewQueryKey {
return [
"project-config-import",
serverId,
repoRoot,
...projectConfigImportPreviewQueryRoot(serverId, repoRoot),
source ? stableProjectConfigImportSourceKey(source) : "none",
] as const;
}
export function projectConfigImportPreviewQueryRoot(serverId: string, repoRoot: string) {
return ["project-config-import", serverId, repoRoot] as const;
}
export function stableProjectConfigImportSourceKey(
source: ProjectConfigImportSourceDescriptor,
): string {
@@ -9,6 +9,7 @@ import { fetchQueryOptions } from "@/data/query";
import {
projectConfigImportPreviewQueryInput,
projectConfigImportPreviewQueryKey,
projectConfigImportPreviewQueryRoot,
stableProjectConfigImportSourceKey,
} from "./preview-cache";
import {
@@ -127,9 +128,26 @@ describe("project config import retries", () => {
}),
).toBe("refresh");
});
it("refreshes the preview after an apply-time source parse failure", () => {
expect(
projectConfigImportApplyFailureRetryAction({
code: "invalid_source_config",
source: protocolSource,
relativePath: ".conductor/settings.toml",
}),
).toBe("refresh");
});
});
describe("project config import preview cache keys", () => {
it("groups source previews under a repository cache root", () => {
expect(projectConfigImportPreviewQueryKey("server", "/repo", fakeSource)).toEqual([
...projectConfigImportPreviewQueryRoot("server", "/repo"),
stableProjectConfigImportSourceKey(fakeSource),
]);
});
it("uses the full source descriptor instead of kind alone", () => {
const alpha = { kind: "variant-source", profile: "alpha" };
const beta = { profile: "beta", kind: "variant-source" };
@@ -8,7 +8,8 @@ export function projectConfigImportApplyFailureRetryAction(
return error.code === "stale_source_config" ||
error.code === "stale_project_config" ||
error.code === "nothing_to_import" ||
error.code === "source_config_not_found"
error.code === "source_config_not_found" ||
error.code === "invalid_source_config"
? "refresh"
: "apply";
}
@@ -28,6 +28,7 @@ import { Switch } from "@/components/ui/switch";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import type { ProjectConfigImportIntent } from "@/project-config-import/route";
import { ProjectConfigImportSection } from "@/project-config-import/project-config-import-section";
import { projectConfigImportPreviewQueryRoot } from "@/project-config-import/preview-cache";
import { SettingsTextAreaCard } from "@/components/settings-textarea";
import { SettingsGroup } from "@/screens/settings/settings-group";
import { SettingsSection } from "@/screens/settings/settings-section";
@@ -216,6 +217,7 @@ function ProjectSettingsBody({
importIntent,
onImportIntentConsumed,
}: ProjectSettingsBodyProps) {
const queryClient = useQueryClient();
const queryKey = useMemo(
() => ["project-config", selectedHost.serverId, selectedHost.repoRoot] as const,
[selectedHost.serverId, selectedHost.repoRoot],
@@ -245,8 +247,11 @@ function ProjectSettingsBody({
const readState = resolveProjectConfigReadState(data);
const handleReload = useCallback(() => {
void queryClient.invalidateQueries({
queryKey: projectConfigImportPreviewQueryRoot(selectedHost.serverId, selectedHost.repoRoot),
});
void readQuery.refetch();
}, [readQuery]);
}, [queryClient, readQuery, selectedHost.repoRoot, selectedHost.serverId]);
const hasMultipleHosts = hosts.length > 1;
@@ -517,6 +522,9 @@ function ProjectConfigForm({
});
setWriteError(null);
queryClient.invalidateQueries({ queryKey: ["projects"] });
void queryClient.invalidateQueries({
queryKey: projectConfigImportPreviewQueryRoot(serverId, repoRoot),
});
toast.show(t("settings.project.actions.saved"), { variant: "success" });
} else {
setWriteError(result.error);
@@ -43,6 +43,16 @@ function writeLocalToml(repo: string, contents: string): void {
writeFileSync(join(repo, ".conductor", "settings.local.toml"), contents);
}
function writeSharedJson(repo: string, value: unknown): void {
mkdirSync(join(repo, ".conductor"), { recursive: true });
writeFileSync(join(repo, ".conductor", "settings.json"), JSON.stringify(value));
}
function writeLocalJson(repo: string, value: unknown): void {
mkdirSync(join(repo, ".conductor"), { recursive: true });
writeFileSync(join(repo, ".conductor", "settings.local.json"), JSON.stringify(value));
}
function inspect(repo: string, paseoConfig = {}) {
return service.inspect({
repoRoot: repo,
@@ -151,6 +161,8 @@ setup = "local setup"
command = "local dev"
`,
);
writeSharedJson(repo, { scripts: { setup: "scoped legacy shared setup" } });
writeLocalJson(repo, { scripts: { setup: "scoped legacy local setup" } });
writeFileSync(
join(repo, "conductor.json"),
JSON.stringify({ scripts: { setup: "legacy setup", run: "legacy run" } }),
@@ -168,6 +180,54 @@ command = "local dev"
});
});
test("deep-merges local run-script fields with shared commands", () => {
const repo = makeRepo();
writeSharedToml(
repo,
`
[scripts.run.dev]
command = "npm run dev"
args = ["--shared"]
[scripts.run.dev.options]
cwd = "apps/web"
`,
);
writeLocalToml(
repo,
`
[scripts.run.dev]
args = ["--local"]
[scripts.run.dev.options]
cwd = "apps/local-web"
`,
);
expect(inspect(repo).preview).toMatchObject({
scripts: {
dev: { command: "cd -- 'apps/local-web' && npm run dev '--local'" },
},
});
});
test("imports legacy scoped JSON settings with local overrides", () => {
const repo = makeRepo();
writeSharedJson(repo, {
scripts: { setup: "shared setup", run: { dev: { command: "npm run dev" } } },
});
writeLocalJson(repo, { scripts: { run: { dev: { args: ["--local"] } } } });
const preview = inspect(repo);
expect(preview.inputs).toEqual([
{ role: "shared", relativePath: ".conductor/settings.json" },
{ role: "local", relativePath: ".conductor/settings.local.json" },
]);
expect(preview.preview).toMatchObject({
worktree: { setup: "shared setup" },
scripts: { dev: { command: "npm run dev '--local'" } },
});
});
test("imports legacy conductor.json when shared TOML is absent", () => {
const repo = makeRepo();
writeFileSync(
@@ -94,18 +94,24 @@ function inspectConductorImport(input: {
}
function discoverConductorSources(repoRoot: string): SourceFile[] {
const localPath = join(repoRoot, ".conductor", "settings.local.toml");
const sharedPath = join(repoRoot, ".conductor", "settings.toml");
const legacyPath = join(repoRoot, "conductor.json");
const localTomlPath = join(repoRoot, ".conductor", "settings.local.toml");
const localJsonPath = join(repoRoot, ".conductor", "settings.local.json");
const sharedTomlPath = join(repoRoot, ".conductor", "settings.toml");
const sharedJsonPath = join(repoRoot, ".conductor", "settings.json");
const rootLegacyPath = join(repoRoot, "conductor.json");
const files: SourceFile[] = [];
if (existsSync(sharedPath)) {
files.push(readSourceFile(repoRoot, sharedPath, "shared"));
} else if (existsSync(legacyPath)) {
files.push(readSourceFile(repoRoot, legacyPath, "legacy"));
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"));
}
if (existsSync(localPath)) {
files.push(readSourceFile(repoRoot, localPath, "local"));
if (existsSync(localTomlPath)) {
files.push(readSourceFile(repoRoot, localTomlPath, "local"));
} else if (existsSync(localJsonPath)) {
files.push(readSourceFile(repoRoot, localJsonPath, "local"));
}
return files;
}
@@ -127,8 +133,7 @@ function loadConductorSettings(
for (const file of sourceFiles) {
let parsed: unknown;
try {
parsed =
file.relativePath === "conductor.json" ? JSON.parse(file.bytes) : parseToml(file.bytes);
parsed = file.relativePath.endsWith(".json") ? JSON.parse(file.bytes) : parseToml(file.bytes);
} catch {
throw new InvalidProjectConfigImportSourceError(source, file.relativePath);
}
@@ -159,7 +164,23 @@ function mergeRunScripts(base: unknown, override: unknown): unknown {
if (!isRecord(base) || !isRecord(override)) {
return override ?? base;
}
return { ...base, ...override };
const merged: Record<string, unknown> = { ...base };
for (const [scriptId, overrideEntry] of Object.entries(override)) {
const baseEntry = base[scriptId];
merged[scriptId] = mergeRunScriptEntry(baseEntry, overrideEntry);
}
return merged;
}
function mergeRunScriptEntry(base: unknown, override: unknown): unknown {
if (!isRecord(base) || !isRecord(override)) {
return override ?? base;
}
const merged: Record<string, unknown> = { ...base, ...override };
if (isRecord(base.options) && isRecord(override.options)) {
merged.options = { ...base.options, ...override.options };
}
return merged;
}
function mapLifecycle(