From 26db053cf9f32a7b79fdfb13a08377c022b93722 Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Fri, 6 Nov 2026 12:15:41 -0800 Subject: [PATCH] fix(setup): pull simulations by their single-type alias in the wizard The wizard downloads the resources you pick one type at a time with pull.ts --type --id ..., and a pull by --id accepts exactly one type. It passed the internal type name, but "simulations" is also the --type group alias for all four simulation types, so picking simulations failed with "Single-resource pull requires exactly one resource type" and stopped setup before simulations, suites and evals were downloaded. Non-interactive setup (npm run setup -- ) does one full pull and was unaffected. Move the --type alias maps and resolveResourceTypes out of config.ts into a config-free resource-type-args.ts, and add typeArgFor(), which spells a type so it resolves to exactly that type (simulations -> simulations/tests). The wizard's per-type pull uses it. A test pins that every resource type round-trips to itself, so a future collision with a group alias fails CI. Co-Authored-By: Claude Opus 5.5 --- src/config.ts | 43 ++-------------------- src/resource-type-args.ts | 62 ++++++++++++++++++++++++++++++++ src/setup.ts | 5 ++- tests/resource-type-args.test.ts | 41 +++++++++++++++++++++ 4 files changed, 110 insertions(+), 41 deletions(-) create mode 100644 src/resource-type-args.ts create mode 100644 tests/resource-type-args.test.ts diff --git a/src/config.ts b/src/config.ts index bf475f6..7ee5460 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,7 +7,7 @@ import { ignorePatternsRead, matchesIgnore as matchesIgnorePatterns, } from "./resource-parse.ts"; -import { VALID_RESOURCE_TYPES } from "./types.ts"; +import { resolveResourceTypes, VALID_TYPE_ARGS } from "./resource-type-args.ts"; // ───────────────────────────────────────────────────────────────────────────── // CLI Argument Parsing @@ -19,23 +19,8 @@ export interface ApplyFilter { resourceIds?: string[]; // Pull only specific remote resource IDs } -// Group aliases: expand a shorthand into multiple resource types -const RESOURCE_GROUP_MAP: Record = { - simulations: [ - "personalities", - "scenarios", - "simulations", - "simulationSuites", - ], -}; - -// Path-based aliases: folder paths to resource types -const RESOURCE_PATH_MAP: Record = { - "simulations/personalities": "personalities", - "simulations/scenarios": "scenarios", - "simulations/tests": "simulations", - "simulations/suites": "simulationSuites", -}; +// The --type aliases (groups like "simulations", paths like +// "simulations/tests") live in resource-type-args.ts. const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/; @@ -67,28 +52,6 @@ function parseEnvironment(): Environment { return envArg; } -// Resolve a type argument into resource types (handles groups, paths, and direct types) -function resolveResourceTypes(arg: string): ResourceType[] | null { - // Check group aliases first (e.g., "simulations" → all 4 simulation types) - if (RESOURCE_GROUP_MAP[arg]) { - return RESOURCE_GROUP_MAP[arg]; - } - // Check path-based aliases (e.g., "simulations/personalities" → ["personalities"]) - if (RESOURCE_PATH_MAP[arg]) { - return [RESOURCE_PATH_MAP[arg]]; - } - // Check direct resource type - if (VALID_RESOURCE_TYPES.includes(arg as ResourceType)) { - return [arg as ResourceType]; - } - return null; -} - -const VALID_TYPE_ARGS = [ - ...VALID_RESOURCE_TYPES, - ...Object.keys(RESOURCE_GROUP_MAP), - ...Object.keys(RESOURCE_PATH_MAP), -]; function parseFlags(): { forceDelete: boolean; diff --git a/src/resource-type-args.ts b/src/resource-type-args.ts new file mode 100644 index 0000000..beca009 --- /dev/null +++ b/src/resource-type-args.ts @@ -0,0 +1,62 @@ +// How a `--type` argument maps to resource types. Config-free on purpose +// (like api-key.ts): config.ts parses argv and exits at import time, so the +// mapping lives here where setup.ts and tests can import it. + +import type { ResourceType } from "./types.ts"; +import { VALID_RESOURCE_TYPES } from "./types.ts"; + +// Group aliases: expand a shorthand into multiple resource types +const RESOURCE_GROUP_MAP: Record = { + simulations: [ + "personalities", + "scenarios", + "simulations", + "simulationSuites", + ], +}; + +// Path-based aliases: folder paths to resource types +const RESOURCE_PATH_MAP: Record = { + "simulations/personalities": "personalities", + "simulations/scenarios": "scenarios", + "simulations/tests": "simulations", + "simulations/suites": "simulationSuites", +}; + +// Resolve a type argument into resource types (handles groups, paths, and direct types) +export function resolveResourceTypes(arg: string): ResourceType[] | null { + // Check group aliases first (e.g., "simulations" → all 4 simulation types) + if (RESOURCE_GROUP_MAP[arg]) { + return RESOURCE_GROUP_MAP[arg]; + } + // Check path-based aliases (e.g., "simulations/personalities" → ["personalities"]) + if (RESOURCE_PATH_MAP[arg]) { + return [RESOURCE_PATH_MAP[arg]]; + } + // Check direct resource type + if (VALID_RESOURCE_TYPES.includes(arg as ResourceType)) { + return [arg as ResourceType]; + } + return null; +} + +export const VALID_TYPE_ARGS = [ + ...VALID_RESOURCE_TYPES, + ...Object.keys(RESOURCE_GROUP_MAP), + ...Object.keys(RESOURCE_PATH_MAP), +]; + +/** + * The `--type` argument that names exactly one resource type. Usually the + * type itself, but `simulations` is also the group alias for all four + * simulation types, so the single type is spelled by its folder path, + * `simulations/tests`. Use this whenever a command builds a `--type` for one + * type, e.g. a single-resource pull with `--id`. + */ +export function typeArgFor(type: string): string { + if (!(type in RESOURCE_GROUP_MAP)) return type; + const pathAlias = Object.keys(RESOURCE_PATH_MAP).find( + (path) => RESOURCE_PATH_MAP[path] === type, + ); + return pathAlias ?? type; +} diff --git a/src/setup.ts b/src/setup.ts index 02dd4b5..6d79c45 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -12,6 +12,7 @@ import { resolveOrgApiKey, } from "./api-key.ts"; import { updateEnvConnection } from "./bindings.ts"; +import { typeArgFor } from "./resource-type-args.ts"; import searchableCheckbox, { BACK_SENTINEL } from "./searchableCheckbox.js"; import { type DirectSetupOptions, @@ -348,7 +349,9 @@ function invokePull( "--force", "--skip-bindings", "--type", - typeKey, + // Not typeKey itself: "simulations" is also the group alias for all + // four simulation types, which a pull by --id refuses. + typeArgFor(typeKey), ...idArgs, ].join(" "); execSync(cmd, { cwd: BASE_DIR, stdio: "inherit", env }); diff --git a/tests/resource-type-args.test.ts b/tests/resource-type-args.test.ts new file mode 100644 index 0000000..df40eda --- /dev/null +++ b/tests/resource-type-args.test.ts @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + resolveResourceTypes, + typeArgFor, +} from "../src/resource-type-args.ts"; +import { VALID_RESOURCE_TYPES } from "../src/types.ts"; + +// The setup wizard downloads what you pick one type at a time, with +// `pull.ts --type --id …`, and a pull by --id accepts exactly one +// type. It passed the internal type name, and "simulations" is also the +// group alias for all four simulation types, so choosing simulations in the +// wizard failed with "Single-resource pull requires exactly one resource +// type" and stopped setup partway. + +test("typeArgFor names exactly one type for every resource type", () => { + for (const type of VALID_RESOURCE_TYPES) { + assert.deepEqual(resolveResourceTypes(typeArgFor(type)), [type], type); + } +}); + +test("typeArgFor spells simulations by its folder path", () => { + assert.equal(typeArgFor("simulations"), "simulations/tests"); +}); + +test("typeArgFor leaves other types unchanged", () => { + assert.equal(typeArgFor("assistants"), "assistants"); +}); + +test("--type simulations still selects every simulation type", () => { + assert.deepEqual(resolveResourceTypes("simulations"), [ + "personalities", + "scenarios", + "simulations", + "simulationSuites", + ]); +}); + +test("resolveResourceTypes rejects an unknown type", () => { + assert.equal(resolveResourceTypes("widgets"), null); +});