From 9c534f6cf2d7121a44fa54dd24b30293f1ec73cc Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 08:50:38 -0500 Subject: [PATCH 01/47] Add nvmrc for consistent node version --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..11c309c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v24.13.1 From 5dd0b6e7ae71f2ba2d2895ce924fbae2707ed5c0 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 10:14:47 -0500 Subject: [PATCH 02/47] Improvements to handle local environment renaming --- src/commands/apply.ts | 204 ++++++++++++++++++++-------------- src/commands/env-rename.ts | 168 ++++++++++++++++++++++++++++ src/commands/init.ts | 4 + src/commands/plan.ts | 57 ++++++++++ src/compose/apply.ts | 218 +++++++++++++++++++++++++++++++------ src/compose/state.ts | 159 +++++++++++++++++++++++++++ src/index.ts | 4 + 7 files changed, 695 insertions(+), 119 deletions(-) create mode 100644 src/commands/env-rename.ts create mode 100644 src/commands/plan.ts create mode 100644 src/compose/state.ts diff --git a/src/commands/apply.ts b/src/commands/apply.ts index 4485317..f822da2 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -2,11 +2,19 @@ import path from "node:path"; import fs from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; -import { readFileSync, readlink } from "node:fs"; +import { readFileSync } from "node:fs"; import type { CommandModule } from "yargs"; import YAML from "yaml"; import { applyEnvironment } from "../compose/apply.js"; import { computeDesiredHashes, readLock, writeLock, type LockFile } from "../compose/lock.js"; +import { + COMPOSE_STATE_FILE, + ensureStateForAliases, + findEnvironmentByAlias, + markEnvironmentRemoteAlias, + writeComposeState, + readComposeState +} from "../compose/state.js"; type Args = { dir: string; @@ -23,11 +31,15 @@ type Args = { type ComposeConfig = { version: number; project: string; - environments: Record; + environments: Record; }; type OpKind = "create" | "update" | "noop" | "warn"; +type RunApplyArgs = Args & { + forcedPlan?: boolean; +}; + export const applyCommand: CommandModule<{}, Args> = { command: "apply", describe: "Apply a Compose IaC directory to the Management API", @@ -70,73 +82,99 @@ export const applyCommand: CommandModule<{}, Args> = { } }, handler: async (args) => { - const rootDir = path.resolve(process.cwd(), args.dir); - - if (args.requireClean && !args.plan) { - await ensureGitClean(rootDir); - } + await runApply(args); + } +}; - const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); +export async function runApply(rawArgs: RunApplyArgs): Promise { + const planOnly = rawArgs.forcedPlan ?? rawArgs.plan; + const rootDir = path.resolve(process.cwd(), rawArgs.dir); - const cfg = await readComposeConfig(composeYamlPath); - const envCfg = cfg.environments[args.env]; - if (!envCfg) { - throw new Error(`Environment "${args.env}" not found in ${composeYamlPath}`); - } + if (rawArgs.requireClean && !planOnly) { + await ensureGitClean(rootDir); + } + + const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); + + const cfg = await readComposeConfig(composeYamlPath); + const envCfg = cfg.environments[rawArgs.env]; + if (!envCfg) { + throw new Error(`Environment "${rawArgs.env}" not found in ${composeYamlPath}`); + } - const clientId = args.clientId ?? process.env.COMPOSE_MGMT_CLIENT_ID; - const clientSecret = args.clientSecret ?? process.env.COMPOSE_MGMT_CLIENT_SECRET; + const statePath = path.join(rootDir, COMPOSE_STATE_FILE); + const stateFromDisk = await readComposeState(rootDir); + const aliases = Object.keys(cfg.environments); + const stateResult = ensureStateForAliases(stateFromDisk, cfg.project, aliases); + const state = stateResult.state; - if (!clientId || !clientSecret) { - throw new Error( - "Missing OAuth credentials. Provide --clientId/--clientSecret " + + let envState = findEnvironmentByAlias(state, rawArgs.env); + if (!envState) { + throw new Error( + `Environment "${rawArgs.env}" has no entry in ${statePath}. ` + + `Run "compose env rename" for explicit alias changes or recreate ${statePath}.` + ); + } + + const clientId = rawArgs.clientId ?? process.env.COMPOSE_MGMT_CLIENT_ID; + const clientSecret = rawArgs.clientSecret ?? process.env.COMPOSE_MGMT_CLIENT_SECRET; + + if (!clientId || !clientSecret) { + throw new Error( + "Missing OAuth credentials. Provide --clientId/--clientSecret " + "or set COMPOSE_MGMT_CLIENT_ID and COMPOSE_MGMT_CLIENT_SECRET." - ); - } + ); + } - const envDir = path.resolve(rootDir, envCfg.dir); - const envDescription = path.resolve(rootDir, envCfg.description); - const scopeVal = args.scope ?? process.env.COMPOSE_MGMT_SCOPE; - const audienceVal = args.audience ?? process.env.COMPOSE_MGMT_AUDIENCE; - - const oauth: { - clientId: string; - clientSecret: string; - scope?: string; - audience?: string; - } = { - clientId, - clientSecret - }; - if (scopeVal !== undefined) oauth.scope = scopeVal; - if (audienceVal !== undefined) oauth.audience = audienceVal; - - const desired = await computeDesiredHashes(envDir); - const prior = await readLock(envDir); - - if (args.plan) { - console.log("State changes since last apply:"); - const priorRes = prior?.resources ?? {}; - printChange("collections", priorRes.collections?.hash, desired.collections.hash); - printChange("typeSchemas", priorRes.typeSchemas?.hash, desired.typeSchemas.hash); - printChange("ingestionFunctions", priorRes.ingestionFunctions?.hash, desired.ingestionFunctions.hash); - printChange("persistedDocs", priorRes.persistedDocs?.hash, desired.persistedDocs.hash); - printChange("webhooks", priorRes.webhooks?.hash, desired.webhooks.hash); - console.log(""); - } + const envDir = path.resolve(rootDir, envCfg.dir); + const envDescription = envCfg.description ?? `${rawArgs.env} Environment`; + const scopeVal = rawArgs.scope ?? process.env.COMPOSE_MGMT_SCOPE; + const audienceVal = rawArgs.audience ?? process.env.COMPOSE_MGMT_AUDIENCE; + + const oauth: { + clientId: string; + clientSecret: string; + scope?: string; + audience?: string; + } = { + clientId, + clientSecret + }; + if (scopeVal !== undefined) oauth.scope = scopeVal; + if (audienceVal !== undefined) oauth.audience = audienceVal; + + const desired = await computeDesiredHashes(envDir); + const prior = await readLock(envDir); + + if (planOnly) { + console.log("State changes since last apply:"); + const priorRes = prior?.resources ?? {}; + printChange("collections", priorRes.collections?.hash, desired.collections.hash); + printChange("typeSchemas", priorRes.typeSchemas?.hash, desired.typeSchemas.hash); + printChange("ingestionFunctions", priorRes.ingestionFunctions?.hash, desired.ingestionFunctions.hash); + printChange("persistedDocs", priorRes.persistedDocs?.hash, desired.persistedDocs.hash); + printChange("webhooks", priorRes.webhooks?.hash, desired.webhooks.hash); + console.log(""); + } - const ops = await applyEnvironment({ - project: cfg.project, - env: args.env, - envDir, - envDescription, - baseUrl: envCfg.managementBaseUrl, - oauth, - planOnly: args.plan - }); - - if (!args.plan) { - const cliVersion = getCliVersionSafe(); + const ops = await applyEnvironment({ + project: cfg.project, + env: rawArgs.env, + ...(envState.remoteAlias ? { remoteEnvAlias: envState.remoteAlias } : {}), + envDir, + envDescription, + baseUrl: envCfg.managementBaseUrl, + oauth, + planOnly + }); + + const byKind: Record = { create: 0, update: 0, noop: 0, warn: 0 }; + for (const op of ops) { + byKind[op.kind] += 1; + } + + if (!planOnly) { + const cliVersion = getCliVersionSafe(); const gitCommit = await getGitCommit(rootDir); const lastApplied: NonNullable = { @@ -149,37 +187,38 @@ export const applyCommand: CommandModule<{}, Args> = { const nextLock: LockFile = { version: 1, project: cfg.project, - environment: args.env, + environment: rawArgs.env, lastApplied, resources: desired }; await writeLock(envDir, nextLock); console.log(`\nWrote lock file: ${path.join(envDir, "compose.lock.json")}`); - } - // Print plan/apply results - const byKind: Record = { create: 0, update: 0, noop: 0, warn: 0 }; - - for (const op of ops) { - byKind[op.kind] += 1; + const environmentWarn = ops.some((op) => op.kind === "warn" && op.resource === "environment"); + if (!environmentWarn) { + markEnvironmentRemoteAlias(state, envState.id, rawArgs.env); } - - for (const op of ops) { - const label = op.kind.toUpperCase().padEnd(6); - const details = "details" in op && op.details ? ` — ${op.details}` : ""; - console.log(`${label} ${op.resource}:${op.alias}${details}`); + await writeComposeState(rootDir, state); + if (stateResult.changed) { + console.log(`Wrote state file: ${statePath}`); } + } - console.log(""); - console.log( - `${args.plan ? "Plan" : "Apply"} complete. ` + - `create=${byKind.create}, update=${byKind.update}, noop=${byKind.noop}, warn=${byKind.warn}` - ); - - process.exitCode = byKind.warn > 0 ? 1 : 0; + for (const op of ops) { + const label = op.kind.toUpperCase().padEnd(6); + const details = "details" in op && op.details ? ` — ${op.details}` : ""; + console.log(`${label} [env=${op.env}] ${op.resource}:${op.alias}${details}`); } -}; + + console.log(""); + console.log( + `${planOnly ? "Plan" : "Apply"} complete. ` + + `create=${byKind.create}, update=${byKind.update}, noop=${byKind.noop}, warn=${byKind.warn}` + ); + + process.exitCode = byKind.warn > 0 ? 1 : 0; +} async function readComposeConfig(filePath: string): Promise { const text = await fs.readFile(filePath, "utf8"); @@ -240,4 +279,3 @@ async function getGitCommit(repoDir: string): Promise { return undefined; } } - diff --git a/src/commands/env-rename.ts b/src/commands/env-rename.ts new file mode 100644 index 0000000..074bf2f --- /dev/null +++ b/src/commands/env-rename.ts @@ -0,0 +1,168 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { CommandModule } from "yargs"; +import YAML from "yaml"; +import { + COMPOSE_STATE_FILE, + ensureStateForAliases, + findEnvironmentByAlias, + renameEnvironmentAlias, + writeComposeState, + readComposeState +} from "../compose/state.js"; + +type Args = { + dir: string; + from: string; + to: string; + moveDir: boolean; +}; + +type ComposeConfig = { + version: number; + project: string; + environments: Record< + string, + { + dir: string; + description?: string; + managementBaseUrl: string; + ingestionBaseUrl?: string; + defaultCollection?: string; + } + >; +}; + +export const envRenameCommand: CommandModule<{}, Args> = { + command: "env rename ", + describe: "Rename an environment alias in local IaC config and state", + builder: { + dir: { + type: "string", + default: "./compose", + describe: "Root Compose IaC directory (contains umbraco-compose.yaml)" + }, + from: { + type: "string", + describe: "Current environment alias" + }, + to: { + type: "string", + describe: "New environment alias" + }, + moveDir: { + type: "boolean", + default: false, + describe: "Also rename the environment directory when it matches ./env/" + } + }, + handler: async (args) => { + if (args.from === args.to) { + throw new Error("--from and --to must be different values."); + } + + const rootDir = path.resolve(process.cwd(), args.dir); + const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); + const cfg = await readComposeConfig(composeYamlPath); + + if (!cfg.environments[args.from]) { + throw new Error(`Environment "${args.from}" not found in ${composeYamlPath}`); + } + if (cfg.environments[args.to]) { + throw new Error(`Environment "${args.to}" already exists in ${composeYamlPath}`); + } + + const statePath = path.join(rootDir, COMPOSE_STATE_FILE); + const stateOnDisk = await readComposeState(rootDir); + const ensuredState = ensureStateForAliases( + stateOnDisk, + cfg.project, + Object.keys(cfg.environments) + ).state; + + const entry = findEnvironmentByAlias(ensuredState, args.from); + if (!entry) { + throw new Error( + `Environment "${args.from}" has no entry in ${statePath}. ` + + `Recreate ${COMPOSE_STATE_FILE} or rename manually.` + ); + } + if (findEnvironmentByAlias(ensuredState, args.to)) { + throw new Error(`Environment "${args.to}" already exists in ${statePath}`); + } + + const renamed = renameEnvironmentAlias(ensuredState, args.from, args.to); + if (!renamed) { + throw new Error(`Failed to update ${statePath}.`); + } + + const envEntries = Object.entries(cfg.environments); + const renamedEntries: Array<[string, ComposeConfig["environments"][string]]> = []; + let originalDirForRename: string | null = null; + let nextDirForRename: string | null = null; + for (const [alias, value] of envEntries) { + if (alias !== args.from) { + renamedEntries.push([alias, value]); + continue; + } + + const next = { ...value }; + if (args.moveDir) { + const defaultDir = `./env/${args.from}`; + if (next.dir === defaultDir) { + originalDirForRename = defaultDir; + nextDirForRename = `./env/${args.to}`; + next.dir = `./env/${args.to}`; + } + } + renamedEntries.push([args.to, next]); + } + + cfg.environments = Object.fromEntries(renamedEntries); + + if (args.moveDir && originalDirForRename && nextDirForRename) { + const oldDir = path.resolve(rootDir, originalDirForRename); + const newDir = path.resolve(rootDir, nextDirForRename); + await maybeRenameDirectory(oldDir, newDir); + } + + await fs.writeFile(composeYamlPath, YAML.stringify(cfg), "utf8"); + await writeComposeState(rootDir, ensuredState); + + console.log(`Renamed environment alias "${args.from}" -> "${args.to}"`); + console.log(`Updated: ${composeYamlPath}`); + console.log(`Updated: ${statePath}`); + } +}; + +async function readComposeConfig(filePath: string): Promise { + const text = await fs.readFile(filePath, "utf8"); + const doc = YAML.parse(text) as ComposeConfig; + if (!doc?.project || !doc?.environments) { + throw new Error(`Invalid umbraco-compose.yaml: missing project/environments`); + } + return doc; +} + +async function maybeRenameDirectory(oldDir: string, newDir: string): Promise { + const oldExists = await exists(oldDir); + if (!oldExists) return; + + const newExists = await exists(newDir); + if (newExists) { + throw new Error( + `Cannot move directory "${oldDir}" to "${newDir}" because destination already exists.` + ); + } + + await fs.rename(oldDir, newDir); +} + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/src/commands/init.ts b/src/commands/init.ts index ffae3b9..4042b1e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -18,6 +18,7 @@ import { writeYamlFile, type WriteMode } from "../iac/write.js"; +import { createInitialState, COMPOSE_STATE_FILE } from "../compose/state.js"; type Args = { dir: string; @@ -108,6 +109,8 @@ export const initCommand: CommandModule<{}, Args> = { defaultCollection: args.collection }); const yamlRes = await writeYamlFile(yamlPath, yamlObj, mode); + const statePath = path.join(rootDir, COMPOSE_STATE_FILE); + const stateRes = await writeJsonFile(statePath, createInitialState(yamlObj.project, envs), mode); // 2) env folders const envRoot = path.join(rootDir, "env"); @@ -115,6 +118,7 @@ export const initCommand: CommandModule<{}, Args> = { const writes: Array<{ file: string; wrote: boolean; reason?: string }> = []; writes.push({ file: yamlPath, wrote: yamlRes.wrote, reason: (yamlRes as any).reason }); + writes.push({ file: statePath, wrote: stateRes.wrote, reason: (stateRes as any).reason }); for (const env of envs) { const dir = path.join(envRoot, env); diff --git a/src/commands/plan.ts b/src/commands/plan.ts new file mode 100644 index 0000000..b8ec343 --- /dev/null +++ b/src/commands/plan.ts @@ -0,0 +1,57 @@ +import type { CommandModule } from "yargs"; +import { runApply } from "./apply.js"; + +type Args = { + dir: string; + env: string; + clientId?: string; + clientSecret?: string; + scope?: string; + audience?: string; + requireClean: boolean; +}; + +export const planCommand: CommandModule<{}, Args> = { + command: "plan", + describe: "Show planned Compose IaC operations without applying changes", + builder: { + dir: { + type: "string", + default: "./compose", + describe: "Root Compose IaC directory (contains umbraco-compose.yaml)" + }, + env: { + type: "string", + default: "dev", + describe: "Environment name to plan (must exist in umbraco-compose.yaml)" + }, + clientId: { + type: "string", + describe: "OAuth client id (or set COMPOSE_MGMT_CLIENT_ID)" + }, + clientSecret: { + type: "string", + describe: "OAuth client secret (or set COMPOSE_MGMT_CLIENT_SECRET)" + }, + scope: { + type: "string", + describe: "Optional OAuth scope" + }, + audience: { + type: "string", + describe: "Optional OAuth audience" + }, + requireClean: { + type: "boolean", + default: false, + describe: "Fail if the git working tree has uncommitted changes" + } + }, + handler: async (args) => { + await runApply({ + ...args, + plan: true, + forcedPlan: true + }); + } +}; diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 9a50866..29ee6e9 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -5,14 +5,17 @@ import { ManagementClient } from "./management-client.js"; import { composeManagementOAuth } from "./auth/providers/oauth-compose.js"; type PlanOp = - | { kind: "create"; resource: string; alias: string; details?: string } - | { kind: "update"; resource: string; alias: string; details?: string } - | { kind: "noop"; resource: string; alias: string; details?: string } - | { kind: "warn"; resource: string; alias: string; details?: string }; + | { kind: "create"; env: string; resource: string; alias: string; details?: string } + | { kind: "update"; env: string; resource: string; alias: string; details?: string } + | { kind: "noop"; env: string; resource: string; alias: string; details?: string } + | { kind: "warn"; env: string; resource: string; alias: string; details?: string }; export type ApplyOptions = { project: string; env: string; + remoteEnvAlias?: string; + tolerateMissingResourceLists?: boolean; + apiEnvironmentAlias?: string; envDir: string; envDescription: string; baseUrl: string; @@ -27,6 +30,7 @@ export type ApplyOptions = { async function ensureEnvironmentExists(client: ManagementClient, opts: ApplyOptions): Promise { const out: PlanOp[] = []; + const opEnv = opts.env; const listPath = `/v1/projects/${encodeURIComponent(opts.project)}/environments`; const existingRes = await client.get(listPath); @@ -34,6 +38,7 @@ async function ensureEnvironmentExists(client: ManagementClient, opts: ApplyOpti if (existingRes.status >= 400) { out.push({ kind: "warn", + env: opEnv, resource: "environment", alias: opts.env, details: `Failed to list environments (${existingRes.status}).` @@ -53,34 +58,73 @@ async function ensureEnvironmentExists(client: ManagementClient, opts: ApplyOpti return typeof alias === "string" && alias === opts.env; }); + const hasAlias = (alias: string | undefined) => + typeof alias === "string" && + items.some((e: any) => { + const value = e?.environmentAlias ?? e?.alias; + return typeof value === "string" && value === alias; + }); + + const priorAlias = opts.remoteEnvAlias; + const hasPriorAlias = hasAlias(priorAlias); + + if (priorAlias && priorAlias !== opts.env && hasPriorAlias && !exists) { + out.push({ + kind: "update", + env: opEnv, + resource: "environment", + alias: opts.env, + details: `rename ${priorAlias} -> ${opts.env}` + }); + + if (opts.planOnly) return out; + + const renameStatus = await tryRenameEnvironmentAlias(client, opts.project, priorAlias, opts.env); + if (renameStatus < 200 || renameStatus >= 300) { + out.push({ + kind: "warn", + env: opEnv, + resource: "environment", + alias: opts.env, + details: `Rename failed (from ${priorAlias}, status ${renameStatus}).` + }); + } + return out; + } + if (exists) { - out.push({ kind: "noop", resource: "environment", alias: opts.env }); + out.push({ kind: "noop", env: opEnv, resource: "environment", alias: opts.env }); return out; } - out.push({ kind: "create", resource: "environment", alias: opts.env }); + out.push({ kind: "create", env: opEnv, resource: "environment", alias: opts.env }); if (opts.planOnly) return out; const createPath = `/v1/projects/${encodeURIComponent(opts.project)}/environments`; const body: any = { environmentAlias: opts.env }; - // Apply env description if it is part of ApplyOptions - if ("envDescription" in opts && (opts as any).envDescription) { - body.description = (opts as any).envDescription; + if (opts.envDescription) { + body.description = opts.envDescription; } const res = await client.post(createPath, body); - // Some APIs return 409 if it already exists (race-safe) if (res.status === 409) { - out.push({ kind: "noop", resource: "environment", alias: opts.env, details: "already exists" }); + out.push({ + kind: "noop", + env: opEnv, + resource: "environment", + alias: opts.env, + details: "already exists" + }); return out; } if (res.status >= 400) { out.push({ kind: "warn", + env: opEnv, resource: "environment", alias: opts.env, details: `Create failed (${res.status}).` @@ -108,26 +152,49 @@ export async function applyEnvironment(opts: ApplyOptions): Promise { // Short circuit if the environment check fails badly. if (ops.some(o => o.kind === "warn" && o.resource === "environment")) return ops; + const hasEnvCreate = ops.some((o) => o.resource === "environment" && o.kind === "create"); + const hasEnvRename = ops.some( + (o) => + o.resource === "environment" && + o.kind === "update" && + typeof o.details === "string" && + o.details.startsWith("rename ") + ); + + const shouldTolerateMissingLists = hasEnvCreate || (!opts.planOnly && hasEnvRename); + + // During plan for a rename, the remote resources still live under the old alias. + // Use that alias for reads so the plan reflects existing remote state. + const apiEnvironmentAlias = + opts.planOnly && hasEnvRename && opts.remoteEnvAlias ? opts.remoteEnvAlias : opts.env; + + const nestedOpts: ApplyOptions = { + ...opts, + tolerateMissingResourceLists: shouldTolerateMissingLists, + apiEnvironmentAlias + }; + // 1) collections - ops.push(...(await applyCollections(client, opts))); + ops.push(...(await applyCollections(client, nestedOpts))); // 2) type schemas - ops.push(...(await applyTypeSchemas(client, opts))); + ops.push(...(await applyTypeSchemas(client, nestedOpts))); // 3) ingestion functions - ops.push(...(await applyIngestionFunctions(client, opts))); + ops.push(...(await applyIngestionFunctions(client, nestedOpts))); // 4) persisted docs - ops.push(...(await applyPersistedDocs(client, opts))); + ops.push(...(await applyPersistedDocs(client, nestedOpts))); // 5) webhooks - ops.push(...(await applyWebhooks(client, opts))); + ops.push(...(await applyWebhooks(client, nestedOpts))); return ops; } function envBase(opts: ApplyOptions) { - return `/v1/projects/${encodeURIComponent(opts.project)}/environments/${encodeURIComponent(opts.env)}`; + const envAlias = opts.apiEnvironmentAlias ?? opts.env; + return `/v1/projects/${encodeURIComponent(opts.project)}/environments/${encodeURIComponent(envAlias)}`; } async function readJsonFile(filePath: string): Promise { @@ -150,6 +217,7 @@ type CollectionsFile = { collections: Array<{ alias: string; description?: strin async function applyCollections(client: ManagementClient, opts: ApplyOptions): Promise { const out: PlanOp[] = []; + const opEnv = opts.env; const filePath = path.join(opts.envDir, "collections.json"); if (!(await exists(filePath))) return out; @@ -159,8 +227,22 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P const listPath = `${envBase(opts)}/collections`; console.log("GET", `${envBase(opts)}/collections`); const existingRes = await client.get(listPath); - if (existingRes.status >= 400) { - out.push({ kind: "warn", resource: "collection", alias: "*", details: `Failed to list collections (${existingRes.status}).` }); + if (existingRes.status === 404 && opts.tolerateMissingResourceLists) { + out.push({ + kind: "noop", + env: opEnv, + resource: "collection", + alias: "*", + details: "List returned 404 during environment convergence; continuing with an empty remote set." + }); + } else if (existingRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias: "*", + details: `Failed to list collections (${existingRes.status}).` + }); return out; } @@ -182,30 +264,54 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P const alias = c.alias; const found = existingAliases.get(alias); if (!found) { - out.push({ kind: "create", resource: "collection", alias }); + out.push({ kind: "create", env: opEnv, resource: "collection", alias }); if (!opts.planOnly) { const createPath = `${envBase(opts)}/collections`; const body = { collectionAlias: alias, description: c.description ?? null }; const res = await client.post(createPath, body); if (res.status >= 400) { - out.push({ kind: "warn", resource: "collection", alias, details: `Create failed (${res.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias, + details: `Create failed (${res.status}).` + }); } } } else { // description update path varies; for MVP we no-op unless missing - out.push({ kind: "noop", resource: "collection", alias }); + out.push({ kind: "noop", env: opEnv, resource: "collection", alias }); } } return out; } +async function tryRenameEnvironmentAlias( + client: ManagementClient, + project: string, + fromAlias: string, + toAlias: string +): Promise { + const renamePath = + `/v1/projects/${encodeURIComponent(project)}` + + `/environments/${encodeURIComponent(fromAlias)}/commands/rename`; + + const response = await client.post(renamePath, { + newEnvironmentAlias: toAlias + }); + + return response.status; +} + /* -------------------- Type Schemas -------------------- */ type TypeSchemaFile = { alias: string; description?: string | null; schema: any }; async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): Promise { const out: PlanOp[] = []; + const opEnv = opts.env; const dir = path.join(opts.envDir, "type-schemas"); if (!(await exists(dir))) return out; @@ -219,7 +325,7 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P const existing = await client.get(getPath); if (existing.status === 404) { - out.push({ kind: "create", resource: "type-schema", alias }); + out.push({ kind: "create", env: opEnv, resource: "type-schema", alias }); if (!opts.planOnly) { const createPath = `${envBase(opts)}/type-schemas`; const body = { @@ -229,14 +335,26 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P }; const res = await client.post(createPath, body); if (res.status >= 400) { - out.push({ kind: "warn", resource: "type-schema", alias, details: `Create failed (${res.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Create failed (${res.status}).` + }); } } continue; } if (existing.status >= 400) { - out.push({ kind: "warn", resource: "type-schema", alias, details: `Failed to read (${existing.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Failed to read (${existing.status}).` + }); continue; } @@ -247,12 +365,13 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P const descChanged = (remoteDesc ?? null) !== (desired.description ?? null); if (!schemaChanged && !descChanged) { - out.push({ kind: "noop", resource: "type-schema", alias }); + out.push({ kind: "noop", env: opEnv, resource: "type-schema", alias }); continue; } out.push({ kind: "update", + env: opEnv, resource: "type-schema", alias, details: `${schemaChanged ? "schema " : ""}${descChanged ? "description" : ""}`.trim() @@ -265,7 +384,13 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P // per spec this endpoint takes the raw schema JSON const res = await client.put(updPath, desired.schema); if (res.status >= 400) { - out.push({ kind: "warn", resource: "type-schema", alias, details: `Update schema failed (${res.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Update schema failed (${res.status}).` + }); } } @@ -284,6 +409,7 @@ type IngestionRegistry = { async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOptions): Promise { const out: PlanOp[] = []; + const opEnv = opts.env; const registryPath = path.join(opts.envDir, "functions", "ingestion.json"); if (!(await exists(registryPath))) return out; @@ -313,7 +439,7 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti const found = existingAliases.get(alias); if (!found) { - out.push({ kind: "create", resource: "ingestion-function", alias }); + out.push({ kind: "create", env: opEnv, resource: "ingestion-function", alias }); if (!opts.planOnly) { const createPath = `${envBase(opts)}/functions/ingestion`; const body = { @@ -323,12 +449,18 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti }; const res = await client.post(createPath, body); if (res.status >= 400) { - out.push({ kind: "warn", resource: "ingestion-function", alias, details: `Create failed (${res.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Create failed (${res.status}).` + }); } } } else { // We don’t know remote script field shape without a GET-by-alias; MVP: always noop. - out.push({ kind: "noop", resource: "ingestion-function", alias }); + out.push({ kind: "noop", env: opEnv, resource: "ingestion-function", alias }); } } @@ -343,6 +475,7 @@ type PersistedRegistry = { async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): Promise { const out: PlanOp[] = []; + const opEnv = opts.env; const registryPath = path.join(opts.envDir, "graphql", "persisted.json"); if (!(await exists(registryPath))) return out; @@ -371,7 +504,7 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): const query = await fs.readFile(queryPath, "utf8"); if (!existingAliases.has(alias)) { - out.push({ kind: "create", resource: "persisted-doc", alias }); + out.push({ kind: "create", env: opEnv, resource: "persisted-doc", alias }); if (!opts.planOnly) { const createPath = `${envBase(opts)}/graphql/persisted-documents`; const body: any = { @@ -381,11 +514,17 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): }; const res = await client.post(createPath, body); if (res.status >= 400) { - out.push({ kind: "warn", resource: "persisted-doc", alias, details: `Create failed (${res.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Create failed (${res.status}).` + }); } } } else { - out.push({ kind: "noop", resource: "persisted-doc", alias }); + out.push({ kind: "noop", env: opEnv, resource: "persisted-doc", alias }); } } @@ -407,6 +546,7 @@ type WebhooksFile = { async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Promise { const out: PlanOp[] = []; + const opEnv = opts.env; const filePath = path.join(opts.envDir, "webhooks.json"); if (!(await exists(filePath))) return out; @@ -431,7 +571,7 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom for (const w of desired.webhooks) { const alias = w.alias; if (!existingAliases.has(alias)) { - out.push({ kind: "create", resource: "webhook", alias }); + out.push({ kind: "create", env: opEnv, resource: "webhook", alias }); if (!opts.planOnly) { const createPath = `${envBase(opts)}/webhooks`; const body: any = { @@ -444,11 +584,17 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom }; const res = await client.post(createPath, body); if (res.status >= 400) { - out.push({ kind: "warn", resource: "webhook", alias, details: `Create failed (${res.status}).` }); + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Create failed (${res.status}).` + }); } } } else { - out.push({ kind: "noop", resource: "webhook", alias }); + out.push({ kind: "noop", env: opEnv, resource: "webhook", alias }); } } diff --git a/src/compose/state.ts b/src/compose/state.ts new file mode 100644 index 0000000..8f9a202 --- /dev/null +++ b/src/compose/state.ts @@ -0,0 +1,159 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +export const COMPOSE_STATE_FILE = "compose.state.json"; + +export type ComposeEnvironmentState = { + id: string; + alias: string; + remoteAlias?: string; +}; + +export type ComposeStateFile = { + version: 1; + project: string; + environments: ComposeEnvironmentState[]; +}; + +export async function readComposeState(rootDir: string): Promise { + const filePath = path.join(rootDir, COMPOSE_STATE_FILE); + try { + const text = await fs.readFile(filePath, "utf8"); + const doc = JSON.parse(text) as ComposeStateFile; + return normalizeState(doc); + } catch (err) { + if (err && typeof err === "object" && "code" in err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } + } + throw err; + } +} + +export async function writeComposeState(rootDir: string, state: ComposeStateFile): Promise { + const filePath = path.join(rootDir, COMPOSE_STATE_FILE); + const text = JSON.stringify(state, null, 2) + "\n"; + await fs.writeFile(filePath, text, "utf8"); +} + +export function ensureStateForAliases( + state: ComposeStateFile | null, + project: string, + aliases: string[] +): { state: ComposeStateFile; changed: boolean } { + if (!state) { + return { + state: createInitialState(project, aliases), + changed: true + }; + } + + if (state.project !== project) { + throw new Error( + `State project mismatch in ${COMPOSE_STATE_FILE}: expected "${project}", found "${state.project}".` + ); + } + + const next = cloneState(state); + let changed = false; + + for (const alias of aliases) { + const existing = next.environments.find((e) => e.alias === alias); + if (!existing) { + next.environments.push({ + id: newEnvId(), + alias + }); + changed = true; + } + } + + return { state: next, changed }; +} + +export function findEnvironmentByAlias( + state: ComposeStateFile, + alias: string +): ComposeEnvironmentState | undefined { + return state.environments.find((e) => e.alias === alias); +} + +export function renameEnvironmentAlias( + state: ComposeStateFile, + fromAlias: string, + toAlias: string +): boolean { + const target = state.environments.find((e) => e.alias === fromAlias); + if (!target) return false; + target.alias = toAlias; + return true; +} + +export function markEnvironmentRemoteAlias( + state: ComposeStateFile, + envId: string, + alias: string +): boolean { + const target = state.environments.find((e) => e.id === envId); + if (!target) return false; + target.remoteAlias = alias; + return true; +} + +export function createInitialState(project: string, aliases: string[]): ComposeStateFile { + return { + version: 1, + project, + environments: aliases.map((alias) => ({ + id: newEnvId(), + alias + })) + }; +} + +function cloneState(state: ComposeStateFile): ComposeStateFile { + return { + version: 1, + project: state.project, + environments: state.environments.map((e) => ({ ...e })) + }; +} + +function normalizeState(state: ComposeStateFile): ComposeStateFile { + if (state.version !== 1 || typeof state.project !== "string" || !Array.isArray(state.environments)) { + throw new Error(`Invalid ${COMPOSE_STATE_FILE}`); + } + + const seenIds = new Set(); + const seenAliases = new Set(); + const normalizedEnvs: ComposeEnvironmentState[] = []; + + for (const e of state.environments) { + const id = typeof e.id === "string" && e.id.length > 0 ? e.id : newEnvId(); + const alias = typeof e.alias === "string" ? e.alias.trim() : ""; + if (!alias.length) continue; + if (seenAliases.has(alias)) continue; + if (seenIds.has(id)) continue; + + seenIds.add(id); + seenAliases.add(alias); + + const env: ComposeEnvironmentState = { id, alias }; + if (typeof e.remoteAlias === "string" && e.remoteAlias.length > 0) { + env.remoteAlias = e.remoteAlias; + } + normalizedEnvs.push(env); + } + + return { + version: 1, + project: state.project, + environments: normalizedEnvs + }; +} + +function newEnvId() { + return `env_${crypto.randomUUID()}`; +} diff --git a/src/index.ts b/src/index.ts index 738e6be..140ea39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,13 +6,17 @@ import { initCommand } from "./commands/init.js"; import { validateCommand } from "./commands/validate.js"; import { applyCommand } from "./commands/apply.js"; import { statusCommand } from "./commands/status.js"; +import { planCommand } from "./commands/plan.js"; +import { envRenameCommand } from "./commands/env-rename.js"; await yargs(hideBin(process.argv)) .scriptName("compose") .command(initCommand) .command(validateCommand) + .command(planCommand) .command(applyCommand) .command(statusCommand) + .command(envRenameCommand) .demandCommand(1, "Try `compose init --help` or `compose validate --help`.") .strict() .help() From d540437eee16ca855ec46c08f16071a504ae1c2b Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 10:15:43 -0500 Subject: [PATCH 03/47] Add a basic README with commands and their status. --- docs/commands.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/commands.md diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..08937b7 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,104 @@ +# Compose CLI Command Contract + +## Scope +This document defines the intended command behavior for the core workflow: + +- `compose init` +- `compose generate ` +- `compose validate` +- `compose pull` +- `compose status` +- `compose plan` +- `compose apply` +- `compose env rename` + +## Shared Behavior +- Compose project root: defaults to `./compose` and must contain `umbraco-compose.yaml`. +- Exit codes: + - `0`: success + - `1`: validation failure, planning/apply warnings, or runtime/config errors +- CI compatibility: + - all commands must be non-interactive + - machine-readable output should be available where relevant (`status --json`) +- Idempotency: + - rerunning the same command with unchanged inputs should produce no new side effects + +## Environment Identity Model +- Local state file: `compose.state.json` at project root. +- Purpose: provide stable local environment IDs so alias changes are handled as rename, not create. +- Model per environment: + - `id`: stable local identity + - `alias`: current local alias in config + - `remoteAlias`: last alias known to be active remotely + +Planner semantics: +- No `remoteAlias` + alias not present remotely: plan `create environment`. +- `remoteAlias` differs from local `alias`: plan `update environment` with rename intent. +- Local alias present remotely and no drift: plan `noop` for environment. + +## Command Contracts + +### `compose init` +- Status: implemented +- Responsibility: + - scaffold project directories and baseline files + - create `umbraco-compose.yaml` + - create `compose.state.json` with environment IDs +- Side effects: writes files under compose root. +- Idempotency: with `--merge`, only missing files are created. + +### `compose generate ` +- Status: planned +- Responsibility: + - scaffold entity files (schema/function/query/webhook/etc.) into the selected environment directory +- Side effects: writes/updates local files only. +- Idempotency: should support deterministic output and safe reruns. + +### `compose validate` +- Status: implemented +- Responsibility: + - local structural and schema checks without API calls +- Side effects: none. +- Idempotency: fully idempotent. + +### `compose pull` +- Status: planned +- Responsibility: + - fetch remote state and materialize files in local format + - update `compose.state.json` and lock data to match pulled state +- Side effects: writes local files. +- Idempotency: repeated pulls with unchanged remote state should produce no diffs. + +### `compose status` +- Status: implemented +- Responsibility: + - compare local desired state vs `compose.lock.json` + - no API calls +- Side effects: none. +- Idempotency: fully idempotent. + +### `compose plan` +- Status: implemented +- Responsibility: + - compute and print API operation plan without mutating remote resources + - includes environment create/rename/noop based on identity mapping +- Side effects: none (reads local files + remote APIs). +- Idempotency: identical inputs should produce equivalent plan output. + +### `compose apply` +- Status: implemented +- Responsibility: + - execute planned operations against management API + - update `compose.lock.json` + - update `compose.state.json.remoteAlias` when environment operation succeeds +- Side effects: remote writes + local lock/state updates. +- Idempotency: rerun after successful apply should converge to mostly noops. + +### `compose env rename` +- Status: implemented +- Responsibility: + - explicit local alias rename in `umbraco-compose.yaml` + - update `compose.state.json` alias while preserving prior `remoteAlias` + - optional local directory move (`--moveDir`) +- Side effects: local config/state writes, optional folder rename. +- Idempotency: one-time alias transition; rerunning same from/to pair should fail as invalid state. From 8788de5ca2e3843e1c74a3efdda18cd5b972fad7 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 10:19:01 -0500 Subject: [PATCH 04/47] Update docs to reflect recent improvements to environment management --- docs/commands.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/commands.md b/docs/commands.md index 08937b7..e43bab0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -20,6 +20,8 @@ This document defines the intended command behavior for the core workflow: - CI compatibility: - all commands must be non-interactive - machine-readable output should be available where relevant (`status --json`) +- Plan/apply operation output: + - each operation line includes explicit environment context in the format `[env=]` - Idempotency: - rerunning the same command with unchanged inputs should produce no new side effects @@ -36,6 +38,10 @@ Planner semantics: - `remoteAlias` differs from local `alias`: plan `update environment` with rename intent. - Local alias present remotely and no drift: plan `noop` for environment. +Alias routing semantics: +- `plan` on pending rename reads nested resources using `remoteAlias` (old alias), so plan diff reflects current remote state. +- `apply` on pending rename performs environment rename first, then uses local alias (`alias`) for nested writes. + ## Command Contracts ### `compose init` @@ -82,6 +88,8 @@ Planner semantics: - Responsibility: - compute and print API operation plan without mutating remote resources - includes environment create/rename/noop based on identity mapping +- Notes: + - on pending rename, nested reads target the old remote alias (`remoteAlias`) - Side effects: none (reads local files + remote APIs). - Idempotency: identical inputs should produce equivalent plan output. @@ -89,8 +97,11 @@ Planner semantics: - Status: implemented - Responsibility: - execute planned operations against management API + - perform environment rename with `POST /v1/projects/{projectAlias}/environments/{environmentAlias}/commands/rename` and body `{ "newEnvironmentAlias": "" }` when rename intent exists - update `compose.lock.json` - update `compose.state.json.remoteAlias` when environment operation succeeds +- Notes: + - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. From 3ee7cb59e5eedb4acfd6c51c3543c143220963a0 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 13:31:20 -0500 Subject: [PATCH 05/47] Add initial test suite for critical path --- test/commands.apply.auth-failures.test.ts | 108 ++++++++++ .../commands.apply.create-env-failure.test.ts | 102 ++++++++++ test/commands.apply.output-snapshots.test.ts | 163 +++++++++++++++ ...mands.apply.partial-failure-writes.test.ts | 107 ++++++++++ test/commands.apply.test.ts | 183 +++++++++++++++++ test/commands.env-rename.test.ts | 192 ++++++++++++++++++ test/commands.plan-exit.test.ts | 87 ++++++++ test/commands.status-validate-exit.test.ts | 102 ++++++++++ ...se.apply.environment-alias-routing.test.ts | 174 ++++++++++++++++ test/compose.apply.failures.test.ts | 129 ++++++++++++ test/compose.management-client.test.ts | 80 ++++++++ test/compose.oauth-base.test.ts | 109 ++++++++++ test/compose.state.test.ts | 70 +++++++ 13 files changed, 1606 insertions(+) create mode 100644 test/commands.apply.auth-failures.test.ts create mode 100644 test/commands.apply.create-env-failure.test.ts create mode 100644 test/commands.apply.output-snapshots.test.ts create mode 100644 test/commands.apply.partial-failure-writes.test.ts create mode 100644 test/commands.apply.test.ts create mode 100644 test/commands.env-rename.test.ts create mode 100644 test/commands.plan-exit.test.ts create mode 100644 test/commands.status-validate-exit.test.ts create mode 100644 test/compose.apply.environment-alias-routing.test.ts create mode 100644 test/compose.apply.failures.test.ts create mode 100644 test/compose.management-client.test.ts create mode 100644 test/compose.oauth-base.test.ts create mode 100644 test/compose.state.test.ts diff --git a/test/commands.apply.auth-failures.test.ts b/test/commands.apply.auth-failures.test.ts new file mode 100644 index 0000000..2316df2 --- /dev/null +++ b/test/commands.apply.auth-failures.test.ts @@ -0,0 +1,108 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createComposeRoot(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-auth-fail-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return root; +} + +test("runApply fails clearly when token endpoint returns non-200", async () => { + const root = await createComposeRoot(); + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return new Response("bad client", { status: 401, headers: { "content-type": "text/plain" } }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await assert.rejects( + () => + runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }), + /OAuth token request failed \(401\): bad client/ + ); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("runApply fails clearly when token endpoint response lacks access_token", async () => { + const root = await createComposeRoot(); + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { token_type: "Bearer", expires_in: 3600 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await assert.rejects( + () => + runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }), + /OAuth token response missing access_token/ + ); + } finally { + globalThis.fetch = oldFetch; + } +}); diff --git a/test/commands.apply.create-env-failure.test.ts b/test/commands.apply.create-env-failure.test.ts new file mode 100644 index 0000000..03be4c3 --- /dev/null +++ b/test/commands.apply.create-env-failure.test.ts @@ -0,0 +1,102 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createComposeRootForMissingEnv(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-create-env-fail-")); + const envDir = path.join(root, "env", "new-env"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + "new-env": { + dir: "./env/new-env", + description: "New Env", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return { root, envDir }; +} + +test("apply create-environment failure sets exitCode=1 and skips lock/state writes", async () => { + const { root, envDir } = await createComposeRootForMissingEnv(); + const oldFetch = globalThis.fetch; + const oldExitCode = process.exitCode; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(500, { error: "server-error" }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "new-env", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assert.equal(process.exitCode, 1); + process.exitCode = oldExitCode; + + const lockPath = path.join(envDir, "compose.lock.json"); + const lockExists = await exists(lockPath); + assert.equal(lockExists, false); + + const state = await readComposeState(root); + assert.equal(state, null); +}); + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/test/commands.apply.output-snapshots.test.ts b/test/commands.apply.output-snapshots.test.ts new file mode 100644 index 0000000..dd96ad0 --- /dev/null +++ b/test/commands.apply.output-snapshots.test.ts @@ -0,0 +1,163 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createComposeRoot(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-output-snapshot-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + return root; +} + +function installMockFetch(): () => void { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + return () => { + globalThis.fetch = oldFetch; + }; +} + +function captureLogs(): { output: string[]; restore: () => void } { + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + return { + output, + restore: () => { + console.log = originalLog; + } + }; +} + +function operationLines(output: string[]): string[] { + return output.filter((line) => /^(CREATE|UPDATE|NOOP|WARN)\s+\[env=/.test(line)); +} + +function finalSummaryLine(output: string[]): string { + const line = output.find((l) => /(Plan|Apply) complete\./.test(l)); + assert.ok(line); + return line; +} + +test("plan output operation lines and summary match expected snapshot", async () => { + const root = await createComposeRoot(); + const restoreFetch = installMockFetch(); + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + + assert.deepEqual(operationLines(output), [ + "NOOP [env=dev] environment:dev", + "CREATE [env=dev] collection:content" + ]); + assert.equal( + finalSummaryLine(output), + "Plan complete. create=1, update=0, noop=1, warn=0" + ); +}); + +test("apply output operation lines and summary match expected snapshot", async () => { + const root = await createComposeRoot(); + const restoreFetch = installMockFetch(); + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + + assert.deepEqual(operationLines(output), [ + "NOOP [env=dev] environment:dev", + "CREATE [env=dev] collection:content" + ]); + assert.equal( + finalSummaryLine(output), + "Apply complete. create=1, update=0, noop=1, warn=0" + ); +}); diff --git a/test/commands.apply.partial-failure-writes.test.ts b/test/commands.apply.partial-failure-writes.test.ts new file mode 100644 index 0000000..b28d9dc --- /dev/null +++ b/test/commands.apply.partial-failure-writes.test.ts @@ -0,0 +1,107 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createComposeRoot(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-partial-warn-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return { root, envDir }; +} + +test("apply skips lock/state writes when nested resource create emits warning", async () => { + const { root, envDir } = await createComposeRoot(); + const oldFetch = globalThis.fetch; + const oldExitCode = process.exitCode; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(500, { error: "collection-create-fail" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assert.equal(process.exitCode, 1); + process.exitCode = oldExitCode; + + const lockExists = await exists(path.join(envDir, "compose.lock.json")); + assert.equal(lockExists, false); + + const state = await readComposeState(root); + assert.equal(state, null); +}); + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/test/commands.apply.test.ts b/test/commands.apply.test.ts new file mode 100644 index 0000000..9a53f8f --- /dev/null +++ b/test/commands.apply.test.ts @@ -0,0 +1,183 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; + +type FetchCall = { + method: string; + url: string; + bodyText?: string; +}; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +async function createComposeRoot(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-cmd-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Development", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify({ webhooks: [] }, null, 2), + "utf8" + ); + + return { root, envDir }; +} + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +function installMockFetch(calls: FetchCall[]): () => void { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") { + bodyText = init.body; + } else if (init?.body instanceof URLSearchParams) { + bodyText = init.body.toString(); + } + calls.push({ method, url, bodyText }); + + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + return () => { + globalThis.fetch = oldFetch; + }; +} + +test("runApply prints operation lines with explicit environment context", async () => { + const { root } = await createComposeRoot(); + const calls: FetchCall[] = []; + const restoreFetch = installMockFetch(calls); + + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + + const originalExitCode = process.exitCode; + process.exitCode = 0; + + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } finally { + restoreFetch(); + console.log = originalLog; + process.exitCode = originalExitCode; + } + + assert.equal( + output.some((line) => line.includes("[env=dev] environment:dev")), + true + ); + assert.equal( + output.some((line) => line.includes("[env=dev] collection:content")), + true + ); + assert.equal(calls.length > 0, true); +}); + +test("runApply writes compose.lock.json and updates compose.state.json after successful apply", async () => { + const { root, envDir } = await createComposeRoot(); + const calls: FetchCall[] = []; + const restoreFetch = installMockFetch(calls); + + const originalExitCode = process.exitCode; + process.exitCode = 0; + + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } finally { + restoreFetch(); + process.exitCode = originalExitCode; + } + + const lockPath = path.join(envDir, "compose.lock.json"); + const lockText = await fs.readFile(lockPath, "utf8"); + const lock = JSON.parse(lockText) as { + version: number; + project: string; + environment: string; + lastApplied?: { at?: string }; + resources: Record; + }; + + assert.equal(lock.version, 1); + assert.equal(lock.project, "test-project"); + assert.equal(lock.environment, "dev"); + assert.equal(typeof lock.lastApplied?.at, "string"); + assert.equal(typeof lock.resources.collections?.hash, "string"); + assert.equal(typeof lock.resources.webhooks?.hash, "string"); + + const state = await readComposeState(root); + assert.ok(state); + const envState = state.environments.find((e) => e.alias === "dev"); + assert.ok(envState); + assert.equal(envState.remoteAlias, "dev"); +}); diff --git a/test/commands.env-rename.test.ts b/test/commands.env-rename.test.ts new file mode 100644 index 0000000..12dd4f2 --- /dev/null +++ b/test/commands.env-rename.test.ts @@ -0,0 +1,192 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { envRenameCommand } from "../src/commands/env-rename.js"; +import { + createInitialState, + markEnvironmentRemoteAlias, + readComposeState, + writeComposeState +} from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +async function writeComposeConfig(rootDir: string, cfg: ComposeConfig): Promise { + await fs.writeFile(path.join(rootDir, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); +} + +async function readComposeConfig(rootDir: string): Promise { + const text = await fs.readFile(path.join(rootDir, "umbraco-compose.yaml"), "utf8"); + return YAML.parse(text) as ComposeConfig; +} + +function baseConfig(): ComposeConfig { + return { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + }, + prod: { + dir: "./env/prod", + description: "Prod", + managementBaseUrl: "https://management.example" + } + } + }; +} + +test("env rename updates umbraco-compose.yaml and compose.state.json while preserving env identity", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-")); + await writeComposeConfig(root, baseConfig()); + + const state = createInitialState("test-project", ["dev", "prod"]); + const dev = state.environments.find((e) => e.alias === "dev"); + assert.ok(dev); + markEnvironmentRemoteAlias(state, dev.id, "dev"); + await writeComposeState(root, state); + + await envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: false + }); + + const cfgAfter = await readComposeConfig(root); + assert.ok(cfgAfter.environments.development); + assert.equal(cfgAfter.environments.dev, undefined); + assert.equal(cfgAfter.environments.development?.dir, "./env/dev"); + + const stateAfter = await readComposeState(root); + assert.ok(stateAfter); + const renamed = stateAfter.environments.find((e) => e.alias === "development"); + assert.ok(renamed); + assert.equal(renamed.id, dev.id); + assert.equal(renamed.remoteAlias, "dev"); +}); + +test("env rename with --moveDir renames default env directory path and folder", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-move-")); + await writeComposeConfig(root, baseConfig()); + await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); + + await fs.mkdir(path.join(root, "env", "dev"), { recursive: true }); + await fs.writeFile(path.join(root, "env", "dev", "collections.json"), "{\"collections\":[]}\n", "utf8"); + + await envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: true + }); + + const cfgAfter = await readComposeConfig(root); + assert.equal(cfgAfter.environments.development?.dir, "./env/development"); + + const oldExists = await exists(path.join(root, "env", "dev")); + const newExists = await exists(path.join(root, "env", "development")); + assert.equal(oldExists, false); + assert.equal(newExists, true); +}); + +test("env rename fails when target alias already exists and leaves files unchanged", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-fail-")); + const cfg = baseConfig(); + await writeComposeConfig(root, cfg); + const state = createInitialState("test-project", ["dev", "prod"]); + await writeComposeState(root, state); + + const beforeYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const beforeState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + + await assert.rejects(() => + envRenameCommand.handler({ + dir: root, + from: "dev", + to: "prod", + moveDir: false + }) + ); + + const afterYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const afterState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + assert.equal(afterYaml, beforeYaml); + assert.equal(afterState, beforeState); +}); + +test("env rename --moveDir fails on destination collision and leaves config/state unchanged", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-collision-")); + await writeComposeConfig(root, baseConfig()); + await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); + + await fs.mkdir(path.join(root, "env", "dev"), { recursive: true }); + await fs.mkdir(path.join(root, "env", "development"), { recursive: true }); + + const beforeYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const beforeState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + + await assert.rejects( + () => + envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: true + }), + /destination already exists/ + ); + + const afterYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const afterState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + assert.equal(afterYaml, beforeYaml); + assert.equal(afterState, beforeState); +}); + +test("env rename --moveDir succeeds when source directory is missing", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-missing-src-")); + await writeComposeConfig(root, baseConfig()); + await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); + + const oldExistsBefore = await exists(path.join(root, "env", "dev")); + assert.equal(oldExistsBefore, false); + + await envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: true + }); + + const cfgAfter = await readComposeConfig(root); + assert.equal(cfgAfter.environments.development?.dir, "./env/development"); + assert.equal(cfgAfter.environments.dev, undefined); + + const stateAfter = await readComposeState(root); + assert.ok(stateAfter); + assert.ok(stateAfter.environments.find((e) => e.alias === "development")); + + const oldExists = await exists(path.join(root, "env", "dev")); + const newExists = await exists(path.join(root, "env", "development")); + assert.equal(oldExists, false); + assert.equal(newExists, false); +}); + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/test/commands.plan-exit.test.ts b/test/commands.plan-exit.test.ts new file mode 100644 index 0000000..672ec65 --- /dev/null +++ b/test/commands.plan-exit.test.ts @@ -0,0 +1,87 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createComposeRoot(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-plan-exit-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return root; +} + +test("plan mode sets process.exitCode=1 when warnings are produced", async () => { + const root = await createComposeRoot(); + const oldFetch = globalThis.fetch; + const oldExitCode = process.exitCode; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(500, { error: "unavailable" }); + } + if (method === "GET") return jsonResponse(200, { edges: [] }); + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + assert.equal(process.exitCode, 1); + } finally { + globalThis.fetch = oldFetch; + process.exitCode = oldExitCode; + } +}); diff --git a/test/commands.status-validate-exit.test.ts b/test/commands.status-validate-exit.test.ts new file mode 100644 index 0000000..2e989cd --- /dev/null +++ b/test/commands.status-validate-exit.test.ts @@ -0,0 +1,102 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { statusCommand } from "../src/commands/status.js"; +import { validateCommand } from "../src/commands/validate.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +async function createStatusFixture(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-status-exit-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return root; +} + +async function createValidateFixtureWithWarnings(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-validate-exit-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "Not-Kebab" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return root; +} + +test("status --failOnChanges sets process.exitCode=1 when lock is missing", async () => { + const root = await createStatusFixture(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + + try { + await statusCommand.handler({ + dir: root, + env: "dev", + json: true, + failOnChanges: true + }); + assert.equal(process.exitCode, 1); + } finally { + process.exitCode = oldExitCode; + } +}); + +test("validate --strict sets process.exitCode=1 when warnings exist", async () => { + const root = await createValidateFixtureWithWarnings(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + + try { + await validateCommand.handler({ + dir: root, + strict: true + }); + assert.equal(process.exitCode, 1); + } finally { + process.exitCode = oldExitCode; + } +}); diff --git a/test/compose.apply.environment-alias-routing.test.ts b/test/compose.apply.environment-alias-routing.test.ts new file mode 100644 index 0000000..3146cf4 --- /dev/null +++ b/test/compose.apply.environment-alias-routing.test.ts @@ -0,0 +1,174 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; + +type FetchCall = { + method: string; + url: string; + bodyText?: string; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function makeEnvDirWithCollections(): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-env-")); + await fs.writeFile( + path.join(dir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + return dir; +} + +test("plan with pending rename reads nested resources from remote alias path", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls: FetchCall[] = []; + const baseUrl = "https://management.example"; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") { + bodyText = init.body; + } else if (init?.body instanceof URLSearchParams) { + bodyText = init.body.toString(); + } + calls.push({ method, url, bodyText }); + + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { + edges: [{ node: { environmentAlias: "iac-dev" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/iac-dev/collections") { + return jsonResponse(200, { edges: [] }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "iac-development", + remoteEnvAlias: "iac-dev", + envDir, + envDescription: "Development", + baseUrl, + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: true + }); + + const hasRenameOp = ops.some( + (o) => o.kind === "update" && o.resource === "environment" && typeof o.details === "string" && o.details.includes("iac-dev -> iac-development") + ); + assert.equal(hasRenameOp, true); + + assert.equal( + calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/collections")), + true + ); + assert.equal( + calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-development/collections")), + false + ); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("apply with pending rename calls rename endpoint and then uses new alias path for nested resources", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls: FetchCall[] = []; + const baseUrl = "https://management.example"; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") { + bodyText = init.body; + } else if (init?.body instanceof URLSearchParams) { + bodyText = init.body.toString(); + } + calls.push({ method, url, bodyText }); + + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { + edges: [{ node: { environmentAlias: "iac-dev" } }] + }); + } + if ( + method === "POST" && + u.pathname === "/v1/projects/proj/environments/iac-dev/commands/rename" + ) { + return jsonResponse(200, { environmentAlias: "iac-development" }); + } + if (u.pathname === "/v1/projects/proj/environments/iac-development/collections") { + return jsonResponse(200, { edges: [] }); + } + if ( + method === "POST" && + u.pathname === "/v1/projects/proj/environments/iac-development/collections" + ) { + return jsonResponse(201, { collectionAlias: "content" }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "iac-development", + remoteEnvAlias: "iac-dev", + envDir, + envDescription: "Development", + baseUrl, + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + const renameCall = calls.find((c) => + c.url.includes("/v1/projects/proj/environments/iac-dev/commands/rename") + ); + assert.ok(renameCall); + assert.equal(renameCall.method, "POST"); + assert.equal(renameCall.bodyText, JSON.stringify({ newEnvironmentAlias: "iac-development" })); + + assert.equal( + calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-development/collections")), + true + ); + assert.equal( + calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/collections")), + false + ); + + assert.equal( + ops.some((o) => o.kind === "update" && o.resource === "environment"), + true + ); + } finally { + globalThis.fetch = oldFetch; + } +}); diff --git a/test/compose.apply.failures.test.ts b/test/compose.apply.failures.test.ts new file mode 100644 index 0000000..d5ddfc3 --- /dev/null +++ b/test/compose.apply.failures.test.ts @@ -0,0 +1,129 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; + +type FetchCall = { + method: string; + url: string; + bodyText?: string; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function makeEnvDirWithCollections(): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-fail-")); + await fs.writeFile( + path.join(dir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + return dir; +} + +test("environment list failure returns environment warning and short-circuits nested operations", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + calls.push({ method, url }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(500, { error: "boom" }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: true + }); + + assert.equal( + ops.some((o) => o.kind === "warn" && o.resource === "environment"), + true + ); + assert.equal( + calls.some((c) => c.url.includes("/environments/dev/collections")), + false + ); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("rename failure returns environment warning with status and short-circuits nested operations", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); + } + if ( + method === "POST" && + u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" + ) { + return jsonResponse(409, { error: "conflict" }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + remoteEnvAlias: "old-dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + assert.equal( + ops.some((o) => o.kind === "warn" && o.resource === "environment" && (o.details ?? "").includes("status 409")), + true + ); + assert.equal( + calls.some((c) => c.url.includes("/environments/dev/collections")), + false + ); + } finally { + globalThis.fetch = oldFetch; + } +}); diff --git a/test/compose.management-client.test.ts b/test/compose.management-client.test.ts new file mode 100644 index 0000000..1249e17 --- /dev/null +++ b/test/compose.management-client.test.ts @@ -0,0 +1,80 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { ManagementClient } from "../src/compose/management-client.js"; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +test("ManagementClient retries once on 401 and invalidates auth cache", async () => { + const oldFetch = globalThis.fetch; + let fetchCount = 0; + let invalidateCount = 0; + let getHeadersCount = 0; + + globalThis.fetch = (async () => { + fetchCount += 1; + if (fetchCount === 1) { + return jsonResponse(401, { error: "expired-token" }); + } + return jsonResponse(200, { ok: true }); + }) as typeof fetch; + + const client = new ManagementClient({ + baseUrl: "https://management.example", + auth: { + getHeaders: async () => { + getHeadersCount += 1; + return { Authorization: `Bearer token-${getHeadersCount}` }; + }, + invalidate: () => { + invalidateCount += 1; + } + } + }); + + try { + const res = await client.get<{ ok: boolean }>("/v1/projects/p/environments"); + assert.equal(res.status, 200); + assert.equal(res.data?.ok, true); + assert.equal(fetchCount, 2); + assert.equal(getHeadersCount, 2); + assert.equal(invalidateCount, 1); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("ManagementClient does not loop infinitely when retry also returns 401", async () => { + const oldFetch = globalThis.fetch; + let fetchCount = 0; + let invalidateCount = 0; + + globalThis.fetch = (async () => { + fetchCount += 1; + return jsonResponse(401, { error: "still-unauthorized" }); + }) as typeof fetch; + + const client = new ManagementClient({ + baseUrl: "https://management.example", + auth: { + getHeaders: async () => ({ Authorization: "Bearer token" }), + invalidate: () => { + invalidateCount += 1; + } + } + }); + + try { + const res = await client.get<{ error: string }>("/v1/projects/p/environments"); + assert.equal(res.status, 401); + assert.equal(res.data?.error, "still-unauthorized"); + assert.equal(fetchCount, 2); + assert.equal(invalidateCount, 1); + } finally { + globalThis.fetch = oldFetch; + } +}); diff --git a/test/compose.oauth-base.test.ts b/test/compose.oauth-base.test.ts new file mode 100644 index 0000000..f902787 --- /dev/null +++ b/test/compose.oauth-base.test.ts @@ -0,0 +1,109 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { oauthClientCredentialsAuth } from "../src/compose/auth/providers/oauth-base.js"; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +test("oauthClientCredentialsAuth caches token between calls until invalidated", async () => { + const oldFetch = globalThis.fetch; + let tokenCallCount = 0; + + globalThis.fetch = (async () => { + tokenCallCount += 1; + return jsonResponse(200, { + access_token: `token-${tokenCallCount}`, + expires_in: 3600 + }); + }) as typeof fetch; + + try { + const auth = oauthClientCredentialsAuth({ + tokenUrl: "https://auth.example/token", + clientId: "client", + clientSecret: "secret" + }); + + const h1 = await auth.getHeaders(); + const h2 = await auth.getHeaders(); + + assert.equal(h1.Authorization, "Bearer token-1"); + assert.equal(h2.Authorization, "Bearer token-1"); + assert.equal(tokenCallCount, 1); + + auth.invalidate?.(); + const h3 = await auth.getHeaders(); + assert.equal(h3.Authorization, "Bearer token-2"); + assert.equal(tokenCallCount, 2); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("oauthClientCredentialsAuth deduplicates concurrent token requests", async () => { + const oldFetch = globalThis.fetch; + let tokenCallCount = 0; + let release: (() => void) | null = null; + const gate = new Promise((resolve) => { + release = resolve; + }); + + globalThis.fetch = (async () => { + tokenCallCount += 1; + await gate; + return jsonResponse(200, { + access_token: "shared-token", + expires_in: 3600 + }); + }) as typeof fetch; + + try { + const auth = oauthClientCredentialsAuth({ + tokenUrl: "https://auth.example/token", + clientId: "client", + clientSecret: "secret" + }); + + const p1 = auth.getHeaders(); + const p2 = auth.getHeaders(); + const p3 = auth.getHeaders(); + + release?.(); + const [h1, h2, h3] = await Promise.all([p1, p2, p3]); + + assert.equal(h1.Authorization, "Bearer shared-token"); + assert.equal(h2.Authorization, "Bearer shared-token"); + assert.equal(h3.Authorization, "Bearer shared-token"); + assert.equal(tokenCallCount, 1); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("oauthClientCredentialsAuth surfaces token endpoint errors", async () => { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async () => + new Response("unauthorized", { + status: 401, + headers: { "content-type": "text/plain" } + })) as typeof fetch; + + try { + const auth = oauthClientCredentialsAuth({ + tokenUrl: "https://auth.example/token", + clientId: "client", + clientSecret: "secret" + }); + + await assert.rejects( + () => auth.getHeaders(), + /OAuth token request failed \(401\): unauthorized/ + ); + } finally { + globalThis.fetch = oldFetch; + } +}); diff --git a/test/compose.state.test.ts b/test/compose.state.test.ts new file mode 100644 index 0000000..c7fa655 --- /dev/null +++ b/test/compose.state.test.ts @@ -0,0 +1,70 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { + COMPOSE_STATE_FILE, + ensureStateForAliases, + findEnvironmentByAlias, + markEnvironmentRemoteAlias, + readComposeState, + renameEnvironmentAlias, + writeComposeState +} from "../src/compose/state.js"; + +test("ensureStateForAliases creates and extends state while preserving existing env ids", () => { + const first = ensureStateForAliases(null, "proj-a", ["dev"]).state; + assert.equal(first.project, "proj-a"); + assert.equal(first.environments.length, 1); + assert.equal(first.environments[0]?.alias, "dev"); + + const originalId = first.environments[0]?.id; + assert.ok(originalId); + + const second = ensureStateForAliases(first, "proj-a", ["dev", "stage"]).state; + assert.equal(second.environments.length, 2); + assert.equal(second.environments.find((e) => e.alias === "dev")?.id, originalId); + assert.ok(second.environments.find((e) => e.alias === "stage")?.id); +}); + +test("renameEnvironmentAlias updates alias and keeps remoteAlias mapping stable by env id", () => { + const state = ensureStateForAliases(null, "proj-b", ["iac-dev"]).state; + const env = findEnvironmentByAlias(state, "iac-dev"); + assert.ok(env); + + const marked = markEnvironmentRemoteAlias(state, env.id, "iac-dev"); + assert.equal(marked, true); + + const renamed = renameEnvironmentAlias(state, "iac-dev", "iac-development"); + assert.equal(renamed, true); + + const after = findEnvironmentByAlias(state, "iac-development"); + assert.ok(after); + assert.equal(after.id, env.id); + assert.equal(after.remoteAlias, "iac-dev"); +}); + +test("readComposeState returns null when file is missing and throws on invalid JSON", async () => { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-state-test-")); + const missing = await readComposeState(tmpRoot); + assert.equal(missing, null); + + const statePath = path.join(tmpRoot, COMPOSE_STATE_FILE); + await fs.writeFile(statePath, "{invalid json", "utf8"); + await assert.rejects(() => readComposeState(tmpRoot)); +}); + +test("writeComposeState and readComposeState roundtrip", async () => { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-state-roundtrip-")); + const initial = ensureStateForAliases(null, "proj-c", ["dev", "prod"]).state; + await writeComposeState(tmpRoot, initial); + + const read = await readComposeState(tmpRoot); + assert.ok(read); + assert.equal(read.project, "proj-c"); + assert.deepEqual( + read.environments.map((e) => e.alias).sort(), + ["dev", "prod"] + ); +}); From f630bad4f43c66850e8a679f838235ca6515a720 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 13:31:37 -0500 Subject: [PATCH 06/47] Add testing documentation. --- docs/testing.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/testing.md diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..0a8671e --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,101 @@ +# Testing Strategy (Initial) + +## Run Tests +- `npm test` +- `npm run test:watch` + +## Current Coverage (Critical Path) +- `test/compose.state.test.ts` + - state initialization and alias mapping growth + - alias rename behavior preserving stable `id` + - `remoteAlias` tracking behavior + - state read/write roundtrip and invalid JSON handling + +- `test/compose.apply.environment-alias-routing.test.ts` + - `plan` with pending env rename reads nested resources using old remote alias path + - `apply` with pending env rename calls the rename endpoint with correct payload: + - `POST /v1/projects/{projectAlias}/environments/{environmentAlias}/commands/rename` + - `{ "newEnvironmentAlias": "" }` + - after rename in apply flow, nested resource calls use the new alias path + +- `test/commands.env-rename.test.ts` + - updates `umbraco-compose.yaml` environment key and state alias mapping + - preserves stable local env id and `remoteAlias` continuity across rename + - supports `--moveDir` for default env directories (`./env/`) + - rejects rename to existing alias without mutating config/state files + - rejects `--moveDir` when destination directory already exists (no config/state mutation) + - supports `--moveDir` when source directory is missing (config/state still updated) + +- `test/commands.apply.test.ts` + - verifies operation output includes environment context (`[env=]`) + - verifies successful apply writes `compose.lock.json` + - verifies successful apply updates `compose.state.json` with `remoteAlias` + +- `test/compose.apply.failures.test.ts` + - verifies environment list failure produces environment warning and short-circuits nested operations + - verifies environment rename failure (e.g. `409`) produces warning with status and short-circuits nested operations + +- `test/commands.plan-exit.test.ts` + - verifies plan mode sets `process.exitCode = 1` when warnings are emitted + +- `test/commands.apply.auth-failures.test.ts` + - verifies apply/plan fails with clear errors when OAuth token request returns non-200 + - verifies apply/plan fails when token response omits `access_token` + +- `test/commands.apply.create-env-failure.test.ts` + - verifies create-environment failure sets non-zero exit behavior + - verifies lock/state writes are skipped under env create failure + +- `test/commands.status-validate-exit.test.ts` + - verifies `status --failOnChanges` sets `process.exitCode = 1` when lock-backed drift exists + - verifies `validate --strict` sets `process.exitCode = 1` when warnings exist + +- `test/compose.management-client.test.ts` + - verifies first-call `401` triggers one auth invalidation and one retry + - verifies retry does not loop infinitely when second call is also `401` + +- `test/commands.apply.partial-failure-writes.test.ts` + - verifies any nested resource warning causes apply to skip lock/state writes + +- `test/compose.oauth-base.test.ts` + - verifies OAuth client-credentials token caching between calls + - verifies `invalidate()` forces token refresh + - verifies concurrent header requests share one in-flight token request + - verifies token endpoint failures surface clear errors + +- `test/commands.apply.output-snapshots.test.ts` + - snapshot-style assertions for exact plan/apply operation lines + - snapshot-style assertions for final plan/apply summary lines + +- `test/contracts.apply-openapi-shape.test.ts` + - contract-shape assertions for key apply endpoints and payloads: + - environment create path + `environmentAlias` payload + - collection create path + payload (`collectionAlias`, `description`) + - environment rename path + `newEnvironmentAlias` payload + - persisted document create payload uses `document` field + - persisted document create emits string `description` even when local description is omitted + - ingestion-function create path + payload (`ingestionFunctionAlias`, `description`, `script`) + - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) + - type-schema update-schema endpoint receives raw schema JSON body + - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) + +- `test/contracts.apply-contract-map.test.ts` + - validates emitted apply write calls against `docs/contracts/apply-contract.json` + - catches drift between documented endpoint/payload contract and implementation + +- `test/commands.generate.test.ts` + - verifies `compose generate` creates/updates local files for: + - `collection` + - `type-schema` + - `ingestion-function` + - verifies duplicate alias protection + +## Why This First +These tests protect the highest-risk behavior we recently fixed: +- local/remote environment alias drift reconciliation +- rename correctness against API contract +- plan/apply pathing differences that can silently regress + +## Next High-Value Additions +- contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented +- command-level tests for `generate persisted-doc` and `generate webhook` scaffolding output From f74c260bd2afb5373a32f3ba8778db5dbf39aa96 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 13:33:49 -0500 Subject: [PATCH 07/47] Update package.json for running tests --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ab3696..c647060 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "dev": "tsx src/index.ts", "build": "tsc -p tsconfig.json", "start": "node dist/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --import tsx --test test/**/*.test.ts", + "test:watch": "node --import tsx --test --watch test/**/*.test.ts" }, "keywords": [ "umbraco", From 4ced1a6c2b125fd68021d7a4c5afba3971dc71e3 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 13:35:22 -0500 Subject: [PATCH 08/47] Add checks and tests for API contract shape --- docs/contracts/apply-contract.json | 52 +++ src/commands/apply.ts | 4 +- src/compose/apply.ts | 4 +- test/contracts.apply-contract-map.test.ts | 356 ++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 516 +++++++++++++++++++++ 5 files changed, 929 insertions(+), 3 deletions(-) create mode 100644 docs/contracts/apply-contract.json create mode 100644 test/contracts.apply-contract-map.test.ts create mode 100644 test/contracts.apply-openapi-shape.test.ts diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json new file mode 100644 index 0000000..f10750c --- /dev/null +++ b/docs/contracts/apply-contract.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "operations": { + "environmentCreate": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments", + "requiredBodyKeys": ["environmentAlias", "description"] + }, + "environmentRename": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/commands/rename", + "requiredBodyKeys": ["newEnvironmentAlias"] + }, + "collectionCreate": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections", + "requiredBodyKeys": ["collectionAlias", "description"] + }, + "typeSchemaCreate": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas", + "requiredBodyKeys": ["typeSchemaAlias", "description", "schema"] + }, + "typeSchemaUpdateSchema": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}/commands/update-schema", + "requiredBodyKeys": [] + }, + "ingestionFunctionCreate": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion", + "requiredBodyKeys": ["ingestionFunctionAlias", "description", "script"] + }, + "persistedDocumentCreate": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents", + "requiredBodyKeys": ["persistedDocumentAlias", "description", "document"] + }, + "webhookCreate": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks", + "requiredBodyKeys": [ + "webhookAlias", + "url", + "eventTypes", + "collectionAliases", + "typeSchemaAliases", + "customHeaders" + ] + } + } +} diff --git a/src/commands/apply.ts b/src/commands/apply.ts index f822da2..b3a94ce 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -173,7 +173,7 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { byKind[op.kind] += 1; } - if (!planOnly) { + if (!planOnly && byKind.warn === 0) { const cliVersion = getCliVersionSafe(); const gitCommit = await getGitCommit(rootDir); @@ -203,6 +203,8 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { if (stateResult.changed) { console.log(`Wrote state file: ${statePath}`); } + } else if (!planOnly) { + console.log("\nSkipped lock/state writes because warnings were emitted."); } for (const op of ops) { diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 29ee6e9..b6b6cf4 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -509,8 +509,8 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): const createPath = `${envBase(opts)}/graphql/persisted-documents`; const body: any = { persistedDocumentAlias: alias, - description: d.description ?? null, - query + description: d.description ?? "", + document: query }; const res = await client.post(createPath, body); if (res.status >= 400) { diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts new file mode 100644 index 0000000..ddbcb99 --- /dev/null +++ b/test/contracts.apply-contract-map.test.ts @@ -0,0 +1,356 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import { applyEnvironment } from "../src/compose/apply.js"; + +type ContractMap = { + version: number; + operations: Record< + string, + { + method: string; + pathTemplate: string; + requiredBodyKeys: string[]; + } + >; +}; + +type CapturedCall = { + method: string; + pathname: string; + body?: Record | unknown; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function readContractMap(): Promise { + const filePath = path.resolve(process.cwd(), "docs/contracts/apply-contract.json"); + const text = await fs.readFile(filePath, "utf8"); + return JSON.parse(text) as ContractMap; +} + +function resolvePath(template: string, params: Record): string { + let out = template; + for (const [k, v] of Object.entries(params)) { + out = out.replaceAll(`{${k}}`, v); + } + return out; +} + +function assertContractCall( + contracts: ContractMap, + operation: keyof ContractMap["operations"], + calls: CapturedCall[], + params: Record +): void { + const contract = contracts.operations[operation]; + assert.ok(contract, `Missing contract entry for ${operation}`); + + const path = resolvePath(contract.pathTemplate, params); + const call = calls.find((c) => c.method === contract.method && c.pathname === path); + assert.ok(call, `Did not find contract call for ${operation}: ${contract.method} ${path}`); + + if (contract.requiredBodyKeys.length > 0) { + assert.ok(call.body && typeof call.body === "object", `${operation} body was not an object`); + const body = call.body as Record; + for (const key of contract.requiredBodyKeys) { + assert.ok(key in body, `${operation} body missing required key: ${key}`); + } + } +} + +async function makeFullEnvDir(): Promise { + const envDir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-contract-map-full-")); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content", description: "Main content" }] }, null, 2), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "type-schemas", "software.schema.json"), + JSON.stringify( + { + alias: "software", + description: "Software", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + } + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "type-schemas", "article.schema.json"), + JSON.stringify( + { + alias: "article", + description: "Article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion.json"), + JSON.stringify( + { + functions: [ + { + alias: "map-content", + description: "Maps content", + scriptFile: "functions/ingestion/map-content.js" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion", "map-content.js"), + "export default (x) => x;", + "utf8" + ); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { + documents: [ + { alias: "doc-1", description: "Query", queryFile: "graphql/persisted/doc-1.gql" } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-1.gql"), "query { ping }", "utf8"); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "send-on-create", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["software"], + customHeaders: { "x-api-key": "abc123" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + return envDir; +} + +async function mkEnvDir(prefix: string): Promise { + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); +} + +test("apply emitted calls match contract map for currently implemented write operations", async () => { + const contracts = await readContractMap(); + const envDir = await makeFullEnvDir(); + const calls: CapturedCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/software") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { oldField: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "software" }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { persistedDocumentAlias: "doc-1" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + const params = { + projectAlias: "proj", + environmentAlias: "dev", + typeSchemaAlias: "article" + }; + + assertContractCall(contracts, "collectionCreate", calls, params); + assertContractCall(contracts, "typeSchemaCreate", calls, params); + assertContractCall(contracts, "typeSchemaUpdateSchema", calls, params); + assertContractCall(contracts, "ingestionFunctionCreate", calls, params); + assertContractCall(contracts, "persistedDocumentCreate", calls, params); + assertContractCall(contracts, "webhookCreate", calls, params); +}); + +test("environment create and rename calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-env-"); + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { environmentAlias: "dev" }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "environmentCreate", calls, { + projectAlias: "proj" + }); + + const renameCalls: CapturedCall[] = []; + const oldFetch2 = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + renameCalls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" && method === "POST") { + return jsonResponse(200, { environmentAlias: "dev" }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + remoteEnvAlias: "old-dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch2; + } + + assertContractCall(contracts, "environmentRename", renameCalls, { + projectAlias: "proj", + environmentAlias: "old-dev" + }); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts new file mode 100644 index 0000000..9ee8160 --- /dev/null +++ b/test/contracts.apply-openapi-shape.test.ts @@ -0,0 +1,516 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; + +type FetchCall = { + method: string; + url: string; + bodyText?: string; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function mkEnvDir(prefix: string): Promise { + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); +} + +function installFetchMock(handler: (u: URL, method: string, bodyText?: string) => Response) { + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + return handler(u, method, bodyText); + }) as typeof fetch; + + return { + calls, + restore: () => { + globalThis.fetch = oldFetch; + } + }; +} + +test("environment create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-create-env-"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { environmentAlias: "dev" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.environmentAlias, "dev"); + assert.equal(body.description, "Development"); +}); + +test("collection create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-collection-create-"); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content", description: "Main content" }] }, null, 2), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/collections") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.collectionAlias, "content"); + assert.equal(body.description, "Main content"); +}); + +test("environment rename uses expected endpoint and payload key", async () => { + const envDir = await mkEnvDir("compose-contract-rename-env-"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" && method === "POST") { + return jsonResponse(200, { environmentAlias: "dev" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + remoteEnvAlias: "old-dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const renameCall = calls.find( + (c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/old-dev/commands/rename") + ); + assert.ok(renameCall); + const body = JSON.parse(renameCall.bodyText ?? "{}") as Record; + assert.deepEqual(body, { newEnvironmentAlias: "dev" }); +}); + +test("persisted-document create uses expected payload field 'document'", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { documents: [{ alias: "doc-1", description: "desc", queryFile: "graphql/persisted/doc-1.gql" }] }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-1.gql"), "query { ping }", "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { persistedDocumentAlias: "doc-1" }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.persistedDocumentAlias, "doc-1"); + assert.equal(body.description, "desc"); + assert.equal(body.document, "query { ping }"); + assert.equal("query" in body, false); +}); + +test("persisted-document create sends string description when local description is omitted", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-missing-desc-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { documents: [{ alias: "doc-2", queryFile: "graphql/persisted/doc-2.gql" }] }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-2.gql"), "query { pong }", "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { persistedDocumentAlias: "doc-2" }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.persistedDocumentAlias, "doc-2"); + assert.equal(body.description, ""); + assert.equal(body.document, "query { pong }"); +}); + +test("ingestion-function create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-ingestion-create-"); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "functions", "ingestion.json"), + JSON.stringify( + { + functions: [ + { + alias: "map-content", + description: "Maps content", + scriptFile: "functions/ingestion/map-content.js" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion", "map-content.js"), + "export default function(x){ return x; }", + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.ingestionFunctionAlias, "map-content"); + assert.equal(body.description, "Maps content"); + assert.equal(typeof body.script, "string"); +}); + +test("webhook create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-webhook-create-"); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "send-on-create", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { "x-api-key": "abc123" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/webhooks") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.webhookAlias, "send-on-create"); + assert.equal(body.url, "https://example.com/hook"); + assert.deepEqual(body.eventTypes, ["content.ingested"]); + assert.deepEqual(body.collectionAliases, ["content"]); + assert.deepEqual(body.typeSchemaAliases, ["article"]); + assert.deepEqual(body.customHeaders, { "x-api-key": "abc123" }); +}); + +test("type-schema update uses raw schema body at update-schema command endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + + const desiredSchema = { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + }; + + await fs.writeFile( + path.join(envDir, "type-schemas", "article.schema.json"), + JSON.stringify( + { + alias: "article", + description: "Article", + schema: desiredSchema + }, + null, + 2 + ), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method, bodyText) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article", + schema: { ...desiredSchema, properties: { title: { type: "string" } } } + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema" && + method === "PUT" + ) { + const parsed = JSON.parse(bodyText ?? "{}"); + return jsonResponse(200, parsed); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const updateCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema") + ); + assert.ok(updateCall); + const body = JSON.parse(updateCall.bodyText ?? "{}") as Record; + assert.deepEqual(body, desiredSchema); + assert.equal("schema" in body, false); +}); + +test("type-schema create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-create-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + + const desiredSchema = { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + }; + + await fs.writeFile( + path.join(envDir, "type-schemas", "software.schema.json"), + JSON.stringify( + { + alias: "software", + description: "Software schema", + schema: desiredSchema + }, + null, + 2 + ), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/software" && method === "GET") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "software" }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const createCall = calls.find( + (c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/type-schemas") + ); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}") as Record; + assert.equal(body.typeSchemaAlias, "software"); + assert.equal(body.description, "Software schema"); + assert.deepEqual(body.schema, desiredSchema); +}); From 8288a45515857bf42faf61b8f0450719121ec6b4 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 13:36:03 -0500 Subject: [PATCH 09/47] Add generate command functionality --- docs/commands.md | 12 +- src/commands/generate.ts | 266 +++++++++++++++++++++++++++++++++ src/index.ts | 2 + test/commands.generate.test.ts | 122 +++++++++++++++ 4 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 src/commands/generate.ts create mode 100644 test/commands.generate.test.ts diff --git a/docs/commands.md b/docs/commands.md index e43bab0..8178d0e 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -54,9 +54,15 @@ Alias routing semantics: - Idempotency: with `--merge`, only missing files are created. ### `compose generate ` -- Status: planned +- Status: implemented (MVP) - Responsibility: - scaffold entity files (schema/function/query/webhook/etc.) into the selected environment directory +- Implemented entities: + - `collection` + - `type-schema` + - `ingestion-function` + - `persisted-doc` + - `webhook` - Side effects: writes/updates local files only. - Idempotency: should support deterministic output and safe reruns. @@ -98,10 +104,10 @@ Alias routing semantics: - Responsibility: - execute planned operations against management API - perform environment rename with `POST /v1/projects/{projectAlias}/environments/{environmentAlias}/commands/rename` and body `{ "newEnvironmentAlias": "" }` when rename intent exists - - update `compose.lock.json` - - update `compose.state.json.remoteAlias` when environment operation succeeds + - update `compose.lock.json` and `compose.state.json.remoteAlias` only when apply completes with zero warnings - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output + - if any warning is emitted, lock/state writes are skipped for safety - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. diff --git a/src/commands/generate.ts b/src/commands/generate.ts new file mode 100644 index 0000000..572bd89 --- /dev/null +++ b/src/commands/generate.ts @@ -0,0 +1,266 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { CommandModule } from "yargs"; +import YAML from "yaml"; + +type Entity = "collection" | "type-schema" | "ingestion-function" | "persisted-doc" | "webhook"; + +type Args = { + dir: string; + env: string; + entity: Entity; + alias: string; + description?: string; + url?: string; +}; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +type CollectionsFile = { + collections: Array<{ alias: string; description?: string | null }>; +}; + +type IngestionRegistry = { + functions: Array<{ alias: string; description?: string | null; scriptFile: string }>; +}; + +type PersistedRegistry = { + documents: Array<{ alias: string; description?: string | null; queryFile: string }>; +}; + +type WebhooksFile = { + webhooks: Array<{ + alias: string; + description?: string; + url: string; + eventTypes: string[]; + collectionAliases?: string[]; + typeSchemaAliases?: string[]; + customHeaders?: Record; + }>; +}; + +export const generateCommand: CommandModule<{}, Args> = { + command: "generate ", + describe: "Generate local Compose entity files/entries for a specific environment", + builder: { + dir: { + type: "string", + default: "./compose", + describe: "Root Compose IaC directory (contains umbraco-compose.yaml)" + }, + env: { + type: "string", + default: "dev", + describe: "Environment alias in umbraco-compose.yaml" + }, + entity: { + type: "string", + choices: ["collection", "type-schema", "ingestion-function", "persisted-doc", "webhook"], + describe: "Entity type to generate" + }, + alias: { + type: "string", + describe: "Alias for the new entity" + }, + description: { + type: "string", + describe: "Optional description override" + }, + url: { + type: "string", + describe: "Webhook URL (only used for entity=webhook)" + } + }, + handler: async (args) => { + const rootDir = path.resolve(process.cwd(), args.dir); + const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); + const cfg = await readComposeConfig(composeYamlPath); + const envCfg = cfg.environments[args.env]; + if (!envCfg) { + throw new Error(`Environment "${args.env}" not found in ${composeYamlPath}`); + } + + const envDir = path.resolve(rootDir, envCfg.dir); + await fs.mkdir(envDir, { recursive: true }); + + switch (args.entity) { + case "collection": + await generateCollection(envDir, args.alias, args.description); + break; + case "type-schema": + await generateTypeSchema(envDir, args.alias, args.description); + break; + case "ingestion-function": + await generateIngestionFunction(envDir, args.alias, args.description); + break; + case "persisted-doc": + await generatePersistedDoc(envDir, args.alias, args.description); + break; + case "webhook": + await generateWebhook(envDir, args.alias, args.description, args.url); + break; + default: + throw new Error(`Unsupported entity: ${args.entity}`); + } + + console.log(`Generated ${args.entity}:${args.alias} for env "${args.env}"`); + } +}; + +async function generateCollection(envDir: string, alias: string, description?: string): Promise { + const filePath = path.join(envDir, "collections.json"); + const doc = await readJson(filePath, { collections: [] }); + if (doc.collections.some((c) => c.alias === alias)) { + throw new Error(`Collection "${alias}" already exists in ${filePath}`); + } + doc.collections.push({ + alias, + description: description ?? `${alias} collection` + }); + await writeJson(filePath, doc); +} + +async function generateTypeSchema(envDir: string, alias: string, description?: string): Promise { + const filePath = path.join(envDir, "type-schemas", `${alias}.schema.json`); + if (await exists(filePath)) { + throw new Error(`Type schema "${alias}" already exists at ${filePath}`); + } + const doc = { + alias, + description: description ?? `${alias} type schema`, + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: {} + } + }; + await writeJson(filePath, doc); +} + +async function generateIngestionFunction(envDir: string, alias: string, description?: string): Promise { + const registryPath = path.join(envDir, "functions", "ingestion.json"); + const scriptRel = `functions/ingestion/${alias}.js`; + const scriptAbs = path.join(envDir, scriptRel); + const reg = await readJson(registryPath, { functions: [] }); + + if (reg.functions.some((f) => f.alias === alias)) { + throw new Error(`Ingestion function "${alias}" already exists in ${registryPath}`); + } + if (await exists(scriptAbs)) { + throw new Error(`Ingestion function script already exists: ${scriptAbs}`); + } + + reg.functions.push({ + alias, + description: description ?? `${alias} ingestion function`, + scriptFile: scriptRel + }); + + await writeJson(registryPath, reg); + await writeText( + scriptAbs, + [ + "export default function(input) {", + " return input;", + "}", + "" + ].join("\n") + ); +} + +async function generatePersistedDoc(envDir: string, alias: string, description?: string): Promise { + const registryPath = path.join(envDir, "graphql", "persisted.json"); + const queryRel = `graphql/persisted/${alias}.gql`; + const queryAbs = path.join(envDir, queryRel); + const reg = await readJson(registryPath, { documents: [] }); + + if (reg.documents.some((d) => d.alias === alias)) { + throw new Error(`Persisted document "${alias}" already exists in ${registryPath}`); + } + if (await exists(queryAbs)) { + throw new Error(`Persisted document query already exists: ${queryAbs}`); + } + + reg.documents.push({ + alias, + description: description ?? `${alias} persisted document`, + queryFile: queryRel + }); + + await writeJson(registryPath, reg); + await writeText( + queryAbs, + [ + "query Example {", + " content {", + " items {", + " __typename", + " }", + " }", + "}", + "" + ].join("\n") + ); +} + +async function generateWebhook( + envDir: string, + alias: string, + description: string | undefined, + url: string | undefined +): Promise { + const filePath = path.join(envDir, "webhooks.json"); + const doc = await readJson(filePath, { webhooks: [] }); + if (doc.webhooks.some((w) => w.alias === alias)) { + throw new Error(`Webhook "${alias}" already exists in ${filePath}`); + } + doc.webhooks.push({ + alias, + ...(description ? { description } : {}), + url: url ?? "https://example.com/webhook", + eventTypes: ["content.ingested"], + collectionAliases: [], + typeSchemaAliases: [], + customHeaders: {} + }); + await writeJson(filePath, doc); +} + +async function readComposeConfig(filePath: string): Promise { + const text = await fs.readFile(filePath, "utf8"); + const doc = YAML.parse(text) as ComposeConfig; + if (!doc?.project || !doc?.environments) { + throw new Error(`Invalid umbraco-compose.yaml: missing project/environments`); + } + return doc; +} + +async function readJson(filePath: string, fallback: T): Promise { + if (!(await exists(filePath))) return fallback; + const text = await fs.readFile(filePath, "utf8"); + return JSON.parse(text) as T; +} + +async function writeJson(filePath: string, value: unknown): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf8"); +} + +async function writeText(filePath: string, value: string): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, value, "utf8"); +} + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/src/index.ts b/src/index.ts index 140ea39..cf608d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,11 +8,13 @@ import { applyCommand } from "./commands/apply.js"; import { statusCommand } from "./commands/status.js"; import { planCommand } from "./commands/plan.js"; import { envRenameCommand } from "./commands/env-rename.js"; +import { generateCommand } from "./commands/generate.js"; await yargs(hideBin(process.argv)) .scriptName("compose") .command(initCommand) .command(validateCommand) + .command(generateCommand) .command(planCommand) .command(applyCommand) .command(statusCommand) diff --git a/test/commands.generate.test.ts b/test/commands.generate.test.ts new file mode 100644 index 0000000..ffcc35b --- /dev/null +++ b/test/commands.generate.test.ts @@ -0,0 +1,122 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +async function createGenerateFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} + +test("generate collection appends a collection entry", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Main content" + }); + + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")) as { + collections: Array<{ alias: string; description?: string }>; + }; + assert.equal(collections.collections.length, 1); + assert.equal(collections.collections[0]?.alias, "content"); + assert.equal(collections.collections[0]?.description, "Main content"); +}); + +test("generate type-schema creates schema file", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + + const schemaPath = path.join(envDir, "type-schemas", "article.schema.json"); + const schema = JSON.parse(await fs.readFile(schemaPath, "utf8")) as { + alias: string; + description?: string; + schema: { $schema: string; allOf: Array<{ $ref: string }>; properties: Record }; + }; + assert.equal(schema.alias, "article"); + assert.equal(schema.description, "Article schema"); + assert.equal(schema.schema.$schema, "https://umbracocompose.com/v1/schema"); + assert.ok(Array.isArray(schema.schema.allOf)); +}); + +test("generate ingestion-function creates script and updates registry", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Maps content" + }); + + const registry = JSON.parse(await fs.readFile(path.join(envDir, "functions", "ingestion.json"), "utf8")) as { + functions: Array<{ alias: string; description?: string; scriptFile: string }>; + }; + assert.equal(registry.functions.length, 1); + assert.equal(registry.functions[0]?.alias, "map-content"); + assert.equal(registry.functions[0]?.scriptFile, "functions/ingestion/map-content.js"); + + const script = await fs.readFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "utf8"); + assert.equal(script.includes("export default function"), true); +}); + +test("generate rejects duplicate aliases", async () => { + const { root } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Main content" + }); + + await assert.rejects(() => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Duplicate" + }) + ); +}); From 0688fefec4d645eaec7e57410677b927e65eaaa8 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 15:37:47 -0500 Subject: [PATCH 10/47] Improve generate command and tests --- docs/commands.md | 5 + docs/testing.md | 32 +++ src/commands/generate.ts | 50 ++++ test/commands.generate-apply-flow.test.ts | 179 ++++++++++++ .../commands.generate-apply-warn-flow.test.ts | 178 ++++++++++++ test/commands.generate-flow.test.ts | 165 +++++++++++ test/commands.generate-status-flow.test.ts | 179 ++++++++++++ test/commands.generate.test.ts | 270 ++++++++++++++++++ 8 files changed, 1058 insertions(+) create mode 100644 test/commands.generate-apply-flow.test.ts create mode 100644 test/commands.generate-apply-warn-flow.test.ts create mode 100644 test/commands.generate-flow.test.ts create mode 100644 test/commands.generate-status-flow.test.ts diff --git a/docs/commands.md b/docs/commands.md index 8178d0e..d862181 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -63,6 +63,11 @@ Alias routing semantics: - `ingestion-function` - `persisted-doc` - `webhook` +- Validation: + - alias must be kebab-case + - reserved/path-unsafe aliases are rejected + - `--url` is only valid for `webhook` + - alias reuse across different entity types is allowed (uniqueness is enforced per entity type) - Side effects: writes/updates local files only. - Idempotency: should support deterministic output and safe reruns. diff --git a/docs/testing.md b/docs/testing.md index 0a8671e..b4cb7f0 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -89,6 +89,38 @@ - `type-schema` - `ingestion-function` - verifies duplicate alias protection + - verifies invalid argument combinations (`--url` rejected for non-webhook entities) + - verifies alias guardrails (kebab-case validation and reserved alias rejection) + - verifies pre-existing target file collision handling: + - existing type-schema file + - existing ingestion script file + - existing persisted query file + - verifies cross-entity alias reuse policy (same alias is allowed across different entity types) + +- `test/commands.generate-flow.test.ts` + - verifies end-to-end baseline flow: + - generate core entities + - validate with `--strict` passes + - plan output contains expected create/noop operations with zero warnings + +- `test/commands.generate-apply-flow.test.ts` + - verifies generate -> apply integration: + - apply output contains expected create/noop operations with zero warnings + - successful apply writes `compose.lock.json` + - successful apply updates `compose.state.json` remote alias + +- `test/commands.generate-apply-warn-flow.test.ts` + - verifies generate -> apply warning path: + - warning is surfaced in apply output + - apply exits non-zero + - lock/state writes are skipped when warnings exist + +- `test/commands.generate-status-flow.test.ts` + - verifies generate -> status interaction transitions: + - `NO LOCK` before first apply + - `UNCHANGED` after successful apply + - `CHANGED` after local edits + - verifies `--failOnChanges` exit behavior for those transitions ## Why This First These tests protect the highest-risk behavior we recently fixed: diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 572bd89..187c522 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -44,6 +44,31 @@ type WebhooksFile = { }>; }; +const WINDOWS_RESERVED_NAMES = new Set([ + "con", + "prn", + "aux", + "nul", + "com1", + "com2", + "com3", + "com4", + "com5", + "com6", + "com7", + "com8", + "com9", + "lpt1", + "lpt2", + "lpt3", + "lpt4", + "lpt5", + "lpt6", + "lpt7", + "lpt8", + "lpt9" +]); + export const generateCommand: CommandModule<{}, Args> = { command: "generate ", describe: "Generate local Compose entity files/entries for a specific environment", @@ -77,6 +102,11 @@ export const generateCommand: CommandModule<{}, Args> = { } }, handler: async (args) => { + if (args.url && args.entity !== "webhook") { + throw new Error(`--url is only supported for entity=webhook (got entity=${args.entity})`); + } + validateAlias(args.alias); + const rootDir = path.resolve(process.cwd(), args.dir); const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); const cfg = await readComposeConfig(composeYamlPath); @@ -264,3 +294,23 @@ async function exists(p: string): Promise { return false; } } + +function validateAlias(alias: string): void { + if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(alias)) { + throw new Error( + `Invalid alias "${alias}". Alias must be kebab-case (lowercase letters, numbers, and hyphens).` + ); + } + + if (alias === "." || alias === "..") { + throw new Error(`Invalid alias "${alias}". Reserved path segment.`); + } + + if (alias.includes("/") || alias.includes("\\")) { + throw new Error(`Invalid alias "${alias}". Alias cannot contain path separators.`); + } + + if (WINDOWS_RESERVED_NAMES.has(alias.toLowerCase())) { + throw new Error(`Invalid alias "${alias}". Reserved filename.`); + } +} diff --git a/test/commands.generate-apply-flow.test.ts b/test/commands.generate-apply-flow.test.ts new file mode 100644 index 0000000..4939e4c --- /dev/null +++ b/test/commands.generate-apply-flow.test.ts @@ -0,0 +1,179 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-apply-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} + +test("generate -> apply writes lock/state and reports expected apply operations", async () => { + const { root, envDir } = await createFixture(); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Map content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "Webhook" + }); + + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "article" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { persistedDocumentAlias: "software-query" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + assert.equal(process.exitCode, 0); + } finally { + globalThis.fetch = oldFetch; + console.log = originalLog; + process.exitCode = oldExitCode; + } + + const lockPath = path.join(envDir, "compose.lock.json"); + const lockExists = await exists(lockPath); + assert.equal(lockExists, true); + + const state = await readComposeState(root); + assert.ok(state); + const envState = state.environments.find((e) => e.alias === "dev"); + assert.ok(envState); + assert.equal(envState.remoteAlias, "dev"); + + assert.equal(output.some((line) => line.includes("CREATE [env=dev] collection:content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] type-schema:article")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] ingestion-function:map-content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); + assert.equal( + output.some((line) => line.includes("Apply complete. create=5, update=0, noop=1, warn=0")), + true + ); +}); + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/test/commands.generate-apply-warn-flow.test.ts b/test/commands.generate-apply-warn-flow.test.ts new file mode 100644 index 0000000..1a6d608 --- /dev/null +++ b/test/commands.generate-apply-warn-flow.test.ts @@ -0,0 +1,178 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-apply-warn-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} + +test("generate -> apply warning path skips lock/state writes", async () => { + const { root, envDir } = await createFixture(); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Map content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "Webhook" + }); + + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "article" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(500, { error: "persisted-create-fail" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + assert.equal(process.exitCode, 1); + } finally { + globalThis.fetch = oldFetch; + console.log = originalLog; + process.exitCode = oldExitCode; + } + + const lockExists = await exists(path.join(envDir, "compose.lock.json")); + assert.equal(lockExists, false); + + const state = await readComposeState(root); + assert.equal(state, null); + + assert.equal( + output.some((line) => line.includes("WARN") && line.includes("[env=dev] persisted-doc:software-query")), + true + ); + assert.equal( + output.some((line) => line.includes("Skipped lock/state writes because warnings were emitted.")), + true + ); + assert.equal( + output.some((line) => line.includes("Apply complete. create=5, update=0, noop=1, warn=1")), + true + ); +}); + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/test/commands.generate-flow.test.ts b/test/commands.generate-flow.test.ts new file mode 100644 index 0000000..6ad3ed8 --- /dev/null +++ b/test/commands.generate-flow.test.ts @@ -0,0 +1,165 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { validateCommand } from "../src/commands/validate.js"; +import { runApply } from "../src/commands/apply.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-flow-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} + +test("generate -> validate --strict -> plan baseline succeeds with expected operations", async () => { + const { root } = await createFixture(); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Map content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "Webhook" + }); + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await validateCommand.handler({ + dir: root, + strict: true + }); + assert.equal(process.exitCode, 0); + } finally { + process.exitCode = oldExitCode; + } + + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + + const oldExitCode2 = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + assert.equal(process.exitCode, 0); + } finally { + globalThis.fetch = oldFetch; + console.log = originalLog; + process.exitCode = oldExitCode2; + } + + assert.equal(output.some((line) => line.includes("NOOP [env=dev] environment:dev")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] collection:content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] type-schema:article")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] ingestion-function:map-content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); + assert.equal( + output.some((line) => line.includes("Plan complete. create=5, update=0, noop=1, warn=0")), + true + ); +}); diff --git a/test/commands.generate-status-flow.test.ts b/test/commands.generate-status-flow.test.ts new file mode 100644 index 0000000..395aede --- /dev/null +++ b/test/commands.generate-status-flow.test.ts @@ -0,0 +1,179 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { statusCommand } from "../src/commands/status.js"; +import { runApply } from "../src/commands/apply.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +type StatusJson = { + project: string; + results: Array<{ + env: string; + groups: Record; + }>; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-status-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} + +async function runStatusJson(args: { + root: string; + failOnChanges: boolean; +}): Promise<{ data: StatusJson; exitCode: number | undefined }> { + const originalLog = console.log; + const out: string[] = []; + console.log = (...values: unknown[]) => { + out.push(values.map((v) => String(v)).join(" ")); + }; + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: args.root, + env: "dev", + json: true, + failOnChanges: args.failOnChanges + }); + const jsonText = out.find((line) => line.trim().startsWith("{")); + assert.ok(jsonText, "status command did not emit JSON output"); + return { + data: JSON.parse(jsonText) as StatusJson, + exitCode: process.exitCode + }; + } finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} + +test("generate -> status reports NO LOCK and sets non-zero exit with --failOnChanges", async () => { + const { root } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + + const result = await runStatusJson({ root, failOnChanges: true }); + const groups = result.data.results[0]?.groups; + assert.ok(groups); + assert.equal(groups.collections?.status, "NO LOCK"); + assert.equal(groups.webhooks?.status, "NO LOCK"); + assert.equal(result.exitCode, 1); +}); + +test("generate -> apply -> status transitions from UNCHANGED to CHANGED after local edit", async () => { + const { root, envDir } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + const oldExit = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + assert.equal(process.exitCode, 0); + } finally { + globalThis.fetch = oldFetch; + process.exitCode = oldExit; + } + + const unchanged = await runStatusJson({ root, failOnChanges: true }); + const unchangedGroups = unchanged.data.results[0]?.groups; + assert.ok(unchangedGroups); + assert.equal(unchangedGroups.collections?.status, "UNCHANGED"); + assert.equal(unchanged.exitCode, 0); + + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify( + { collections: [{ alias: "content", description: "Content updated" }] }, + null, + 2 + ) + "\n", + "utf8" + ); + + const changed = await runStatusJson({ root, failOnChanges: true }); + const changedGroups = changed.data.results[0]?.groups; + assert.ok(changedGroups); + assert.equal(changedGroups.collections?.status, "CHANGED"); + assert.equal(changed.exitCode, 1); +}); diff --git a/test/commands.generate.test.ts b/test/commands.generate.test.ts index ffcc35b..3b05746 100644 --- a/test/commands.generate.test.ts +++ b/test/commands.generate.test.ts @@ -100,6 +100,78 @@ test("generate ingestion-function creates script and updates registry", async () assert.equal(script.includes("export default function"), true); }); +test("generate persisted-doc creates query file and updates registry", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + + const registry = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")) as { + documents: Array<{ alias: string; description?: string; queryFile: string }>; + }; + assert.equal(registry.documents.length, 1); + assert.equal(registry.documents[0]?.alias, "software-query"); + assert.equal(registry.documents[0]?.description, "Software query"); + assert.equal(registry.documents[0]?.queryFile, "graphql/persisted/software-query.gql"); + + const query = await fs.readFile(path.join(envDir, "graphql", "persisted", "software-query.gql"), "utf8"); + assert.equal(query.includes("query Example"), true); +}); + +test("generate webhook creates webhook entry with default URL", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "My webhook" + }); + + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")) as { + webhooks: Array<{ + alias: string; + description?: string; + url: string; + eventTypes: string[]; + collectionAliases: string[]; + typeSchemaAliases: string[]; + customHeaders: Record; + }>; + }; + + assert.equal(webhooks.webhooks.length, 1); + assert.equal(webhooks.webhooks[0]?.alias, "send-on-create"); + assert.equal(webhooks.webhooks[0]?.description, "My webhook"); + assert.equal(webhooks.webhooks[0]?.url, "https://example.com/webhook"); + assert.deepEqual(webhooks.webhooks[0]?.eventTypes, ["content.ingested"]); + assert.deepEqual(webhooks.webhooks[0]?.collectionAliases, []); + assert.deepEqual(webhooks.webhooks[0]?.typeSchemaAliases, []); + assert.deepEqual(webhooks.webhooks[0]?.customHeaders, {}); +}); + +test("generate webhook accepts explicit URL", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-update", + description: "URL override", + url: "https://hooks.example.com/compose" + }); + + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")) as { + webhooks: Array<{ alias: string; url: string }>; + }; + assert.equal(webhooks.webhooks[0]?.alias, "send-on-update"); + assert.equal(webhooks.webhooks[0]?.url, "https://hooks.example.com/compose"); +}); + test("generate rejects duplicate aliases", async () => { const { root } = await createGenerateFixture(); await generateCommand.handler({ @@ -120,3 +192,201 @@ test("generate rejects duplicate aliases", async () => { }) ); }); + +test("generate rejects --url for non-webhook entities", async () => { + const { root, envDir } = await createGenerateFixture(); + const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + + await assert.rejects( + () => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + url: "https://hooks.example.com/compose" + }), + /--url is only supported for entity=webhook/ + ); + + const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + assert.equal(after, before); +}); + +test("generate rejects invalid alias format", async () => { + const { root, envDir } = await createGenerateFixture(); + const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + + await assert.rejects( + () => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "Not_Kebab" + }), + /must be kebab-case/ + ); + + const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + assert.equal(after, before); +}); + +test("generate rejects reserved alias names", async () => { + const { root, envDir } = await createGenerateFixture(); + const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + + await assert.rejects( + () => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "con" + }), + /Reserved filename/ + ); + + const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + assert.equal(after, before); +}); + +test("generate type-schema rejects when schema file already exists", async () => { + const { root, envDir } = await createGenerateFixture(); + const schemaPath = path.join(envDir, "type-schemas", "article.schema.json"); + await fs.writeFile( + schemaPath, + JSON.stringify( + { + alias: "article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [], + properties: {} + } + }, + null, + 2 + ) + "\n", + "utf8" + ); + const before = await fs.readFile(schemaPath, "utf8"); + + await assert.rejects( + () => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article" + }), + /already exists/ + ); + + const after = await fs.readFile(schemaPath, "utf8"); + assert.equal(after, before); +}); + +test("generate ingestion-function rejects when script file already exists", async () => { + const { root, envDir } = await createGenerateFixture(); + const scriptPath = path.join(envDir, "functions", "ingestion", "map-content.js"); + await fs.writeFile(scriptPath, "export default () => null;\n", "utf8"); + const registryPath = path.join(envDir, "functions", "ingestion.json"); + const beforeRegistry = await fs.readFile(registryPath, "utf8"); + + await assert.rejects( + () => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content" + }), + /script already exists/ + ); + + const afterRegistry = await fs.readFile(registryPath, "utf8"); + assert.equal(afterRegistry, beforeRegistry); +}); + +test("generate persisted-doc rejects when query file already exists", async () => { + const { root, envDir } = await createGenerateFixture(); + const queryPath = path.join(envDir, "graphql", "persisted", "software-query.gql"); + await fs.writeFile(queryPath, "query Existing { __typename }\n", "utf8"); + const registryPath = path.join(envDir, "graphql", "persisted.json"); + const beforeRegistry = await fs.readFile(registryPath, "utf8"); + + await assert.rejects( + () => + generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query" + }), + /query already exists/ + ); + + const afterRegistry = await fs.readFile(registryPath, "utf8"); + assert.equal(afterRegistry, beforeRegistry); +}); + +test("generate allows same alias across different entity types", async () => { + const { root, envDir } = await createGenerateFixture(); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "shared-alias", + description: "Shared collection alias" + }); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "shared-alias", + description: "Shared schema alias" + }); + + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")) as { + collections: Array<{ alias: string }>; + }; + const schema = JSON.parse( + await fs.readFile(path.join(envDir, "type-schemas", "shared-alias.schema.json"), "utf8") + ) as { alias: string }; + + assert.equal(collections.collections.some((c) => c.alias === "shared-alias"), true); + assert.equal(schema.alias, "shared-alias"); +}); + +test("generate allows same alias for webhook and persisted-doc", async () => { + const { root, envDir } = await createGenerateFixture(); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "shared", + description: "Webhook alias" + }); + + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "shared", + description: "Persisted alias" + }); + + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")) as { + webhooks: Array<{ alias: string }>; + }; + const persisted = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")) as { + documents: Array<{ alias: string }>; + }; + + assert.equal(webhooks.webhooks.some((w) => w.alias === "shared"), true); + assert.equal(persisted.documents.some((d) => d.alias === "shared"), true); +}); From a67c90c75f983bfd06b51b7ab877c490c8399704 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 15:38:49 -0500 Subject: [PATCH 11/47] Add test for multi-environment scenarios --- docs/testing.md | 8 +- test/commands.status-multi-env.test.ts | 129 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 test/commands.status-multi-env.test.ts diff --git a/docs/testing.md b/docs/testing.md index b4cb7f0..a7c0c12 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -50,6 +50,10 @@ - verifies `status --failOnChanges` sets `process.exitCode = 1` when lock-backed drift exists - verifies `validate --strict` sets `process.exitCode = 1` when warnings exist +- `test/commands.status-multi-env.test.ts` + - verifies `status` without `--env` reports all configured environments + - verifies mixed multi-env state (`UNCHANGED` + `NO LOCK`) sets non-zero exit with `--failOnChanges` + - `test/compose.management-client.test.ts` - verifies first-call `401` triggers one auth invalidation and one retry - verifies retry does not loop infinitely when second call is also `401` @@ -88,6 +92,8 @@ - `collection` - `type-schema` - `ingestion-function` + - `persisted-doc` + - `webhook` (default + explicit URL) - verifies duplicate alias protection - verifies invalid argument combinations (`--url` rejected for non-webhook entities) - verifies alias guardrails (kebab-case validation and reserved alias rejection) @@ -130,4 +136,4 @@ These tests protect the highest-risk behavior we recently fixed: ## Next High-Value Additions - contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented -- command-level tests for `generate persisted-doc` and `generate webhook` scaffolding output +- command-level tests for multi-environment apply/plan orchestration once batching is implemented diff --git a/test/commands.status-multi-env.test.ts b/test/commands.status-multi-env.test.ts new file mode 100644 index 0000000..5173d4d --- /dev/null +++ b/test/commands.status-multi-env.test.ts @@ -0,0 +1,129 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { statusCommand } from "../src/commands/status.js"; +import { computeDesiredHashes, type LockFile } from "../src/compose/lock.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +type StatusJson = { + project: string; + results: Array<{ + env: string; + hasLock: boolean; + groups: Record; + }>; +}; + +async function createEnvScaffold(envDir: string): Promise { + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); +} + +async function createFixture(): Promise<{ root: string; devDir: string; prodDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-status-multi-env-")); + const devDir = path.join(root, "env", "dev"); + const prodDir = path.join(root, "env", "prod"); + + await createEnvScaffold(devDir); + await createEnvScaffold(prodDir); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + }, + prod: { + dir: "./env/prod", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + + return { root, devDir, prodDir }; +} + +async function writeMatchingLock(envDir: string): Promise { + const desired = await computeDesiredHashes(envDir); + const lock: LockFile = { + version: 1, + project: "test-project", + environment: path.basename(envDir), + resources: desired + }; + await fs.writeFile(path.join(envDir, "compose.lock.json"), JSON.stringify(lock, null, 2) + "\n", "utf8"); +} + +async function runStatusJson(args: { + root: string; + failOnChanges: boolean; +}): Promise<{ data: StatusJson; exitCode: number | undefined }> { + const originalLog = console.log; + const out: string[] = []; + console.log = (...values: unknown[]) => { + out.push(values.map((v) => String(v)).join(" ")); + }; + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: args.root, + json: true, + failOnChanges: args.failOnChanges + }); + const jsonText = out.find((line) => line.trim().startsWith("{")); + assert.ok(jsonText, "status command did not emit JSON output"); + return { + data: JSON.parse(jsonText) as StatusJson, + exitCode: process.exitCode + }; + } finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} + +test("status without --env returns results for all configured environments", async () => { + const { root, devDir, prodDir } = await createFixture(); + await writeMatchingLock(devDir); + await writeMatchingLock(prodDir); + + const result = await runStatusJson({ root, failOnChanges: true }); + + const envs = result.data.results.map((r) => r.env).sort(); + assert.deepEqual(envs, ["dev", "prod"]); + assert.equal(result.exitCode, 0); +}); + +test("status across multiple environments sets exitCode=1 when any environment has changes/no lock", async () => { + const { root, devDir } = await createFixture(); + await writeMatchingLock(devDir); + // prod intentionally has no lock + + const result = await runStatusJson({ root, failOnChanges: true }); + const dev = result.data.results.find((r) => r.env === "dev"); + const prod = result.data.results.find((r) => r.env === "prod"); + + assert.ok(dev); + assert.ok(prod); + assert.equal(dev.groups.collections?.status, "UNCHANGED"); + assert.equal(prod.groups.collections?.status, "NO LOCK"); + assert.equal(result.exitCode, 1); +}); From be650506a1a479506a6da0b38af1b33b0415db67 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 15:47:50 -0500 Subject: [PATCH 12/47] Implement MVP for pull command --- docs/commands.md | 14 +- docs/testing.md | 7 + src/commands/pull.ts | 323 +++++++++++++++++++++++++++++++++++++ src/index.ts | 2 + test/commands.pull.test.ts | 210 ++++++++++++++++++++++++ 5 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 src/commands/pull.ts create mode 100644 test/commands.pull.test.ts diff --git a/docs/commands.md b/docs/commands.md index d862181..da5bd0a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -79,10 +79,20 @@ Alias routing semantics: - Idempotency: fully idempotent. ### `compose pull` -- Status: planned +- Status: implemented (MVP) - Responsibility: - fetch remote state and materialize files in local format - - update `compose.state.json` and lock data to match pulled state + - update `compose.state.json` to track remote alias for pulled environment +- Implemented pull scope: + - collections + - type schemas + - ingestion function registry + scripts + - persisted document registry + query files + - webhooks +- Notes: + - stale generated files in managed folders are removed when no longer present remotely + - updates `compose.state.json.remoteAlias` for pulled environment + - does not write `compose.lock.json` - Side effects: writes local files. - Idempotency: repeated pulls with unchanged remote state should produce no diffs. diff --git a/docs/testing.md b/docs/testing.md index a7c0c12..a725471 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -54,6 +54,12 @@ - verifies `status` without `--env` reports all configured environments - verifies mixed multi-env state (`UNCHANGED` + `NO LOCK`) sets non-zero exit with `--failOnChanges` +- `test/commands.pull.test.ts` + - verifies `pull` writes local IaC files from remote responses for implemented entities + - verifies stale managed files are removed during pull + - verifies `compose.state.json.remoteAlias` is updated for pulled environment + - verifies pull fails when target environment is missing remotely + - `test/compose.management-client.test.ts` - verifies first-call `401` triggers one auth invalidation and one retry - verifies retry does not loop infinitely when second call is also `401` @@ -137,3 +143,4 @@ These tests protect the highest-risk behavior we recently fixed: ## Next High-Value Additions - contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented - command-level tests for multi-environment apply/plan orchestration once batching is implemented +- command-level tests for `pull` idempotency and partial-failure behavior diff --git a/src/commands/pull.ts b/src/commands/pull.ts new file mode 100644 index 0000000..7501268 --- /dev/null +++ b/src/commands/pull.ts @@ -0,0 +1,323 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { CommandModule } from "yargs"; +import YAML from "yaml"; +import { ManagementClient } from "../compose/management-client.js"; +import { composeManagementOAuth } from "../compose/auth/providers/oauth-compose.js"; +import { + ensureStateForAliases, + findEnvironmentByAlias, + markEnvironmentRemoteAlias, + readComposeState, + writeComposeState +} from "../compose/state.js"; + +type Args = { + dir: string; + env: string; + clientId?: string; + clientSecret?: string; + scope?: string; + audience?: string; +}; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +type CollectionsFile = { collections: Array<{ alias: string; description?: string | null }> }; +type IngestionRegistry = { functions: Array<{ alias: string; description?: string | null; scriptFile: string }> }; +type PersistedRegistry = { documents: Array<{ alias: string; description?: string | null; queryFile: string }> }; +type WebhooksFile = { + webhooks: Array<{ + alias: string; + description?: string | null; + url: string; + eventTypes: string[]; + collectionAliases?: string[]; + typeSchemaAliases?: string[]; + customHeaders?: Record; + }>; +}; + +export const pullCommand: CommandModule<{}, Args> = { + command: "pull", + describe: "Pull remote Compose environment state into local IaC files", + builder: { + dir: { + type: "string", + default: "./compose", + describe: "Root Compose IaC directory (contains umbraco-compose.yaml)" + }, + env: { + type: "string", + default: "dev", + describe: "Environment alias in umbraco-compose.yaml" + }, + clientId: { + type: "string", + describe: "OAuth client id (or set COMPOSE_MGMT_CLIENT_ID)" + }, + clientSecret: { + type: "string", + describe: "OAuth client secret (or set COMPOSE_MGMT_CLIENT_SECRET)" + }, + scope: { + type: "string", + describe: "Optional OAuth scope" + }, + audience: { + type: "string", + describe: "Optional OAuth audience" + } + }, + handler: async (args) => { + const rootDir = path.resolve(process.cwd(), args.dir); + const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); + const cfg = await readComposeConfig(composeYamlPath); + const envCfg = cfg.environments[args.env]; + if (!envCfg) { + throw new Error(`Environment "${args.env}" not found in ${composeYamlPath}`); + } + + const clientId = args.clientId ?? process.env.COMPOSE_MGMT_CLIENT_ID; + const clientSecret = args.clientSecret ?? process.env.COMPOSE_MGMT_CLIENT_SECRET; + if (!clientId || !clientSecret) { + throw new Error( + "Missing OAuth credentials. Provide --clientId/--clientSecret or set COMPOSE_MGMT_CLIENT_ID and COMPOSE_MGMT_CLIENT_SECRET." + ); + } + + const envDir = path.resolve(rootDir, envCfg.dir); + await fs.mkdir(envDir, { recursive: true }); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const client = new ManagementClient({ + baseUrl: envCfg.managementBaseUrl, + auth: composeManagementOAuth({ + clientId, + clientSecret, + baseUrl: envCfg.managementBaseUrl + }) + }); + + const remoteEnvExists = await ensureRemoteEnvironmentExists(client, cfg.project, args.env); + if (!remoteEnvExists) { + throw new Error(`Environment "${args.env}" was not found remotely for project "${cfg.project}".`); + } + + await pullCollections(client, cfg.project, args.env, envDir); + await pullTypeSchemas(client, cfg.project, args.env, envDir); + await pullIngestionFunctions(client, cfg.project, args.env, envDir); + await pullPersistedDocs(client, cfg.project, args.env, envDir); + await pullWebhooks(client, cfg.project, args.env, envDir); + + const state = ensureStateForAliases(await readComposeState(rootDir), cfg.project, Object.keys(cfg.environments)).state; + const envState = findEnvironmentByAlias(state, args.env); + if (envState) { + markEnvironmentRemoteAlias(state, envState.id, args.env); + } + await writeComposeState(rootDir, state); + + console.log(`Pulled remote state for env "${args.env}" into ${envDir}`); + } +}; + +async function pullCollections(client: ManagementClient, project: string, env: string, envDir: string): Promise { + const res = await client.get(`${envBase(project, env)}/collections`); + if (res.status >= 400) { + throw new Error(`Failed to list collections (${res.status}).`); + } + const items = asNodes(res.data, ["collectionAlias", "alias"]); + const collections: CollectionsFile = { + collections: items + .map((c) => ({ + alias: String(c.collectionAlias ?? c.alias), + ...(typeof c.description === "string" || c.description === null ? { description: c.description } : {}) + })) + .sort((a, b) => a.alias.localeCompare(b.alias)) + }; + await writeJson(path.join(envDir, "collections.json"), collections); +} + +async function pullTypeSchemas(client: ManagementClient, project: string, env: string, envDir: string): Promise { + const list = await client.get(`${envBase(project, env)}/type-schemas`); + if (list.status >= 400) { + throw new Error(`Failed to list type schemas (${list.status}).`); + } + const items = asNodes(list.data, ["typeSchemaAlias", "alias"]); + const keep = new Set(); + + for (const item of items) { + const alias = String(item.typeSchemaAlias ?? item.alias); + const get = await client.get(`${envBase(project, env)}/type-schemas/${encodeURIComponent(alias)}`); + if (get.status >= 400) { + throw new Error(`Failed to get type schema "${alias}" (${get.status}).`); + } + const schema = get.data?.schema ?? get.data?.typeSchema?.schema; + const description = get.data?.description ?? get.data?.typeSchema?.description ?? null; + const out = { + alias, + description, + schema + }; + const file = path.join(envDir, "type-schemas", `${alias}.schema.json`); + await writeJson(file, out); + keep.add(path.basename(file)); + } + + await removeMissingByExtension(path.join(envDir, "type-schemas"), ".schema.json", keep); +} + +async function pullIngestionFunctions(client: ManagementClient, project: string, env: string, envDir: string): Promise { + const list = await client.get(`${envBase(project, env)}/functions/ingestion`); + if (list.status >= 400) { + throw new Error(`Failed to list ingestion functions (${list.status}).`); + } + const items = asNodes(list.data, ["ingestionFunctionAlias", "alias"]); + const registry: IngestionRegistry = { functions: [] }; + const keep = new Set(); + + for (const item of items) { + const alias = String(item.ingestionFunctionAlias ?? item.alias); + const get = await client.get(`${envBase(project, env)}/functions/ingestion/${encodeURIComponent(alias)}`); + if (get.status >= 400) { + throw new Error(`Failed to get ingestion function "${alias}" (${get.status}).`); + } + const script = get.data?.script ?? get.data?.ingestionFunction?.script ?? ""; + const description = get.data?.description ?? get.data?.ingestionFunction?.description ?? null; + const scriptRel = `functions/ingestion/${alias}.js`; + const scriptAbs = path.join(envDir, scriptRel); + await writeText(scriptAbs, `${script}\n`); + keep.add(path.basename(scriptAbs)); + registry.functions.push({ + alias, + description, + scriptFile: scriptRel + }); + } + + registry.functions.sort((a, b) => a.alias.localeCompare(b.alias)); + await writeJson(path.join(envDir, "functions", "ingestion.json"), registry); + await removeMissingByExtension(path.join(envDir, "functions", "ingestion"), ".js", keep); +} + +async function pullPersistedDocs(client: ManagementClient, project: string, env: string, envDir: string): Promise { + const list = await client.get(`${envBase(project, env)}/graphql/persisted-documents`); + if (list.status >= 400) { + throw new Error(`Failed to list persisted docs (${list.status}).`); + } + const items = asNodes(list.data, ["persistedDocumentAlias", "alias"]); + const registry: PersistedRegistry = { documents: [] }; + const keep = new Set(); + + for (const item of items) { + const alias = String(item.persistedDocumentAlias ?? item.alias); + const get = await client.get( + `${envBase(project, env)}/graphql/persisted-documents/${encodeURIComponent(alias)}` + ); + if (get.status >= 400) { + throw new Error(`Failed to get persisted doc "${alias}" (${get.status}).`); + } + const document = get.data?.document ?? get.data?.query ?? ""; + const description = get.data?.description ?? null; + const queryRel = `graphql/persisted/${alias}.gql`; + const queryAbs = path.join(envDir, queryRel); + await writeText(queryAbs, `${document}\n`); + keep.add(path.basename(queryAbs)); + registry.documents.push({ + alias, + description, + queryFile: queryRel + }); + } + + registry.documents.sort((a, b) => a.alias.localeCompare(b.alias)); + await writeJson(path.join(envDir, "graphql", "persisted.json"), registry); + await removeMissingByExtension(path.join(envDir, "graphql", "persisted"), ".gql", keep); +} + +async function pullWebhooks(client: ManagementClient, project: string, env: string, envDir: string): Promise { + const list = await client.get(`${envBase(project, env)}/webhooks`); + if (list.status >= 400) { + throw new Error(`Failed to list webhooks (${list.status}).`); + } + const items = asNodes(list.data, ["webhookAlias", "alias"]); + const webhooks: WebhooksFile = { webhooks: [] }; + + for (const item of items) { + const alias = String(item.webhookAlias ?? item.alias); + const get = await client.get(`${envBase(project, env)}/webhooks/${encodeURIComponent(alias)}`); + if (get.status >= 400) { + throw new Error(`Failed to get webhook "${alias}" (${get.status}).`); + } + const dto = get.data ?? {}; + webhooks.webhooks.push({ + alias, + ...(dto.description !== undefined ? { description: dto.description } : {}), + url: dto.url ?? "", + eventTypes: dto.eventTypes ?? [], + collectionAliases: dto.collectionAliases ?? [], + typeSchemaAliases: dto.typeSchemaAliases ?? [], + customHeaders: dto.customHeaders ?? {} + }); + } + + webhooks.webhooks.sort((a, b) => a.alias.localeCompare(b.alias)); + await writeJson(path.join(envDir, "webhooks.json"), webhooks); +} + +async function ensureRemoteEnvironmentExists(client: ManagementClient, project: string, env: string): Promise { + const res = await client.get(`/v1/projects/${encodeURIComponent(project)}/environments`); + if (res.status >= 400) { + throw new Error(`Failed to list environments (${res.status}).`); + } + const items = asNodes(res.data, ["environmentAlias", "alias"]); + return items.some((i) => String(i.environmentAlias ?? i.alias) === env); +} + +function asNodes(data: any, aliasKeys: string[]): any[] { + const arr = data?.items ?? data?.nodes ?? data?.edges?.map((e: any) => e?.node) ?? []; + if (!Array.isArray(arr)) return []; + return arr.filter((node) => { + if (!node || typeof node !== "object") return false; + return aliasKeys.some((k) => typeof node[k] === "string"); + }); +} + +function envBase(project: string, env: string): string { + return `/v1/projects/${encodeURIComponent(project)}/environments/${encodeURIComponent(env)}`; +} + +async function readComposeConfig(filePath: string): Promise { + const text = await fs.readFile(filePath, "utf8"); + const doc = YAML.parse(text) as ComposeConfig; + if (!doc?.project || !doc?.environments) { + throw new Error(`Invalid umbraco-compose.yaml: missing project/environments`); + } + return doc; +} + +async function writeJson(filePath: string, value: unknown): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf8"); +} + +async function writeText(filePath: string, text: string): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, text, "utf8"); +} + +async function removeMissingByExtension(dir: string, ext: string, keepFileNames: Set): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile()) continue; + if (!entry.name.endsWith(ext)) continue; + if (keepFileNames.has(entry.name)) continue; + await fs.unlink(path.join(dir, entry.name)); + } +} diff --git a/src/index.ts b/src/index.ts index cf608d7..120b882 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,14 @@ import { statusCommand } from "./commands/status.js"; import { planCommand } from "./commands/plan.js"; import { envRenameCommand } from "./commands/env-rename.js"; import { generateCommand } from "./commands/generate.js"; +import { pullCommand } from "./commands/pull.js"; await yargs(hideBin(process.argv)) .scriptName("compose") .command(initCommand) .command(validateCommand) .command(generateCommand) + .command(pullCommand) .command(planCommand) .command(applyCommand) .command(statusCommand) diff --git a/test/commands.pull.test.ts b/test/commands.pull.test.ts new file mode 100644 index 0000000..5d8d0f3 --- /dev/null +++ b/test/commands.pull.test.ts @@ -0,0 +1,210 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { pullCommand } from "../src/commands/pull.js"; +import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + + // stale files that should be removed on pull + await fs.writeFile(path.join(envDir, "type-schemas", "stale.schema.json"), "{}\n", "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "stale.js"), "export default null;\n", "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "stale.gql"), "query Stale { __typename }\n", "utf8"); + + await writeComposeState(root, createInitialState("test-project", ["dev"])); + return { root, envDir }; +} + +test("pull writes local files from API and updates state remoteAlias", async () => { + const { root, envDir } = await createFixture(); + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content", description: "Main content" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { + edges: [{ node: { ingestionFunctionAlias: "map-content", description: "Maps content" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/map-content") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { + edges: [{ node: { persistedDocumentAlias: "software-query", description: "Software query" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/software-query") { + return jsonResponse(200, { + persistedDocumentAlias: "software-query", + description: "Software query", + document: "query Software { content { items { __typename } } }" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/send-on-create") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + description: "Webhook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { "x-api-key": "abc123" } + }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + } finally { + globalThis.fetch = oldFetch; + } + + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")) as { + collections: Array<{ alias: string; description?: string }>; + }; + assert.equal(collections.collections[0]?.alias, "content"); + + const schema = JSON.parse(await fs.readFile(path.join(envDir, "type-schemas", "article.schema.json"), "utf8")) as { + alias: string; + }; + assert.equal(schema.alias, "article"); + assert.equal(await exists(path.join(envDir, "type-schemas", "stale.schema.json")), false); + + const ingestionRegistry = JSON.parse(await fs.readFile(path.join(envDir, "functions", "ingestion.json"), "utf8")) as { + functions: Array<{ alias: string; scriptFile: string }>; + }; + assert.equal(ingestionRegistry.functions[0]?.alias, "map-content"); + assert.equal(await exists(path.join(envDir, "functions", "ingestion", "stale.js")), false); + + const persistedRegistry = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")) as { + documents: Array<{ alias: string; queryFile: string }>; + }; + assert.equal(persistedRegistry.documents[0]?.alias, "software-query"); + assert.equal(await exists(path.join(envDir, "graphql", "persisted", "stale.gql")), false); + + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")) as { + webhooks: Array<{ alias: string }>; + }; + assert.equal(webhooks.webhooks[0]?.alias, "send-on-create"); + + const state = await readComposeState(root); + assert.ok(state); + assert.equal(state.environments.find((e) => e.alias === "dev")?.remoteAlias, "dev"); +}); + +test("pull fails when target environment is not present remotely", async () => { + const { root } = await createFixture(); + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "other" } }] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await assert.rejects( + () => + pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }), + /Environment "dev" was not found remotely/ + ); + } finally { + globalThis.fetch = oldFetch; + } +}); + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} From 36862fa087a52506450c91691c3c85bfd36229f9 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 15:56:15 -0500 Subject: [PATCH 13/47] Improve tests for pull command --- docs/commands.md | 1 + docs/contracts/pull-contract.json | 45 +++++ docs/testing.md | 12 +- test/commands.pull-hardening.test.ts | 240 +++++++++++++++++++++++ test/contracts.pull-contract-map.test.ts | 145 ++++++++++++++ 5 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 docs/contracts/pull-contract.json create mode 100644 test/commands.pull-hardening.test.ts create mode 100644 test/contracts.pull-contract-map.test.ts diff --git a/docs/commands.md b/docs/commands.md index da5bd0a..7f29cdd 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -93,6 +93,7 @@ Alias routing semantics: - stale generated files in managed folders are removed when no longer present remotely - updates `compose.state.json.remoteAlias` for pulled environment - does not write `compose.lock.json` + - current failure model is non-atomic: a pull failure can leave earlier pulled files updated - Side effects: writes local files. - Idempotency: repeated pulls with unchanged remote state should produce no diffs. diff --git a/docs/contracts/pull-contract.json b/docs/contracts/pull-contract.json new file mode 100644 index 0000000..f0d2004 --- /dev/null +++ b/docs/contracts/pull-contract.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "operations": { + "environmentList": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments" + }, + "collectionList": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections" + }, + "typeSchemaList": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas" + }, + "typeSchemaGet": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}" + }, + "ingestionFunctionList": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion" + }, + "ingestionFunctionGet": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}" + }, + "persistedDocumentList": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents" + }, + "persistedDocumentGet": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}" + }, + "webhookList": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks" + }, + "webhookGet": { + "method": "GET", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}" + } + } +} diff --git a/docs/testing.md b/docs/testing.md index a725471..38516c8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -60,6 +60,16 @@ - verifies `compose.state.json.remoteAlias` is updated for pulled environment - verifies pull fails when target environment is missing remotely +- `test/commands.pull-hardening.test.ts` + - verifies `pull` idempotency for unchanged remote responses + - verifies partial-failure behavior explicitly: + - state alias metadata is not advanced on failed pull + - earlier pulled files may already be updated (non-atomic pull model) + +- `test/contracts.pull-contract-map.test.ts` + - validates emitted pull read calls against `docs/contracts/pull-contract.json` + - catches drift between documented pull endpoint paths and implementation + - `test/compose.management-client.test.ts` - verifies first-call `401` triggers one auth invalidation and one retry - verifies retry does not loop infinitely when second call is also `401` @@ -143,4 +153,4 @@ These tests protect the highest-risk behavior we recently fixed: ## Next High-Value Additions - contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented - command-level tests for multi-environment apply/plan orchestration once batching is implemented -- command-level tests for `pull` idempotency and partial-failure behavior +- pull atomicity improvements (or transactional staging) with corresponding behavior-contract tests diff --git a/test/commands.pull-hardening.test.ts b/test/commands.pull-hardening.test.ts new file mode 100644 index 0000000..0f3b6fa --- /dev/null +++ b/test/commands.pull-hardening.test.ts @@ -0,0 +1,240 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import crypto from "node:crypto"; +import YAML from "yaml"; +import { pullCommand } from "../src/commands/pull.js"; +import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function createFixture(): Promise<{ root: string; envDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-hardening-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await writeComposeState(root, createInitialState("test-project", ["dev"])); + return { root, envDir }; +} + +function installSuccessfulPullFetch() { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Main content" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content", description: "Maps content" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/map-content") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "software-query", description: "Software query" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/software-query") { + return jsonResponse(200, { + persistedDocumentAlias: "software-query", + description: "Software query", + document: "query Software { content { items { __typename } } }" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/send-on-create") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + description: "Webhook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { "x-api-key": "abc123" } + }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + return () => { + globalThis.fetch = oldFetch; + }; +} + +async function snapshotDir(root: string): Promise { + const files = await listFiles(root); + const parts: Array<{ file: string; hash: string }> = []; + for (const rel of files) { + const abs = path.join(root, rel); + const text = await fs.readFile(abs, "utf8"); + parts.push({ + file: rel, + hash: sha(text) + }); + } + parts.sort((a, b) => a.file.localeCompare(b.file)); + return sha(JSON.stringify(parts)); +} + +test("pull is idempotent for unchanged remote responses", async () => { + const { root, envDir } = await createFixture(); + const restoreFetch = installSuccessfulPullFetch(); + + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + const first = await snapshotDir(envDir); + + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + const second = await snapshotDir(envDir); + + assert.equal(second, first); + } finally { + restoreFetch(); + } +}); + +test("pull partial failure keeps previous state remoteAlias unchanged and leaves partial file writes explicit", async () => { + const { root, envDir } = await createFixture(); + // First do a successful pull to set baseline and state + const restoreSuccess = installSuccessfulPullFetch(); + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + } finally { + restoreSuccess(); + } + + const stateBefore = await readComposeState(root); + assert.ok(stateBefore); + const remoteBefore = stateBefore.environments.find((e) => e.alias === "dev")?.remoteAlias; + assert.equal(remoteBefore, "dev"); + + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Changed before fail" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(500, { error: "type-schema-list-fail" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await assert.rejects( + () => + pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }), + /Failed to list type schemas \(500\)\./ + ); + } finally { + globalThis.fetch = oldFetch; + } + + const stateAfter = await readComposeState(root); + assert.ok(stateAfter); + const remoteAfter = stateAfter.environments.find((e) => e.alias === "dev")?.remoteAlias; + // state is not advanced during failed pull + assert.equal(remoteAfter, "dev"); + + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")) as { + collections: Array<{ alias: string; description?: string }>; + }; + // current behavior is partial write before failure; this test locks that behavior explicitly + assert.equal(collections.collections[0]?.description, "Changed before fail"); +}); + +async function listFiles(root: string): Promise { + const out: string[] = []; + async function walk(dir: string) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const abs = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(abs); + } else if (entry.isFile()) { + out.push(path.relative(root, abs)); + } + } + } + await walk(root); + return out; +} + +function sha(s: string): string { + return crypto.createHash("sha256").update(s).digest("hex"); +} diff --git a/test/contracts.pull-contract-map.test.ts b/test/contracts.pull-contract-map.test.ts new file mode 100644 index 0000000..a664a4f --- /dev/null +++ b/test/contracts.pull-contract-map.test.ts @@ -0,0 +1,145 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import YAML from "yaml"; +import { pullCommand } from "../src/commands/pull.js"; +import { createInitialState, writeComposeState } from "../src/compose/state.js"; + +type ContractMap = { + version: number; + operations: Record< + string, + { + method: string; + pathTemplate: string; + } + >; +}; + +type CapturedCall = { + method: string; + pathname: string; +}; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function readContractMap(): Promise { + const filePath = path.resolve(process.cwd(), "docs/contracts/pull-contract.json"); + const text = await fs.readFile(filePath, "utf8"); + return JSON.parse(text) as ContractMap; +} + +function resolvePath(template: string, params: Record): string { + let out = template; + for (const [k, v] of Object.entries(params)) { + out = out.replaceAll(`{${k}}`, v); + } + return out; +} + +function assertContractCall( + contracts: ContractMap, + operation: keyof ContractMap["operations"], + calls: CapturedCall[], + params: Record +): void { + const contract = contracts.operations[operation]; + assert.ok(contract, `Missing contract entry for ${operation}`); + const expectedPath = resolvePath(contract.pathTemplate, params); + const call = calls.find((c) => c.method === contract.method && c.pathname === expectedPath); + assert.ok(call, `Missing call for ${operation}: ${contract.method} ${expectedPath}`); +} + +async function createFixture(): Promise<{ root: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-contract-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await writeComposeState(root, createInitialState("test-project", ["dev"])); + return { root }; +} + +test("pull emitted read calls match pull contract map", async () => { + const contracts = await readContractMap(); + const { root } = await createFixture(); + const calls: CapturedCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + calls.push({ method, pathname: u.pathname }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") return jsonResponse(200, { edges: [] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") return jsonResponse(200, { typeSchemaAlias: "article", schema: {}, description: null }); + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/fn-a") return jsonResponse(200, { ingestionFunctionAlias: "fn-a", script: "export default () => null;" }); + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/doc-a") return jsonResponse(200, { persistedDocumentAlias: "doc-a", document: "query { __typename }" }); + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-a" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/hook-a") return jsonResponse(200, { webhookAlias: "hook-a", url: "https://example.com/hook", eventTypes: [], collectionAliases: [], typeSchemaAliases: [], customHeaders: {} }); + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + } finally { + globalThis.fetch = oldFetch; + } + + const baseParams = { + projectAlias: "test-project", + environmentAlias: "dev", + typeSchemaAlias: "article", + ingestionFunctionAlias: "fn-a", + persistedDocumentAlias: "doc-a", + webhookAlias: "hook-a" + }; + + assertContractCall(contracts, "environmentList", calls, baseParams); + assertContractCall(contracts, "collectionList", calls, baseParams); + assertContractCall(contracts, "typeSchemaList", calls, baseParams); + assertContractCall(contracts, "typeSchemaGet", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionList", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionGet", calls, baseParams); + assertContractCall(contracts, "persistedDocumentList", calls, baseParams); + assertContractCall(contracts, "persistedDocumentGet", calls, baseParams); + assertContractCall(contracts, "webhookList", calls, baseParams); + assertContractCall(contracts, "webhookGet", calls, baseParams); +}); From 784787173d0b5ea48e05a0a070480be0cb90fd1b Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 16:05:18 -0500 Subject: [PATCH 14/47] Add MVP of clone command --- docs/commands.md | 10 +++ docs/testing.md | 5 ++ src/commands/clone.ts | 128 ++++++++++++++++++++++++++++++++++++ src/commands/pull.ts | 94 +++++++++++++------------- src/index.ts | 2 + test/commands.clone.test.ts | 98 +++++++++++++++++++++++++++ 6 files changed, 292 insertions(+), 45 deletions(-) create mode 100644 src/commands/clone.ts create mode 100644 test/commands.clone.test.ts diff --git a/docs/commands.md b/docs/commands.md index 7f29cdd..1e93027 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -6,6 +6,7 @@ This document defines the intended command behavior for the core workflow: - `compose init` - `compose generate ` - `compose validate` +- `compose clone` - `compose pull` - `compose status` - `compose plan` @@ -97,6 +98,15 @@ Alias routing semantics: - Side effects: writes local files. - Idempotency: repeated pulls with unchanged remote state should produce no diffs. +### `compose clone` +- Status: implemented (MVP) +- Responsibility: + - bootstrap a local compose directory from remote project/env state + - scaffold minimal `umbraco-compose.yaml` + - execute `pull` for the selected environment +- Side effects: writes local project files and pulled entity files. +- Idempotency: repeated clone to same non-empty directory requires `--force`. + ### `compose status` - Status: implemented - Responsibility: diff --git a/docs/testing.md b/docs/testing.md index 38516c8..9078a1d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -70,6 +70,10 @@ - validates emitted pull read calls against `docs/contracts/pull-contract.json` - catches drift between documented pull endpoint paths and implementation +- `test/commands.clone.test.ts` + - verifies clone bootstraps local config and delegates to pull for local file materialization + - verifies clone rejects non-empty target directories unless `--force` + - `test/compose.management-client.test.ts` - verifies first-call `401` triggers one auth invalidation and one retry - verifies retry does not loop infinitely when second call is also `401` @@ -154,3 +158,4 @@ These tests protect the highest-risk behavior we recently fixed: - contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented - command-level tests for multi-environment apply/plan orchestration once batching is implemented - pull atomicity improvements (or transactional staging) with corresponding behavior-contract tests +- clone idempotency and `--force` behavior tests for pre-existing compose files diff --git a/src/commands/clone.ts b/src/commands/clone.ts new file mode 100644 index 0000000..9af2504 --- /dev/null +++ b/src/commands/clone.ts @@ -0,0 +1,128 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { CommandModule } from "yargs"; +import YAML from "yaml"; +import { runPull } from "./pull.js"; + +type Args = { + dir: string; + project: string; + env: string; + baseUrl: string; + clientId?: string; + clientSecret?: string; + scope?: string; + audience?: string; + force: boolean; +}; + +export const cloneCommand: CommandModule<{}, Args> = { + command: "clone", + describe: "Bootstrap a local Compose project from remote state", + builder: { + dir: { + type: "string", + default: "./compose", + describe: "Target local directory for the cloned project" + }, + project: { + type: "string", + demandOption: true, + describe: "Remote project alias to clone" + }, + env: { + type: "string", + default: "dev", + describe: "Environment alias to clone" + }, + baseUrl: { + type: "string", + default: "https://management.umbracocompose.com", + describe: "Compose Management API base URL" + }, + clientId: { + type: "string", + describe: "OAuth client id (or set COMPOSE_MGMT_CLIENT_ID)" + }, + clientSecret: { + type: "string", + describe: "OAuth client secret (or set COMPOSE_MGMT_CLIENT_SECRET)" + }, + scope: { + type: "string", + describe: "Optional OAuth scope" + }, + audience: { + type: "string", + describe: "Optional OAuth audience" + }, + force: { + type: "boolean", + default: false, + describe: "Allow cloning into a non-empty directory" + } + }, + handler: async (args) => { + const rootDir = path.resolve(process.cwd(), args.dir); + await ensureTargetDir(rootDir, args.force); + await scaffoldCloneRoot(rootDir, args.project, args.env, args.baseUrl); + + await runPull({ + dir: rootDir, + env: args.env, + ...(args.clientId ? { clientId: args.clientId } : {}), + ...(args.clientSecret ? { clientSecret: args.clientSecret } : {}), + ...(args.scope ? { scope: args.scope } : {}), + ...(args.audience ? { audience: args.audience } : {}) + }); + + console.log(`Cloned project "${args.project}" env "${args.env}" into ${rootDir}`); + } +}; + +async function ensureTargetDir(rootDir: string, force: boolean): Promise { + const exists = await pathExists(rootDir); + if (!exists) { + await fs.mkdir(rootDir, { recursive: true }); + return; + } + + const entries = await fs.readdir(rootDir); + if (entries.length > 0 && !force) { + throw new Error( + `Clone target directory is not empty: ${rootDir}. ` + + `Use --force to proceed anyway.` + ); + } +} + +async function scaffoldCloneRoot( + rootDir: string, + project: string, + env: string, + baseUrl: string +): Promise { + const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); + const doc = { + version: 1, + project, + environments: { + [env]: { + dir: `./env/${env}`, + description: `${env} Environment`, + managementBaseUrl: baseUrl + } + } + }; + await fs.mkdir(path.join(rootDir, "env", env), { recursive: true }); + await fs.writeFile(composeYamlPath, YAML.stringify(doc), "utf8"); +} + +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} diff --git a/src/commands/pull.ts b/src/commands/pull.ts index 7501268..85ce481 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -74,58 +74,62 @@ export const pullCommand: CommandModule<{}, Args> = { } }, handler: async (args) => { - const rootDir = path.resolve(process.cwd(), args.dir); - const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); - const cfg = await readComposeConfig(composeYamlPath); - const envCfg = cfg.environments[args.env]; - if (!envCfg) { - throw new Error(`Environment "${args.env}" not found in ${composeYamlPath}`); - } + await runPull(args); + } +}; - const clientId = args.clientId ?? process.env.COMPOSE_MGMT_CLIENT_ID; - const clientSecret = args.clientSecret ?? process.env.COMPOSE_MGMT_CLIENT_SECRET; - if (!clientId || !clientSecret) { - throw new Error( - "Missing OAuth credentials. Provide --clientId/--clientSecret or set COMPOSE_MGMT_CLIENT_ID and COMPOSE_MGMT_CLIENT_SECRET." - ); - } +export async function runPull(args: Args): Promise { + const rootDir = path.resolve(process.cwd(), args.dir); + const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); + const cfg = await readComposeConfig(composeYamlPath); + const envCfg = cfg.environments[args.env]; + if (!envCfg) { + throw new Error(`Environment "${args.env}" not found in ${composeYamlPath}`); + } - const envDir = path.resolve(rootDir, envCfg.dir); - await fs.mkdir(envDir, { recursive: true }); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - - const client = new ManagementClient({ - baseUrl: envCfg.managementBaseUrl, - auth: composeManagementOAuth({ - clientId, - clientSecret, - baseUrl: envCfg.managementBaseUrl - }) - }); + const clientId = args.clientId ?? process.env.COMPOSE_MGMT_CLIENT_ID; + const clientSecret = args.clientSecret ?? process.env.COMPOSE_MGMT_CLIENT_SECRET; + if (!clientId || !clientSecret) { + throw new Error( + "Missing OAuth credentials. Provide --clientId/--clientSecret or set COMPOSE_MGMT_CLIENT_ID and COMPOSE_MGMT_CLIENT_SECRET." + ); + } - const remoteEnvExists = await ensureRemoteEnvironmentExists(client, cfg.project, args.env); - if (!remoteEnvExists) { - throw new Error(`Environment "${args.env}" was not found remotely for project "${cfg.project}".`); - } + const envDir = path.resolve(rootDir, envCfg.dir); + await fs.mkdir(envDir, { recursive: true }); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await pullCollections(client, cfg.project, args.env, envDir); - await pullTypeSchemas(client, cfg.project, args.env, envDir); - await pullIngestionFunctions(client, cfg.project, args.env, envDir); - await pullPersistedDocs(client, cfg.project, args.env, envDir); - await pullWebhooks(client, cfg.project, args.env, envDir); + const client = new ManagementClient({ + baseUrl: envCfg.managementBaseUrl, + auth: composeManagementOAuth({ + clientId, + clientSecret, + baseUrl: envCfg.managementBaseUrl + }) + }); - const state = ensureStateForAliases(await readComposeState(rootDir), cfg.project, Object.keys(cfg.environments)).state; - const envState = findEnvironmentByAlias(state, args.env); - if (envState) { - markEnvironmentRemoteAlias(state, envState.id, args.env); - } - await writeComposeState(rootDir, state); + const remoteEnvExists = await ensureRemoteEnvironmentExists(client, cfg.project, args.env); + if (!remoteEnvExists) { + throw new Error(`Environment "${args.env}" was not found remotely for project "${cfg.project}".`); + } - console.log(`Pulled remote state for env "${args.env}" into ${envDir}`); + await pullCollections(client, cfg.project, args.env, envDir); + await pullTypeSchemas(client, cfg.project, args.env, envDir); + await pullIngestionFunctions(client, cfg.project, args.env, envDir); + await pullPersistedDocs(client, cfg.project, args.env, envDir); + await pullWebhooks(client, cfg.project, args.env, envDir); + + const state = ensureStateForAliases(await readComposeState(rootDir), cfg.project, Object.keys(cfg.environments)).state; + const envState = findEnvironmentByAlias(state, args.env); + if (envState) { + markEnvironmentRemoteAlias(state, envState.id, args.env); } -}; + await writeComposeState(rootDir, state); + + console.log(`Pulled remote state for env "${args.env}" into ${envDir}`); +} async function pullCollections(client: ManagementClient, project: string, env: string, envDir: string): Promise { const res = await client.get(`${envBase(project, env)}/collections`); diff --git a/src/index.ts b/src/index.ts index 120b882..9be501e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,12 +10,14 @@ import { planCommand } from "./commands/plan.js"; import { envRenameCommand } from "./commands/env-rename.js"; import { generateCommand } from "./commands/generate.js"; import { pullCommand } from "./commands/pull.js"; +import { cloneCommand } from "./commands/clone.js"; await yargs(hideBin(process.argv)) .scriptName("compose") .command(initCommand) .command(validateCommand) .command(generateCommand) + .command(cloneCommand) .command(pullCommand) .command(planCommand) .command(applyCommand) diff --git a/test/commands.clone.test.ts b/test/commands.clone.test.ts new file mode 100644 index 0000000..7e88b94 --- /dev/null +++ b/test/commands.clone.test.ts @@ -0,0 +1,98 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { cloneCommand } from "../src/commands/clone.js"; +import { readComposeState } from "../src/compose/state.js"; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +test("clone scaffolds project and pulls remote env state", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-clone-")); + const targetDir = path.join(root, "cloned-compose"); + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/my-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/collections") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Main content" } }] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await cloneCommand.handler({ + dir: targetDir, + project: "my-project", + env: "dev", + baseUrl: "https://management.example", + clientId: "client", + clientSecret: "secret", + force: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + const composeYamlPath = path.join(targetDir, "umbraco-compose.yaml"); + const cfg = YAML.parse(await fs.readFile(composeYamlPath, "utf8")) as { + project: string; + environments: Record; + }; + assert.equal(cfg.project, "my-project"); + assert.equal(cfg.environments.dev?.dir, "./env/dev"); + + const collectionsPath = path.join(targetDir, "env", "dev", "collections.json"); + const collections = JSON.parse(await fs.readFile(collectionsPath, "utf8")) as { + collections: Array<{ alias: string }>; + }; + assert.equal(collections.collections[0]?.alias, "content"); + + const state = await readComposeState(targetDir); + assert.ok(state); + assert.equal(state.environments.find((e) => e.alias === "dev")?.remoteAlias, "dev"); +}); + +test("clone fails when target directory is non-empty without --force", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-clone-nonempty-")); + await fs.writeFile(path.join(root, "keep.txt"), "keep\n", "utf8"); + + await assert.rejects( + () => + cloneCommand.handler({ + dir: root, + project: "my-project", + env: "dev", + baseUrl: "https://management.example", + clientId: "client", + clientSecret: "secret", + force: false + }), + /target directory is not empty/ + ); +}); From e7eeff9f0d0cf09d1eeadf64d119c24a4a1d31c0 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 16:15:11 -0500 Subject: [PATCH 15/47] Improve pull tests to prevent partial mutations on failed pulls --- docs/commands.md | 2 +- docs/testing.md | 4 +- src/commands/pull.ts | 189 +++++++++++++++++++-------- test/commands.pull-hardening.test.ts | 5 +- 4 files changed, 137 insertions(+), 63 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 1e93027..5dd448c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -94,7 +94,7 @@ Alias routing semantics: - stale generated files in managed folders are removed when no longer present remotely - updates `compose.state.json.remoteAlias` for pulled environment - does not write `compose.lock.json` - - current failure model is non-atomic: a pull failure can leave earlier pulled files updated + - uses staged commits for managed files; fetch failures do not partially mutate local managed artifacts - Side effects: writes local files. - Idempotency: repeated pulls with unchanged remote state should produce no diffs. diff --git a/docs/testing.md b/docs/testing.md index 9078a1d..c7e696e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -64,7 +64,7 @@ - verifies `pull` idempotency for unchanged remote responses - verifies partial-failure behavior explicitly: - state alias metadata is not advanced on failed pull - - earlier pulled files may already be updated (non-atomic pull model) + - local managed artifacts are not partially mutated before failure - `test/contracts.pull-contract-map.test.ts` - validates emitted pull read calls against `docs/contracts/pull-contract.json` @@ -157,5 +157,5 @@ These tests protect the highest-risk behavior we recently fixed: ## Next High-Value Additions - contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented - command-level tests for multi-environment apply/plan orchestration once batching is implemented -- pull atomicity improvements (or transactional staging) with corresponding behavior-contract tests +- staged pull rollback edge-case tests (filesystem errors during commit) - clone idempotency and `--force` behavior tests for pre-existing compose files diff --git a/src/commands/pull.ts b/src/commands/pull.ts index 85ce481..437fe67 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -42,6 +42,20 @@ type WebhooksFile = { }>; }; +type PulledSnapshot = { + collections: CollectionsFile; + typeSchemas: Array<{ alias: string; description?: string | null; schema: unknown }>; + ingestion: { + registry: IngestionRegistry; + scripts: Array<{ alias: string; script: string }>; + }; + persisted: { + registry: PersistedRegistry; + documents: Array<{ alias: string; document: string }>; + }; + webhooks: WebhooksFile; +}; + export const pullCommand: CommandModule<{}, Args> = { command: "pull", describe: "Pull remote Compose environment state into local IaC files", @@ -97,9 +111,6 @@ export async function runPull(args: Args): Promise { const envDir = path.resolve(rootDir, envCfg.dir); await fs.mkdir(envDir, { recursive: true }); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); const client = new ManagementClient({ baseUrl: envCfg.managementBaseUrl, @@ -115,11 +126,15 @@ export async function runPull(args: Args): Promise { throw new Error(`Environment "${args.env}" was not found remotely for project "${cfg.project}".`); } - await pullCollections(client, cfg.project, args.env, envDir); - await pullTypeSchemas(client, cfg.project, args.env, envDir); - await pullIngestionFunctions(client, cfg.project, args.env, envDir); - await pullPersistedDocs(client, cfg.project, args.env, envDir); - await pullWebhooks(client, cfg.project, args.env, envDir); + const snapshot = await fetchSnapshot(client, cfg.project, args.env); + const stagingDir = path.join(envDir, `.__pull_staging__${Date.now()}_${Math.random().toString(36).slice(2)}`); + + try { + await materializeSnapshotToStaging(stagingDir, snapshot); + await commitStagedSnapshot(envDir, stagingDir); + } finally { + await fs.rm(stagingDir, { recursive: true, force: true }); + } const state = ensureStateForAliases(await readComposeState(rootDir), cfg.project, Object.keys(cfg.environments)).state; const envState = findEnvironmentByAlias(state, args.env); @@ -131,13 +146,29 @@ export async function runPull(args: Args): Promise { console.log(`Pulled remote state for env "${args.env}" into ${envDir}`); } -async function pullCollections(client: ManagementClient, project: string, env: string, envDir: string): Promise { +async function fetchSnapshot(client: ManagementClient, project: string, env: string): Promise { + const collections = await pullCollections(client, project, env); + const typeSchemas = await pullTypeSchemas(client, project, env); + const ingestion = await pullIngestionFunctions(client, project, env); + const persisted = await pullPersistedDocs(client, project, env); + const webhooks = await pullWebhooks(client, project, env); + + return { + collections, + typeSchemas, + ingestion, + persisted, + webhooks + }; +} + +async function pullCollections(client: ManagementClient, project: string, env: string): Promise { const res = await client.get(`${envBase(project, env)}/collections`); if (res.status >= 400) { throw new Error(`Failed to list collections (${res.status}).`); } const items = asNodes(res.data, ["collectionAlias", "alias"]); - const collections: CollectionsFile = { + return { collections: items .map((c) => ({ alias: String(c.collectionAlias ?? c.alias), @@ -145,16 +176,20 @@ async function pullCollections(client: ManagementClient, project: string, env: s })) .sort((a, b) => a.alias.localeCompare(b.alias)) }; - await writeJson(path.join(envDir, "collections.json"), collections); } -async function pullTypeSchemas(client: ManagementClient, project: string, env: string, envDir: string): Promise { +async function pullTypeSchemas( + client: ManagementClient, + project: string, + env: string +): Promise> { const list = await client.get(`${envBase(project, env)}/type-schemas`); if (list.status >= 400) { throw new Error(`Failed to list type schemas (${list.status}).`); } + const items = asNodes(list.data, ["typeSchemaAlias", "alias"]); - const keep = new Set(); + const out: Array<{ alias: string; description?: string | null; schema: unknown }> = []; for (const item of items) { const alias = String(item.typeSchemaAlias ?? item.alias); @@ -162,29 +197,30 @@ async function pullTypeSchemas(client: ManagementClient, project: string, env: s if (get.status >= 400) { throw new Error(`Failed to get type schema "${alias}" (${get.status}).`); } - const schema = get.data?.schema ?? get.data?.typeSchema?.schema; - const description = get.data?.description ?? get.data?.typeSchema?.description ?? null; - const out = { + out.push({ alias, - description, - schema - }; - const file = path.join(envDir, "type-schemas", `${alias}.schema.json`); - await writeJson(file, out); - keep.add(path.basename(file)); + description: get.data?.description ?? get.data?.typeSchema?.description ?? null, + schema: get.data?.schema ?? get.data?.typeSchema?.schema + }); } - await removeMissingByExtension(path.join(envDir, "type-schemas"), ".schema.json", keep); + out.sort((a, b) => a.alias.localeCompare(b.alias)); + return out; } -async function pullIngestionFunctions(client: ManagementClient, project: string, env: string, envDir: string): Promise { +async function pullIngestionFunctions( + client: ManagementClient, + project: string, + env: string +): Promise<{ registry: IngestionRegistry; scripts: Array<{ alias: string; script: string }> }> { const list = await client.get(`${envBase(project, env)}/functions/ingestion`); if (list.status >= 400) { throw new Error(`Failed to list ingestion functions (${list.status}).`); } + const items = asNodes(list.data, ["ingestionFunctionAlias", "alias"]); const registry: IngestionRegistry = { functions: [] }; - const keep = new Set(); + const scripts: Array<{ alias: string; script: string }> = []; for (const item of items) { const alias = String(item.ingestionFunctionAlias ?? item.alias); @@ -192,32 +228,36 @@ async function pullIngestionFunctions(client: ManagementClient, project: string, if (get.status >= 400) { throw new Error(`Failed to get ingestion function "${alias}" (${get.status}).`); } - const script = get.data?.script ?? get.data?.ingestionFunction?.script ?? ""; + const description = get.data?.description ?? get.data?.ingestionFunction?.description ?? null; - const scriptRel = `functions/ingestion/${alias}.js`; - const scriptAbs = path.join(envDir, scriptRel); - await writeText(scriptAbs, `${script}\n`); - keep.add(path.basename(scriptAbs)); + const script = String(get.data?.script ?? get.data?.ingestionFunction?.script ?? ""); + registry.functions.push({ alias, description, - scriptFile: scriptRel + scriptFile: `functions/ingestion/${alias}.js` }); + scripts.push({ alias, script }); } registry.functions.sort((a, b) => a.alias.localeCompare(b.alias)); - await writeJson(path.join(envDir, "functions", "ingestion.json"), registry); - await removeMissingByExtension(path.join(envDir, "functions", "ingestion"), ".js", keep); + scripts.sort((a, b) => a.alias.localeCompare(b.alias)); + return { registry, scripts }; } -async function pullPersistedDocs(client: ManagementClient, project: string, env: string, envDir: string): Promise { +async function pullPersistedDocs( + client: ManagementClient, + project: string, + env: string +): Promise<{ registry: PersistedRegistry; documents: Array<{ alias: string; document: string }> }> { const list = await client.get(`${envBase(project, env)}/graphql/persisted-documents`); if (list.status >= 400) { throw new Error(`Failed to list persisted docs (${list.status}).`); } + const items = asNodes(list.data, ["persistedDocumentAlias", "alias"]); const registry: PersistedRegistry = { documents: [] }; - const keep = new Set(); + const documents: Array<{ alias: string; document: string }> = []; for (const item of items) { const alias = String(item.persistedDocumentAlias ?? item.alias); @@ -227,25 +267,24 @@ async function pullPersistedDocs(client: ManagementClient, project: string, env: if (get.status >= 400) { throw new Error(`Failed to get persisted doc "${alias}" (${get.status}).`); } - const document = get.data?.document ?? get.data?.query ?? ""; - const description = get.data?.description ?? null; - const queryRel = `graphql/persisted/${alias}.gql`; - const queryAbs = path.join(envDir, queryRel); - await writeText(queryAbs, `${document}\n`); - keep.add(path.basename(queryAbs)); + registry.documents.push({ alias, - description, - queryFile: queryRel + description: get.data?.description ?? null, + queryFile: `graphql/persisted/${alias}.gql` + }); + documents.push({ + alias, + document: String(get.data?.document ?? get.data?.query ?? "") }); } registry.documents.sort((a, b) => a.alias.localeCompare(b.alias)); - await writeJson(path.join(envDir, "graphql", "persisted.json"), registry); - await removeMissingByExtension(path.join(envDir, "graphql", "persisted"), ".gql", keep); + documents.sort((a, b) => a.alias.localeCompare(b.alias)); + return { registry, documents }; } -async function pullWebhooks(client: ManagementClient, project: string, env: string, envDir: string): Promise { +async function pullWebhooks(client: ManagementClient, project: string, env: string): Promise { const list = await client.get(`${envBase(project, env)}/webhooks`); if (list.status >= 400) { throw new Error(`Failed to list webhooks (${list.status}).`); @@ -272,7 +311,53 @@ async function pullWebhooks(client: ManagementClient, project: string, env: stri } webhooks.webhooks.sort((a, b) => a.alias.localeCompare(b.alias)); - await writeJson(path.join(envDir, "webhooks.json"), webhooks); + return webhooks; +} + +async function materializeSnapshotToStaging(stagingDir: string, snapshot: PulledSnapshot): Promise { + await fs.mkdir(path.join(stagingDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(stagingDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(stagingDir, "graphql", "persisted"), { recursive: true }); + + await writeJson(path.join(stagingDir, "collections.json"), snapshot.collections); + await writeJson(path.join(stagingDir, "webhooks.json"), snapshot.webhooks); + + for (const ts of snapshot.typeSchemas) { + await writeJson(path.join(stagingDir, "type-schemas", `${ts.alias}.schema.json`), ts); + } + + await writeJson(path.join(stagingDir, "functions", "ingestion.json"), snapshot.ingestion.registry); + for (const script of snapshot.ingestion.scripts) { + await writeText(path.join(stagingDir, "functions", "ingestion", `${script.alias}.js`), `${script.script}\n`); + } + + await writeJson(path.join(stagingDir, "graphql", "persisted.json"), snapshot.persisted.registry); + for (const d of snapshot.persisted.documents) { + await writeText(path.join(stagingDir, "graphql", "persisted", `${d.alias}.gql`), `${d.document}\n`); + } +} + +async function commitStagedSnapshot(envDir: string, stagingDir: string): Promise { + await replaceDir(path.join(stagingDir, "type-schemas"), path.join(envDir, "type-schemas")); + await replaceDir(path.join(stagingDir, "functions", "ingestion"), path.join(envDir, "functions", "ingestion")); + await replaceDir(path.join(stagingDir, "graphql", "persisted"), path.join(envDir, "graphql", "persisted")); + + await replaceFile(path.join(stagingDir, "collections.json"), path.join(envDir, "collections.json")); + await replaceFile(path.join(stagingDir, "webhooks.json"), path.join(envDir, "webhooks.json")); + await replaceFile(path.join(stagingDir, "functions", "ingestion.json"), path.join(envDir, "functions", "ingestion.json")); + await replaceFile(path.join(stagingDir, "graphql", "persisted.json"), path.join(envDir, "graphql", "persisted.json")); +} + +async function replaceDir(stagedDir: string, targetDir: string): Promise { + await fs.mkdir(path.dirname(targetDir), { recursive: true }); + await fs.rm(targetDir, { recursive: true, force: true }); + await fs.rename(stagedDir, targetDir); +} + +async function replaceFile(stagedFile: string, targetFile: string): Promise { + await fs.mkdir(path.dirname(targetFile), { recursive: true }); + await fs.rm(targetFile, { force: true }); + await fs.rename(stagedFile, targetFile); } async function ensureRemoteEnvironmentExists(client: ManagementClient, project: string, env: string): Promise { @@ -315,13 +400,3 @@ async function writeText(filePath: string, text: string): Promise { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, text, "utf8"); } - -async function removeMissingByExtension(dir: string, ext: string, keepFileNames: Set): Promise { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isFile()) continue; - if (!entry.name.endsWith(ext)) continue; - if (keepFileNames.has(entry.name)) continue; - await fs.unlink(path.join(dir, entry.name)); - } -} diff --git a/test/commands.pull-hardening.test.ts b/test/commands.pull-hardening.test.ts index 0f3b6fa..ab3058f 100644 --- a/test/commands.pull-hardening.test.ts +++ b/test/commands.pull-hardening.test.ts @@ -155,7 +155,7 @@ test("pull is idempotent for unchanged remote responses", async () => { } }); -test("pull partial failure keeps previous state remoteAlias unchanged and leaves partial file writes explicit", async () => { +test("pull partial failure keeps previous state remoteAlias unchanged and does not partially mutate local files", async () => { const { root, envDir } = await createFixture(); // First do a successful pull to set baseline and state const restoreSuccess = installSuccessfulPullFetch(); @@ -214,8 +214,7 @@ test("pull partial failure keeps previous state remoteAlias unchanged and leaves const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")) as { collections: Array<{ alias: string; description?: string }>; }; - // current behavior is partial write before failure; this test locks that behavior explicitly - assert.equal(collections.collections[0]?.description, "Changed before fail"); + assert.equal(collections.collections[0]?.description, "Main content"); }); async function listFiles(root: string): Promise { From af9681ad3da9ec88601a10c5f8189012216afbce Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 16:24:15 -0500 Subject: [PATCH 16/47] Support update-by-diff for ingestion function script and description --- docs/commands.md | 1 + docs/contracts/apply-contract.json | 10 +++ docs/testing.md | 2 + src/compose/apply.ts | 80 ++++++++++++++++++- test/contracts.apply-contract-map.test.ts | 81 +++++++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 92 ++++++++++++++++++++++ 6 files changed, 264 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 5dd448c..167c4d7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -133,6 +133,7 @@ Alias routing semantics: - update `compose.lock.json` and `compose.state.json.remoteAlias` only when apply completes with zero warnings - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output + - ingestion functions support update-by-diff for script and description (via update commands) - if any warning is emitted, lock/state writes are skipped for safety - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index f10750c..f0b4f65 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -31,6 +31,16 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion", "requiredBodyKeys": ["ingestionFunctionAlias", "description", "script"] }, + "ingestionFunctionUpdateScript": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}/commands/update-script", + "requiredBodyKeys": ["script"] + }, + "ingestionFunctionUpdateDescription": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}/commands/update-description", + "requiredBodyKeys": ["newDescription"] + }, "persistedDocumentCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents", diff --git a/docs/testing.md b/docs/testing.md index c7e696e..5f67dee 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -99,6 +99,7 @@ - persisted document create payload uses `document` field - persisted document create emits string `description` even when local description is omitted - ingestion-function create path + payload (`ingestionFunctionAlias`, `description`, `script`) + - ingestion-function update commands (`update-script`, `update-description`) payload shape - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) - type-schema update-schema endpoint receives raw schema JSON body - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) @@ -106,6 +107,7 @@ - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation + - includes ingestion update contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: diff --git a/src/compose/apply.ts b/src/compose/apply.ts index b6b6cf4..bad453b 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -418,6 +418,16 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti // List existing const listPath = `${envBase(opts)}/functions/ingestion`; const existingRes = await client.get(listPath); + if (existingRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias: "*", + details: `Failed to list ingestion functions (${existingRes.status}).` + }); + return out; + } const existingAliases = new Map(); const items = @@ -459,8 +469,74 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti } } } else { - // We don’t know remote script field shape without a GET-by-alias; MVP: always noop. - out.push({ kind: "noop", env: opEnv, resource: "ingestion-function", alias }); + const getPath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + + const remoteScript = String(existing.data?.script ?? existing.data?.ingestionFunction?.script ?? ""); + const remoteDescription = String( + existing.data?.description ?? existing.data?.ingestionFunction?.description ?? "" + ); + const desiredDescription = fn.description ?? ""; + + const scriptChanged = remoteScript !== script; + const descriptionChanged = remoteDescription !== desiredDescription; + + if (!scriptChanged && !descriptionChanged) { + out.push({ kind: "noop", env: opEnv, resource: "ingestion-function", alias }); + continue; + } + + out.push({ + kind: "update", + env: opEnv, + resource: "ingestion-function", + alias, + details: `${scriptChanged ? "script " : ""}${descriptionChanged ? "description" : ""}`.trim() + }); + + if (opts.planOnly) continue; + + if (scriptChanged) { + const updateScriptPath = + `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}/commands/update-script`; + const updateScriptRes = await client.put(updateScriptPath, { script }); + if (updateScriptRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Update script failed (${updateScriptRes.status}).` + }); + } + } + + if (descriptionChanged) { + const updateDescPath = + `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}/commands/update-description`; + const updateDescRes = await client.put(updateDescPath, { + newDescription: desiredDescription + }); + if (updateDescRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Update description failed (${updateDescRes.status}).` + }); + } + } } } diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index ddbcb99..e9f7ad8 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -354,3 +354,84 @@ test("environment create and rename calls match contract map", async () => { environmentAlias: "old-dev" }); }); + +test("ingestion update calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-ingestion-update-"); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "functions", "ingestion.json"), + JSON.stringify( + { + functions: [ + { + alias: "fn-a", + description: "New desc", + scriptFile: "functions/ingestion/fn-a.js" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "fn-a.js"), "export default (x) => x;", "utf8"); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a") { + return jsonResponse(200, { ingestionFunctionAlias: "fn-a", description: "Old desc", script: "export default () => null;" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a/commands/update-script" && method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "ingestionFunctionUpdateScript", calls, { + projectAlias: "proj", + environmentAlias: "dev", + ingestionFunctionAlias: "fn-a" + }); + assertContractCall(contracts, "ingestionFunctionUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + ingestionFunctionAlias: "fn-a" + }); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index 9ee8160..c139543 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -323,6 +323,98 @@ test("ingestion-function create uses expected endpoint and payload shape", async assert.equal(typeof body.script, "string"); }); +test("ingestion-function update uses expected update-script and update-description commands", async () => { + const envDir = await mkEnvDir("compose-contract-ingestion-update-"); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "functions", "ingestion.json"), + JSON.stringify( + { + functions: [ + { + alias: "map-content", + description: "New description", + scriptFile: "functions/ingestion/map-content.js" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion", "map-content.js"), + "export default function(input){ return input; }", + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content", + description: "Old description", + script: "export default () => null;" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-script" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const updateScriptCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-script") + ); + assert.ok(updateScriptCall); + const updateScriptBody = JSON.parse(updateScriptCall.bodyText ?? "{}") as Record; + assert.equal(typeof updateScriptBody.script, "string"); + + const updateDescriptionCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes( + "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-description" + ) + ); + assert.ok(updateDescriptionCall); + const updateDescriptionBody = JSON.parse(updateDescriptionCall.bodyText ?? "{}") as Record; + assert.equal(updateDescriptionBody.newDescription, "New description"); +}); + test("webhook create uses expected endpoint and payload shape", async () => { const envDir = await mkEnvDir("compose-contract-webhook-create-"); await fs.writeFile( From 2a99a7d6ccc59a031827f9f1914a796b1a46837c Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Wed, 11 Feb 2026 16:37:37 -0500 Subject: [PATCH 17/47] Support update-by-diff for persisted docs document and description --- docs/commands.md | 1 + docs/contracts/apply-contract.json | 10 +++ docs/testing.md | 5 +- src/compose/apply.ts | 85 ++++++++++++++++++- test/contracts.apply-contract-map.test.ts | 94 ++++++++++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 88 ++++++++++++++++++++ 6 files changed, 279 insertions(+), 4 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 167c4d7..7c8f43c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -134,6 +134,7 @@ Alias routing semantics: - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - ingestion functions support update-by-diff for script and description (via update commands) + - persisted documents support update-by-diff for document and description (via update commands) - if any warning is emitted, lock/state writes are skipped for safety - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index f0b4f65..b9b203c 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -46,6 +46,16 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents", "requiredBodyKeys": ["persistedDocumentAlias", "description", "document"] }, + "persistedDocumentUpdateDescription": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}/commands/update-description", + "requiredBodyKeys": ["newDescription"] + }, + "persistedDocumentUpdateDocument": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}/commands/update-document", + "requiredBodyKeys": ["newDocument"] + }, "webhookCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks", diff --git a/docs/testing.md b/docs/testing.md index 5f67dee..0425325 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -98,6 +98,7 @@ - environment rename path + `newEnvironmentAlias` payload - persisted document create payload uses `document` field - persisted document create emits string `description` even when local description is omitted + - persisted-document update commands (`update-document`, `update-description`) payload shape - ingestion-function create path + payload (`ingestionFunctionAlias`, `description`, `script`) - ingestion-function update commands (`update-script`, `update-description`) payload shape - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) @@ -107,7 +108,7 @@ - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes ingestion update contract-map assertions + - includes ingestion and persisted-document update contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: @@ -157,7 +158,7 @@ These tests protect the highest-risk behavior we recently fixed: - plan/apply pathing differences that can silently regress ## Next High-Value Additions -- contract coverage expansion for ingestion/webhook/persisted update commands as they are implemented +- contract coverage expansion for webhook update commands as they are implemented - command-level tests for multi-environment apply/plan orchestration once batching is implemented - staged pull rollback edge-case tests (filesystem errors during commit) - clone idempotency and `--force` behavior tests for pre-existing compose files diff --git a/src/compose/apply.ts b/src/compose/apply.ts index bad453b..4d30646 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -557,9 +557,19 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): const reg = await readJsonFile(registryPath); - // Endpoints exist; exact DTO fields can vary. MVP: create if missing, noop otherwise. const listPath = `${envBase(opts)}/graphql/persisted-documents`; const existingRes = await client.get(listPath); + if (existingRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias: "*", + details: `Failed to list persisted docs (${existingRes.status}).` + }); + return out; + } + const existingAliases = new Set(); const items = @@ -600,7 +610,78 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): } } } else { - out.push({ kind: "noop", env: opEnv, resource: "persisted-doc", alias }); + const getPath = `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + + const remoteDocument = String( + existing.data?.document ?? existing.data?.query ?? existing.data?.persistedDocument?.document ?? "" + ); + const remoteDescription = String( + existing.data?.description ?? existing.data?.persistedDocument?.description ?? "" + ); + const desiredDescription = d.description ?? ""; + + const documentChanged = remoteDocument !== query; + const descriptionChanged = remoteDescription !== desiredDescription; + + if (!documentChanged && !descriptionChanged) { + out.push({ kind: "noop", env: opEnv, resource: "persisted-doc", alias }); + continue; + } + + out.push({ + kind: "update", + env: opEnv, + resource: "persisted-doc", + alias, + details: `${documentChanged ? "document " : ""}${descriptionChanged ? "description" : ""}`.trim() + }); + + if (opts.planOnly) continue; + + if (documentChanged) { + const updateDocumentPath = + `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}/commands/update-document`; + const updateDocumentRes = await client.put(updateDocumentPath, { + newDocument: query + }); + if (updateDocumentRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Update document failed (${updateDocumentRes.status}).` + }); + } + } + + if (descriptionChanged) { + const updateDescriptionPath = + `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}/commands/update-description`; + const updateDescriptionRes = await client.put(updateDescriptionPath, { + newDescription: desiredDescription + }); + if (updateDescriptionRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Update description failed (${updateDescriptionRes.status}).` + }); + } + } } } diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index e9f7ad8..9fb9f4a 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -435,3 +435,97 @@ test("ingestion update calls match contract map", async () => { ingestionFunctionAlias: "fn-a" }); }); + +test("persisted-document update calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-persisted-update-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { + documents: [ + { + alias: "doc-a", + description: "New persisted doc description", + queryFile: "graphql/persisted/doc-a.gql" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-a.gql"), "query { updated }", "utf8"); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-a", + description: "Old persisted doc description", + document: "query { old }" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a/commands/update-document" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "persistedDocumentUpdateDocument", calls, { + projectAlias: "proj", + environmentAlias: "dev", + persistedDocumentAlias: "doc-a" + }); + assertContractCall(contracts, "persistedDocumentUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + persistedDocumentAlias: "doc-a" + }); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index c139543..ec84726 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -262,6 +262,94 @@ test("persisted-document create sends string description when local description assert.equal(body.document, "query { pong }"); }); +test("persisted-document update uses expected update-document and update-description commands", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-update-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { + documents: [ + { + alias: "doc-3", + description: "New description", + queryFile: "graphql/persisted/doc-3.gql" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-3.gql"), "query { newDoc }", "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-3" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-3", + description: "Old description", + document: "query { oldDoc }" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-document" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const updateDocumentCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-document") + ); + assert.ok(updateDocumentCall); + const updateDocumentBody = JSON.parse(updateDocumentCall.bodyText ?? "{}") as Record; + assert.equal(updateDocumentBody.newDocument, "query { newDoc }"); + + const updateDescriptionCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes( + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-description" + ) + ); + assert.ok(updateDescriptionCall); + const updateDescriptionBody = JSON.parse(updateDescriptionCall.bodyText ?? "{}") as Record; + assert.equal(updateDescriptionBody.newDescription, "New description"); +}); + test("ingestion-function create uses expected endpoint and payload shape", async () => { const envDir = await mkEnvDir("compose-contract-ingestion-create-"); await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); From ca15ce86da7a68e0bcb932e82023030a59f3b7f4 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 08:51:11 -0500 Subject: [PATCH 18/47] Add support for update-by-diff for webhooks (url, event types, collections, type schemas, and headers) --- docs/commands.md | 1 + docs/contracts/apply-contract.json | 25 ++++ docs/testing.md | 4 +- src/compose/apply.ts | 157 ++++++++++++++++++++- test/contracts.apply-contract-map.test.ts | 117 +++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 133 +++++++++++++++++ 6 files changed, 434 insertions(+), 3 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 7c8f43c..2a40dc9 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -135,6 +135,7 @@ Alias routing semantics: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - ingestion functions support update-by-diff for script and description (via update commands) - persisted documents support update-by-diff for document and description (via update commands) + - webhooks support update-by-diff for url, event types, collections, type schemas, and headers (via update commands) - if any warning is emitted, lock/state writes are skipped for safety - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index b9b203c..9bb147e 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -67,6 +67,31 @@ "typeSchemaAliases", "customHeaders" ] + }, + "webhookUpdateUrl": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-url", + "requiredBodyKeys": ["url"] + }, + "webhookUpdateEventTypes": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-event-types", + "requiredBodyKeys": ["eventTypes"] + }, + "webhookUpdateCollections": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-collections", + "requiredBodyKeys": ["collectionAliases"] + }, + "webhookUpdateTypeSchemas": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-type-schemas", + "requiredBodyKeys": ["typeSchemaAliases"] + }, + "webhookUpdateHeaders": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-headers", + "requiredBodyKeys": ["newHeaders"] } } } diff --git a/docs/testing.md b/docs/testing.md index 0425325..fb36923 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -102,13 +102,14 @@ - ingestion-function create path + payload (`ingestionFunctionAlias`, `description`, `script`) - ingestion-function update commands (`update-script`, `update-description`) payload shape - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) + - webhook update commands (`update-url`, `update-event-types`, `update-collections`, `update-type-schemas`, `update-headers`) payload shapes - type-schema update-schema endpoint receives raw schema JSON body - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes ingestion and persisted-document update contract-map assertions + - includes ingestion, persisted-document, and webhook update contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: @@ -158,7 +159,6 @@ These tests protect the highest-risk behavior we recently fixed: - plan/apply pathing differences that can silently regress ## Next High-Value Additions -- contract coverage expansion for webhook update commands as they are implemented - command-level tests for multi-environment apply/plan orchestration once batching is implemented - staged pull rollback edge-case tests (filesystem errors during commit) - clone idempotency and `--force` behavior tests for pre-existing compose files diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 4d30646..249bb8f 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -751,9 +751,164 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom } } } else { - out.push({ kind: "noop", env: opEnv, resource: "webhook", alias }); + const getPath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + + const remoteUrl = String(existing.data?.url ?? existing.data?.webhook?.url ?? ""); + const remoteEventTypes = normalizeStringArray( + existing.data?.eventTypes ?? existing.data?.webhook?.eventTypes ?? [] + ); + const remoteCollections = normalizeStringArray( + existing.data?.collectionAliases ?? existing.data?.webhook?.collectionAliases ?? [] + ); + const remoteTypeSchemas = normalizeStringArray( + existing.data?.typeSchemaAliases ?? existing.data?.webhook?.typeSchemaAliases ?? [] + ); + const remoteHeaders = normalizeStringMap( + existing.data?.customHeaders ?? existing.data?.webhook?.customHeaders ?? {} + ); + + const desiredUrl = w.url; + const desiredEventTypes = normalizeStringArray(w.eventTypes ?? []); + const desiredCollections = normalizeStringArray(w.collectionAliases ?? []); + const desiredTypeSchemas = normalizeStringArray(w.typeSchemaAliases ?? []); + const desiredHeaders = normalizeStringMap(w.customHeaders ?? {}); + + const urlChanged = remoteUrl !== desiredUrl; + const eventTypesChanged = stringify(remoteEventTypes) !== stringify(desiredEventTypes); + const collectionsChanged = stringify(remoteCollections) !== stringify(desiredCollections); + const typeSchemasChanged = stringify(remoteTypeSchemas) !== stringify(desiredTypeSchemas); + const headersChanged = stringify(remoteHeaders) !== stringify(desiredHeaders); + + if (!urlChanged && !eventTypesChanged && !collectionsChanged && !typeSchemasChanged && !headersChanged) { + out.push({ kind: "noop", env: opEnv, resource: "webhook", alias }); + continue; + } + + out.push({ + kind: "update", + env: opEnv, + resource: "webhook", + alias, + details: [ + urlChanged ? "url" : "", + eventTypesChanged ? "eventTypes" : "", + collectionsChanged ? "collections" : "", + typeSchemasChanged ? "typeSchemas" : "", + headersChanged ? "headers" : "" + ] + .filter(Boolean) + .join(" ") + }); + + if (opts.planOnly) continue; + + if (urlChanged) { + const updateUrlPath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-url`; + const updateUrlRes = await client.put(updateUrlPath, { url: desiredUrl }); + if (updateUrlRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Update url failed (${updateUrlRes.status}).` + }); + } + } + + if (eventTypesChanged) { + const updateEventTypesPath = + `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-event-types`; + const updateEventTypesRes = await client.put(updateEventTypesPath, { + eventTypes: desiredEventTypes + }); + if (updateEventTypesRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Update event types failed (${updateEventTypesRes.status}).` + }); + } + } + + if (collectionsChanged) { + const updateCollectionsPath = + `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-collections`; + const updateCollectionsRes = await client.put(updateCollectionsPath, { + collectionAliases: desiredCollections + }); + if (updateCollectionsRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Update collections failed (${updateCollectionsRes.status}).` + }); + } + } + + if (typeSchemasChanged) { + const updateTypeSchemasPath = + `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-type-schemas`; + const updateTypeSchemasRes = await client.put(updateTypeSchemasPath, { + typeSchemaAliases: desiredTypeSchemas + }); + if (updateTypeSchemasRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Update type schemas failed (${updateTypeSchemasRes.status}).` + }); + } + } + + if (headersChanged) { + const updateHeadersPath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-headers`; + const updateHeadersRes = await client.put(updateHeadersPath, { + newHeaders: desiredHeaders + }); + if (updateHeadersRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Update headers failed (${updateHeadersRes.status}).` + }); + } + } } } return out; } + +function normalizeStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.map((v) => String(v)).sort((a, b) => a.localeCompare(b)); +} + +function normalizeStringMap(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + out[k] = String(v); + } + return out; +} diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index 9fb9f4a..443f556 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -529,3 +529,120 @@ test("persisted-document update calls match contract map", async () => { persistedDocumentAlias: "doc-a" }); }); + +test("webhook update calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-webhook-update-"); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "send-on-create", + url: "https://example.com/new", + eventTypes: ["content.deleted"], + collectionAliases: ["articles"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + url: "https://example.com/old", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["software"], + customHeaders: { authorization: "Bearer old" } + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + const params = { + projectAlias: "proj", + environmentAlias: "dev", + webhookAlias: "send-on-create" + }; + assertContractCall(contracts, "webhookUpdateUrl", calls, params); + assertContractCall(contracts, "webhookUpdateEventTypes", calls, params); + assertContractCall(contracts, "webhookUpdateCollections", calls, params); + assertContractCall(contracts, "webhookUpdateTypeSchemas", calls, params); + assertContractCall(contracts, "webhookUpdateHeaders", calls, params); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index ec84726..967ec97 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -564,6 +564,139 @@ test("webhook create uses expected endpoint and payload shape", async () => { assert.deepEqual(body.customHeaders, { "x-api-key": "abc123" }); }); +test("webhook update uses expected command endpoints and payload shapes", async () => { + const envDir = await mkEnvDir("compose-contract-webhook-update-"); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "send-on-create", + url: "https://example.com/new", + eventTypes: ["content.deleted"], + collectionAliases: ["articles"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + url: "https://example.com/old", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["software"], + customHeaders: { authorization: "Bearer old" } + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const updateUrlCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url") + ); + assert.ok(updateUrlCall); + assert.deepEqual(JSON.parse(updateUrlCall.bodyText ?? "{}"), { url: "https://example.com/new" }); + + const updateEventTypesCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types") + ); + assert.ok(updateEventTypesCall); + assert.deepEqual(JSON.parse(updateEventTypesCall.bodyText ?? "{}"), { eventTypes: ["content.deleted"] }); + + const updateCollectionsCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections") + ); + assert.ok(updateCollectionsCall); + assert.deepEqual(JSON.parse(updateCollectionsCall.bodyText ?? "{}"), { collectionAliases: ["articles"] }); + + const updateTypeSchemasCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas") + ); + assert.ok(updateTypeSchemasCall); + assert.deepEqual(JSON.parse(updateTypeSchemasCall.bodyText ?? "{}"), { typeSchemaAliases: ["article"] }); + + const updateHeadersCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers") + ); + assert.ok(updateHeadersCall); + assert.deepEqual(JSON.parse(updateHeadersCall.bodyText ?? "{}"), { + newHeaders: { authorization: "Bearer abc" } + }); +}); + test("type-schema update uses raw schema body at update-schema command endpoint", async () => { const envDir = await mkEnvDir("compose-contract-type-schema-"); await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); From cdd0a3bca05450092bd0c23514871fccd023dcb5 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 09:05:31 -0500 Subject: [PATCH 19/47] Improve support for multi-environment planning and applying --- docs/commands.md | 8 + docs/testing.md | 7 + src/commands/apply.ts | 173 +++++++++------- src/commands/plan.ts | 5 +- test/commands.apply-multi-env.test.ts | 197 +++++++++++++++++++ test/commands.apply.output-snapshots.test.ts | 28 +++ 6 files changed, 344 insertions(+), 74 deletions(-) create mode 100644 test/commands.apply-multi-env.test.ts diff --git a/docs/commands.md b/docs/commands.md index 2a40dc9..b0f3ad6 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -23,6 +23,8 @@ This document defines the intended command behavior for the core workflow: - machine-readable output should be available where relevant (`status --json`) - Plan/apply operation output: - each operation line includes explicit environment context in the format `[env=]` + - each environment run emits a per-environment summary line (`Plan env ...` / `Apply env ...`) + - final output includes environment aggregate counts (`Environments processed: total=..., succeeded=..., warned=...`) - Idempotency: - rerunning the same command with unchanged inputs should produce no new side effects @@ -120,6 +122,9 @@ Alias routing semantics: - Responsibility: - compute and print API operation plan without mutating remote resources - includes environment create/rename/noop based on identity mapping +- Scope: + - `--env ` plans a single environment + - omitting `--env` plans all configured environments - Notes: - on pending rename, nested reads target the old remote alias (`remoteAlias`) - Side effects: none (reads local files + remote APIs). @@ -131,6 +136,9 @@ Alias routing semantics: - execute planned operations against management API - perform environment rename with `POST /v1/projects/{projectAlias}/environments/{environmentAlias}/commands/rename` and body `{ "newEnvironmentAlias": "" }` when rename intent exists - update `compose.lock.json` and `compose.state.json.remoteAlias` only when apply completes with zero warnings +- Scope: + - `--env ` applies a single environment + - omitting `--env` applies all configured environments - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - ingestion functions support update-by-diff for script and description (via update commands) diff --git a/docs/testing.md b/docs/testing.md index fb36923..90bd431 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -89,8 +89,15 @@ - `test/commands.apply.output-snapshots.test.ts` - snapshot-style assertions for exact plan/apply operation lines + - snapshot-style assertions for per-environment summary lines + - snapshot-style assertions for environment aggregate summary line - snapshot-style assertions for final plan/apply summary lines +- `test/commands.apply-multi-env.test.ts` + - verifies plan/apply run across all configured environments when `--env` is omitted + - verifies mixed outcome behavior: successful env writes lock/state metadata while warned env skips lock writes + - verifies non-zero exit when any environment emits warnings + - `test/contracts.apply-openapi-shape.test.ts` - contract-shape assertions for key apply endpoints and payloads: - environment create path + `environmentAlias` payload diff --git a/src/commands/apply.ts b/src/commands/apply.ts index b3a94ce..460fd93 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -18,7 +18,7 @@ import { type Args = { dir: string; - env: string; + env?: string; clientId?: string; clientSecret?: string; scope?: string; @@ -51,8 +51,7 @@ export const applyCommand: CommandModule<{}, Args> = { }, env: { type: "string", - default: "dev", - describe: "Environment name to apply (must exist in umbraco-compose.yaml)" + describe: "Environment name to apply (must exist in umbraco-compose.yaml). Omit to apply all environments." }, clientId: { type: "string", @@ -97,10 +96,13 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); const cfg = await readComposeConfig(composeYamlPath); - const envCfg = cfg.environments[rawArgs.env]; - if (!envCfg) { + if (rawArgs.env && !cfg.environments[rawArgs.env]) { throw new Error(`Environment "${rawArgs.env}" not found in ${composeYamlPath}`); } + const targetEnvs = rawArgs.env ? [rawArgs.env] : Object.keys(cfg.environments); + if (targetEnvs.length === 0) { + throw new Error(`No environments configured in ${composeYamlPath}`); + } const statePath = path.join(rootDir, COMPOSE_STATE_FILE); const stateFromDisk = await readComposeState(rootDir); @@ -108,14 +110,6 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { const stateResult = ensureStateForAliases(stateFromDisk, cfg.project, aliases); const state = stateResult.state; - let envState = findEnvironmentByAlias(state, rawArgs.env); - if (!envState) { - throw new Error( - `Environment "${rawArgs.env}" has no entry in ${statePath}. ` + - `Run "compose env rename" for explicit alias changes or recreate ${statePath}.` - ); - } - const clientId = rawArgs.clientId ?? process.env.COMPOSE_MGMT_CLIENT_ID; const clientSecret = rawArgs.clientSecret ?? process.env.COMPOSE_MGMT_CLIENT_SECRET; @@ -126,8 +120,6 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { ); } - const envDir = path.resolve(rootDir, envCfg.dir); - const envDescription = envCfg.description ?? `${rawArgs.env} Environment`; const scopeVal = rawArgs.scope ?? process.env.COMPOSE_MGMT_SCOPE; const audienceVal = rawArgs.audience ?? process.env.COMPOSE_MGMT_AUDIENCE; @@ -143,77 +135,116 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { if (scopeVal !== undefined) oauth.scope = scopeVal; if (audienceVal !== undefined) oauth.audience = audienceVal; - const desired = await computeDesiredHashes(envDir); - const prior = await readLock(envDir); - - if (planOnly) { - console.log("State changes since last apply:"); - const priorRes = prior?.resources ?? {}; - printChange("collections", priorRes.collections?.hash, desired.collections.hash); - printChange("typeSchemas", priorRes.typeSchemas?.hash, desired.typeSchemas.hash); - printChange("ingestionFunctions", priorRes.ingestionFunctions?.hash, desired.ingestionFunctions.hash); - printChange("persistedDocs", priorRes.persistedDocs?.hash, desired.persistedDocs.hash); - printChange("webhooks", priorRes.webhooks?.hash, desired.webhooks.hash); - console.log(""); - } - - const ops = await applyEnvironment({ - project: cfg.project, - env: rawArgs.env, - ...(envState.remoteAlias ? { remoteEnvAlias: envState.remoteAlias } : {}), - envDir, - envDescription, - baseUrl: envCfg.managementBaseUrl, - oauth, - planOnly - }); - const byKind: Record = { create: 0, update: 0, noop: 0, warn: 0 }; - for (const op of ops) { - byKind[op.kind] += 1; - } + let stateShouldWrite = false; + let succeededEnvironments = 0; + let warnedEnvironments = 0; + + for (const envAlias of targetEnvs) { + const envCfg = cfg.environments[envAlias]; + const envDir = path.resolve(rootDir, envCfg.dir); + const envDescription = envCfg.description ?? `${envAlias} Environment`; + + const envState = findEnvironmentByAlias(state, envAlias); + if (!envState) { + throw new Error( + `Environment "${envAlias}" has no entry in ${statePath}. ` + + `Run "compose env rename" for explicit alias changes or recreate ${statePath}.` + ); + } - if (!planOnly && byKind.warn === 0) { - const cliVersion = getCliVersionSafe(); - const gitCommit = await getGitCommit(rootDir); + const desired = await computeDesiredHashes(envDir); + const prior = await readLock(envDir); - const lastApplied: NonNullable = { - at: new Date().toISOString() - }; + if (targetEnvs.length > 1) { + console.log(`Environment: ${envAlias}`); + } - if (cliVersion !== undefined) lastApplied.cliVersion = cliVersion; - if (gitCommit !== undefined) lastApplied.gitCommit = gitCommit; + if (planOnly) { + console.log("State changes since last apply:"); + const priorRes = prior?.resources ?? {}; + printChange("collections", priorRes.collections?.hash, desired.collections.hash); + printChange("typeSchemas", priorRes.typeSchemas?.hash, desired.typeSchemas.hash); + printChange("ingestionFunctions", priorRes.ingestionFunctions?.hash, desired.ingestionFunctions.hash); + printChange("persistedDocs", priorRes.persistedDocs?.hash, desired.persistedDocs.hash); + printChange("webhooks", priorRes.webhooks?.hash, desired.webhooks.hash); + console.log(""); + } - const nextLock: LockFile = { - version: 1, + const ops = await applyEnvironment({ project: cfg.project, - environment: rawArgs.env, - lastApplied, - resources: desired - }; + env: envAlias, + ...(envState.remoteAlias ? { remoteEnvAlias: envState.remoteAlias } : {}), + envDir, + envDescription, + baseUrl: envCfg.managementBaseUrl, + oauth, + planOnly + }); + + const envByKind: Record = { create: 0, update: 0, noop: 0, warn: 0 }; + for (const op of ops) { + envByKind[op.kind] += 1; + byKind[op.kind] += 1; + } - await writeLock(envDir, nextLock); - console.log(`\nWrote lock file: ${path.join(envDir, "compose.lock.json")}`); + if (!planOnly && envByKind.warn === 0) { + const cliVersion = getCliVersionSafe(); + const gitCommit = await getGitCommit(rootDir); + + const lastApplied: NonNullable = { + at: new Date().toISOString() + }; + + if (cliVersion !== undefined) lastApplied.cliVersion = cliVersion; + if (gitCommit !== undefined) lastApplied.gitCommit = gitCommit; + + const nextLock: LockFile = { + version: 1, + project: cfg.project, + environment: envAlias, + lastApplied, + resources: desired + }; + + await writeLock(envDir, nextLock); + console.log(`\nWrote lock file: ${path.join(envDir, "compose.lock.json")}`); + + const environmentWarn = ops.some((op) => op.kind === "warn" && op.resource === "environment"); + if (!environmentWarn) { + markEnvironmentRemoteAlias(state, envState.id, envAlias); + } + stateShouldWrite = true; + } else if (!planOnly) { + console.log("\nSkipped lock/state writes because warnings were emitted."); + } - const environmentWarn = ops.some((op) => op.kind === "warn" && op.resource === "environment"); - if (!environmentWarn) { - markEnvironmentRemoteAlias(state, envState.id, rawArgs.env); + for (const op of ops) { + const label = op.kind.toUpperCase().padEnd(6); + const details = "details" in op && op.details ? ` — ${op.details}` : ""; + console.log(`${label} [env=${op.env}] ${op.resource}:${op.alias}${details}`); } + + if (envByKind.warn > 0) warnedEnvironments += 1; + else succeededEnvironments += 1; + + console.log( + `${planOnly ? "Plan" : "Apply"} env ${envAlias}: ` + + `create=${envByKind.create}, update=${envByKind.update}, noop=${envByKind.noop}, warn=${envByKind.warn}` + ); + console.log(""); + } + + if (!planOnly && stateShouldWrite) { await writeComposeState(rootDir, state); if (stateResult.changed) { console.log(`Wrote state file: ${statePath}`); } - } else if (!planOnly) { - console.log("\nSkipped lock/state writes because warnings were emitted."); - } - - for (const op of ops) { - const label = op.kind.toUpperCase().padEnd(6); - const details = "details" in op && op.details ? ` — ${op.details}` : ""; - console.log(`${label} [env=${op.env}] ${op.resource}:${op.alias}${details}`); } - console.log(""); + console.log( + `Environments processed: total=${targetEnvs.length}, succeeded=${succeededEnvironments}, warned=${warnedEnvironments}` + ); console.log( `${planOnly ? "Plan" : "Apply"} complete. ` + `create=${byKind.create}, update=${byKind.update}, noop=${byKind.noop}, warn=${byKind.warn}` diff --git a/src/commands/plan.ts b/src/commands/plan.ts index b8ec343..8c76c48 100644 --- a/src/commands/plan.ts +++ b/src/commands/plan.ts @@ -3,7 +3,7 @@ import { runApply } from "./apply.js"; type Args = { dir: string; - env: string; + env?: string; clientId?: string; clientSecret?: string; scope?: string; @@ -22,8 +22,7 @@ export const planCommand: CommandModule<{}, Args> = { }, env: { type: "string", - default: "dev", - describe: "Environment name to plan (must exist in umbraco-compose.yaml)" + describe: "Environment name to plan (must exist in umbraco-compose.yaml). Omit to plan all environments." }, clientId: { type: "string", diff --git a/test/commands.apply-multi-env.test.ts b/test/commands.apply-multi-env.test.ts new file mode 100644 index 0000000..287a597 --- /dev/null +++ b/test/commands.apply-multi-env.test.ts @@ -0,0 +1,197 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + +async function createFixture(): Promise<{ root: string; devDir: string; prodDir: string }> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-multi-env-")); + const devDir = path.join(root, "env", "dev"); + const prodDir = path.join(root, "env", "prod"); + await fs.mkdir(devDir, { recursive: true }); + await fs.mkdir(prodDir, { recursive: true }); + + await fs.writeFile( + path.join(devDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(devDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + await fs.writeFile( + path.join(prodDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), + "utf8" + ); + await fs.writeFile(path.join(prodDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + }, + prod: { + dir: "./env/prod", + description: "Prod", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + + return { root, devDir, prodDir }; +} + +function installMockFetch(): () => void { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { + edges: [{ node: { environmentAlias: "dev" } }, { node: { environmentAlias: "prod" } }] + }); + } + + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [] }); + if (method === "POST") return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/prod/collections") { + return jsonResponse(500, { error: "prod unavailable" }); + } + + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/prod/webhooks") { + return jsonResponse(200, { edges: [] }); + } + + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + return () => { + globalThis.fetch = oldFetch; + }; +} + +test("plan without --env evaluates all configured environments and warns if any environment warns", async () => { + const { root } = await createFixture(); + const restoreFetch = installMockFetch(); + const oldExitCode = process.exitCode; + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((a) => String(a)).join(" ")); + }; + + let observedExitCode: number | undefined; + process.exitCode = 0; + try { + await runApply({ + dir: root, + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + observedExitCode = process.exitCode; + } finally { + restoreFetch(); + console.log = originalLog; + process.exitCode = oldExitCode; + } + + assert.equal(output.some((line) => line.includes("[env=dev] collection:content")), true); + assert.equal(output.some((line) => line.includes("[env=prod] collection:*")), true); + assert.equal(output.some((line) => line.includes("Plan env dev: create=1, update=0, noop=1, warn=0")), true); + assert.equal(output.some((line) => line.includes("Plan env prod: create=0, update=0, noop=1, warn=1")), true); + assert.equal( + output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), + true + ); + assert.equal(output.some((line) => line.includes("Plan complete.")), true); + assert.equal(observedExitCode, 1); +}); + +test("apply without --env writes lock/state for successful environments and skips failed environments", async () => { + const { root, devDir, prodDir } = await createFixture(); + const restoreFetch = installMockFetch(); + const oldExitCode = process.exitCode; + const originalLog = console.log; + const output: string[] = []; + console.log = (...args: unknown[]) => { + output.push(args.map((a) => String(a)).join(" ")); + }; + + let observedExitCode: number | undefined; + process.exitCode = 0; + try { + await runApply({ + dir: root, + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + observedExitCode = process.exitCode; + } finally { + restoreFetch(); + console.log = originalLog; + process.exitCode = oldExitCode; + } + + assert.equal(await exists(path.join(devDir, "compose.lock.json")), true); + assert.equal(await exists(path.join(prodDir, "compose.lock.json")), false); + + const state = await readComposeState(root); + assert.ok(state); + const dev = state.environments.find((e) => e.alias === "dev"); + const prod = state.environments.find((e) => e.alias === "prod"); + assert.ok(dev); + assert.ok(prod); + assert.equal(dev.remoteAlias, "dev"); + assert.equal(prod.remoteAlias, undefined); + assert.equal(output.some((line) => line.includes("Apply env dev: create=1, update=0, noop=1, warn=0")), true); + assert.equal(output.some((line) => line.includes("Apply env prod: create=0, update=0, noop=1, warn=1")), true); + assert.equal( + output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), + true + ); + assert.equal(observedExitCode, 1); +}); diff --git a/test/commands.apply.output-snapshots.test.ts b/test/commands.apply.output-snapshots.test.ts index dd96ad0..dac2d63 100644 --- a/test/commands.apply.output-snapshots.test.ts +++ b/test/commands.apply.output-snapshots.test.ts @@ -98,6 +98,18 @@ function finalSummaryLine(output: string[]): string { return line; } +function environmentSummaryLine(output: string[]): string { + const line = output.find((l) => /^Environments processed: /.test(l)); + assert.ok(line); + return line; +} + +function perEnvironmentRunSummaryLine(output: string[]): string { + const line = output.find((l) => /^(Plan|Apply) env /.test(l)); + assert.ok(line); + return line; +} + test("plan output operation lines and summary match expected snapshot", async () => { const root = await createComposeRoot(); const restoreFetch = installMockFetch(); @@ -124,6 +136,14 @@ test("plan output operation lines and summary match expected snapshot", async () "NOOP [env=dev] environment:dev", "CREATE [env=dev] collection:content" ]); + assert.equal( + perEnvironmentRunSummaryLine(output), + "Plan env dev: create=1, update=0, noop=1, warn=0" + ); + assert.equal( + environmentSummaryLine(output), + "Environments processed: total=1, succeeded=1, warned=0" + ); assert.equal( finalSummaryLine(output), "Plan complete. create=1, update=0, noop=1, warn=0" @@ -156,6 +176,14 @@ test("apply output operation lines and summary match expected snapshot", async ( "NOOP [env=dev] environment:dev", "CREATE [env=dev] collection:content" ]); + assert.equal( + perEnvironmentRunSummaryLine(output), + "Apply env dev: create=1, update=0, noop=1, warn=0" + ); + assert.equal( + environmentSummaryLine(output), + "Environments processed: total=1, succeeded=1, warned=0" + ); assert.equal( finalSummaryLine(output), "Apply complete. create=1, update=0, noop=1, warn=0" From 133cae6d68f194085a9826b714dccddf1c27bb9f Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 09:45:24 -0500 Subject: [PATCH 20/47] Improve support for convergence on persisted docs and webhooks --- docs/commands.md | 5 +- docs/contracts/apply-contract.json | 10 ++ docs/testing.md | 6 +- src/commands/apply.ts | 10 +- src/compose/apply.ts | 49 ++++++++ test/commands.apply-multi-env.test.ts | 8 +- test/commands.apply.output-snapshots.test.ts | 10 +- test/commands.generate-apply-flow.test.ts | 2 +- .../commands.generate-apply-warn-flow.test.ts | 2 +- test/commands.generate-flow.test.ts | 2 +- test/contracts.apply-contract-map.test.ts | 107 ++++++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 77 +++++++++++++ 12 files changed, 267 insertions(+), 21 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index b0f3ad6..6cc2d99 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -23,6 +23,7 @@ This document defines the intended command behavior for the core workflow: - machine-readable output should be available where relevant (`status --json`) - Plan/apply operation output: - each operation line includes explicit environment context in the format `[env=]` + - operation kinds include `CREATE`, `DELETE`, `UPDATE`, `NOOP`, `WARN` - each environment run emits a per-environment summary line (`Plan env ...` / `Apply env ...`) - final output includes environment aggregate counts (`Environments processed: total=..., succeeded=..., warned=...`) - Idempotency: @@ -142,8 +143,8 @@ Alias routing semantics: - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - ingestion functions support update-by-diff for script and description (via update commands) - - persisted documents support update-by-diff for document and description (via update commands) - - webhooks support update-by-diff for url, event types, collections, type schemas, and headers (via update commands) + - persisted documents support create/update/delete convergence + - webhooks support create/update/delete convergence - if any warning is emitted, lock/state writes are skipped for safety - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index 9bb147e..607b064 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -56,6 +56,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}/commands/update-document", "requiredBodyKeys": ["newDocument"] }, + "persistedDocumentDelete": { + "method": "DELETE", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}", + "requiredBodyKeys": [] + }, "webhookCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks", @@ -92,6 +97,11 @@ "method": "PUT", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-headers", "requiredBodyKeys": ["newHeaders"] + }, + "webhookDelete": { + "method": "DELETE", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}", + "requiredBodyKeys": [] } } } diff --git a/docs/testing.md b/docs/testing.md index 90bd431..5421925 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -91,7 +91,7 @@ - snapshot-style assertions for exact plan/apply operation lines - snapshot-style assertions for per-environment summary lines - snapshot-style assertions for environment aggregate summary line - - snapshot-style assertions for final plan/apply summary lines + - snapshot-style assertions for final plan/apply summary lines (including `delete` count) - `test/commands.apply-multi-env.test.ts` - verifies plan/apply run across all configured environments when `--env` is omitted @@ -106,17 +106,19 @@ - persisted document create payload uses `document` field - persisted document create emits string `description` even when local description is omitted - persisted-document update commands (`update-document`, `update-description`) payload shape + - persisted-document delete endpoint shape - ingestion-function create path + payload (`ingestionFunctionAlias`, `description`, `script`) - ingestion-function update commands (`update-script`, `update-description`) payload shape - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) - webhook update commands (`update-url`, `update-event-types`, `update-collections`, `update-type-schemas`, `update-headers`) payload shapes + - webhook delete endpoint shape - type-schema update-schema endpoint receives raw schema JSON body - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes ingestion, persisted-document, and webhook update contract-map assertions + - includes ingestion, persisted-document, and webhook update/delete contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: diff --git a/src/commands/apply.ts b/src/commands/apply.ts index 460fd93..488b22e 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -34,7 +34,7 @@ type ComposeConfig = { environments: Record; }; -type OpKind = "create" | "update" | "noop" | "warn"; +type OpKind = "create" | "delete" | "update" | "noop" | "warn"; type RunApplyArgs = Args & { forcedPlan?: boolean; @@ -135,7 +135,7 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { if (scopeVal !== undefined) oauth.scope = scopeVal; if (audienceVal !== undefined) oauth.audience = audienceVal; - const byKind: Record = { create: 0, update: 0, noop: 0, warn: 0 }; + const byKind: Record = { create: 0, delete: 0, update: 0, noop: 0, warn: 0 }; let stateShouldWrite = false; let succeededEnvironments = 0; let warnedEnvironments = 0; @@ -182,7 +182,7 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { planOnly }); - const envByKind: Record = { create: 0, update: 0, noop: 0, warn: 0 }; + const envByKind: Record = { create: 0, delete: 0, update: 0, noop: 0, warn: 0 }; for (const op of ops) { envByKind[op.kind] += 1; byKind[op.kind] += 1; @@ -230,7 +230,7 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { console.log( `${planOnly ? "Plan" : "Apply"} env ${envAlias}: ` + - `create=${envByKind.create}, update=${envByKind.update}, noop=${envByKind.noop}, warn=${envByKind.warn}` + `create=${envByKind.create}, delete=${envByKind.delete}, update=${envByKind.update}, noop=${envByKind.noop}, warn=${envByKind.warn}` ); console.log(""); } @@ -247,7 +247,7 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { ); console.log( `${planOnly ? "Plan" : "Apply"} complete. ` + - `create=${byKind.create}, update=${byKind.update}, noop=${byKind.noop}, warn=${byKind.warn}` + `create=${byKind.create}, delete=${byKind.delete}, update=${byKind.update}, noop=${byKind.noop}, warn=${byKind.warn}` ); process.exitCode = byKind.warn > 0 ? 1 : 0; diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 249bb8f..40a64a7 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -6,6 +6,7 @@ import { composeManagementOAuth } from "./auth/providers/oauth-compose.js"; type PlanOp = | { kind: "create"; env: string; resource: string; alias: string; details?: string } + | { kind: "delete"; env: string; resource: string; alias: string; details?: string } | { kind: "update"; env: string; resource: string; alias: string; details?: string } | { kind: "noop"; env: string; resource: string; alias: string; details?: string } | { kind: "warn"; env: string; resource: string; alias: string; details?: string }; @@ -556,6 +557,7 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): if (!(await exists(registryPath))) return out; const reg = await readJsonFile(registryPath); + const desiredAliases = new Set(reg.documents.map((d) => d.alias)); const listPath = `${envBase(opts)}/graphql/persisted-documents`; const existingRes = await client.get(listPath); @@ -685,6 +687,24 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): } } + for (const alias of existingAliases) { + if (desiredAliases.has(alias)) continue; + out.push({ kind: "delete", env: opEnv, resource: "persisted-doc", alias }); + if (!opts.planOnly) { + const deletePath = `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}`; + const deleteRes = await client.delete(deletePath); + if (deleteRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Delete failed (${deleteRes.status}).` + }); + } + } + } + return out; } @@ -708,9 +728,20 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom if (!(await exists(filePath))) return out; const desired = await readJsonFile(filePath); + const desiredAliases = new Set(desired.webhooks.map((w) => w.alias)); const listPath = `${envBase(opts)}/webhooks`; const existingRes = await client.get(listPath); + if (existingRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias: "*", + details: `Failed to list webhooks (${existingRes.status}).` + }); + return out; + } const existingAliases = new Set(); const items = @@ -896,6 +927,24 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom } } + for (const alias of existingAliases) { + if (desiredAliases.has(alias)) continue; + out.push({ kind: "delete", env: opEnv, resource: "webhook", alias }); + if (!opts.planOnly) { + const deletePath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}`; + const deleteRes = await client.delete(deletePath); + if (deleteRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Delete failed (${deleteRes.status}).` + }); + } + } + } + return out; } diff --git a/test/commands.apply-multi-env.test.ts b/test/commands.apply-multi-env.test.ts index 287a597..b10a4e1 100644 --- a/test/commands.apply-multi-env.test.ts +++ b/test/commands.apply-multi-env.test.ts @@ -139,8 +139,8 @@ test("plan without --env evaluates all configured environments and warns if any assert.equal(output.some((line) => line.includes("[env=dev] collection:content")), true); assert.equal(output.some((line) => line.includes("[env=prod] collection:*")), true); - assert.equal(output.some((line) => line.includes("Plan env dev: create=1, update=0, noop=1, warn=0")), true); - assert.equal(output.some((line) => line.includes("Plan env prod: create=0, update=0, noop=1, warn=1")), true); + assert.equal(output.some((line) => line.includes("Plan env dev: create=1, delete=0, update=0, noop=1, warn=0")), true); + assert.equal(output.some((line) => line.includes("Plan env prod: create=0, delete=0, update=0, noop=1, warn=1")), true); assert.equal( output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), true @@ -187,8 +187,8 @@ test("apply without --env writes lock/state for successful environments and skip assert.ok(prod); assert.equal(dev.remoteAlias, "dev"); assert.equal(prod.remoteAlias, undefined); - assert.equal(output.some((line) => line.includes("Apply env dev: create=1, update=0, noop=1, warn=0")), true); - assert.equal(output.some((line) => line.includes("Apply env prod: create=0, update=0, noop=1, warn=1")), true); + assert.equal(output.some((line) => line.includes("Apply env dev: create=1, delete=0, update=0, noop=1, warn=0")), true); + assert.equal(output.some((line) => line.includes("Apply env prod: create=0, delete=0, update=0, noop=1, warn=1")), true); assert.equal( output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), true diff --git a/test/commands.apply.output-snapshots.test.ts b/test/commands.apply.output-snapshots.test.ts index dac2d63..b827c8d 100644 --- a/test/commands.apply.output-snapshots.test.ts +++ b/test/commands.apply.output-snapshots.test.ts @@ -89,7 +89,7 @@ function captureLogs(): { output: string[]; restore: () => void } { } function operationLines(output: string[]): string[] { - return output.filter((line) => /^(CREATE|UPDATE|NOOP|WARN)\s+\[env=/.test(line)); + return output.filter((line) => /^(CREATE|DELETE|UPDATE|NOOP|WARN)\s+\[env=/.test(line)); } function finalSummaryLine(output: string[]): string { @@ -138,7 +138,7 @@ test("plan output operation lines and summary match expected snapshot", async () ]); assert.equal( perEnvironmentRunSummaryLine(output), - "Plan env dev: create=1, update=0, noop=1, warn=0" + "Plan env dev: create=1, delete=0, update=0, noop=1, warn=0" ); assert.equal( environmentSummaryLine(output), @@ -146,7 +146,7 @@ test("plan output operation lines and summary match expected snapshot", async () ); assert.equal( finalSummaryLine(output), - "Plan complete. create=1, update=0, noop=1, warn=0" + "Plan complete. create=1, delete=0, update=0, noop=1, warn=0" ); }); @@ -178,7 +178,7 @@ test("apply output operation lines and summary match expected snapshot", async ( ]); assert.equal( perEnvironmentRunSummaryLine(output), - "Apply env dev: create=1, update=0, noop=1, warn=0" + "Apply env dev: create=1, delete=0, update=0, noop=1, warn=0" ); assert.equal( environmentSummaryLine(output), @@ -186,6 +186,6 @@ test("apply output operation lines and summary match expected snapshot", async ( ); assert.equal( finalSummaryLine(output), - "Apply complete. create=1, update=0, noop=1, warn=0" + "Apply complete. create=1, delete=0, update=0, noop=1, warn=0" ); }); diff --git a/test/commands.generate-apply-flow.test.ts b/test/commands.generate-apply-flow.test.ts index 4939e4c..cdb60bb 100644 --- a/test/commands.generate-apply-flow.test.ts +++ b/test/commands.generate-apply-flow.test.ts @@ -164,7 +164,7 @@ test("generate -> apply writes lock/state and reports expected apply operations" assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); assert.equal( - output.some((line) => line.includes("Apply complete. create=5, update=0, noop=1, warn=0")), + output.some((line) => line.includes("Apply complete. create=5, delete=0, update=0, noop=1, warn=0")), true ); }); diff --git a/test/commands.generate-apply-warn-flow.test.ts b/test/commands.generate-apply-warn-flow.test.ts index 1a6d608..5aaf6de 100644 --- a/test/commands.generate-apply-warn-flow.test.ts +++ b/test/commands.generate-apply-warn-flow.test.ts @@ -163,7 +163,7 @@ test("generate -> apply warning path skips lock/state writes", async () => { true ); assert.equal( - output.some((line) => line.includes("Apply complete. create=5, update=0, noop=1, warn=1")), + output.some((line) => line.includes("Apply complete. create=5, delete=0, update=0, noop=1, warn=1")), true ); }); diff --git a/test/commands.generate-flow.test.ts b/test/commands.generate-flow.test.ts index 6ad3ed8..903d934 100644 --- a/test/commands.generate-flow.test.ts +++ b/test/commands.generate-flow.test.ts @@ -159,7 +159,7 @@ test("generate -> validate --strict -> plan baseline succeeds with expected oper assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); assert.equal( - output.some((line) => line.includes("Plan complete. create=5, update=0, noop=1, warn=0")), + output.some((line) => line.includes("Plan complete. create=5, delete=0, update=0, noop=1, warn=0")), true ); }); diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index 443f556..c7277a2 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -646,3 +646,110 @@ test("webhook update calls match contract map", async () => { assertContractCall(contracts, "webhookUpdateTypeSchemas", calls, params); assertContractCall(contracts, "webhookUpdateHeaders", calls, params); }); + +test("persisted-document delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-persisted-delete-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify({ documents: [] }, null, 2), + "utf8" + ); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "persistedDocumentDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + persistedDocumentAlias: "doc-a" + }); +}); + +test("webhook delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-webhook-delete-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "webhookDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + webhookAlias: "send-on-create" + }); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index 967ec97..e19dc76 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -350,6 +350,46 @@ test("persisted-document update uses expected update-document and update-descrip assert.equal(updateDescriptionBody.newDescription, "New description"); }); +test("persisted-document delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-delete-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-4" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-4" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const deleteCall = calls.find( + (c) => + c.method === "DELETE" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-4") + ); + assert.ok(deleteCall); +}); + test("ingestion-function create uses expected endpoint and payload shape", async () => { const envDir = await mkEnvDir("compose-contract-ingestion-create-"); await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); @@ -697,6 +737,43 @@ test("webhook update uses expected command endpoints and payload shapes", async }); }); +test("webhook delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-webhook-delete-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const deleteCall = calls.find( + (c) => c.method === "DELETE" && c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create") + ); + assert.ok(deleteCall); +}); + test("type-schema update uses raw schema body at update-schema command endpoint", async () => { const envDir = await mkEnvDir("compose-contract-type-schema-"); await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); From d2406e00e701a877c37f65e51b4cc0649e9127d5 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 09:52:09 -0500 Subject: [PATCH 21/47] Improve convergence support for collections and type schemas --- docs/commands.md | 2 + docs/contracts/apply-contract.json | 10 ++ docs/testing.md | 4 +- src/compose/apply.ts | 61 +++++++++++ test/commands.generate-apply-flow.test.ts | 3 + .../commands.generate-apply-warn-flow.test.ts | 3 + test/commands.generate-flow.test.ts | 3 + test/contracts.apply-contract-map.test.ts | 102 ++++++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 76 +++++++++++++ 9 files changed, 263 insertions(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 6cc2d99..d4ab4b9 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -142,6 +142,8 @@ Alias routing semantics: - omitting `--env` applies all configured environments - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output + - collections support create/delete convergence + - type schemas support create/update-schema/delete convergence - ingestion functions support update-by-diff for script and description (via update commands) - persisted documents support create/update/delete convergence - webhooks support create/update/delete convergence diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index 607b064..cf402f6 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -16,6 +16,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections", "requiredBodyKeys": ["collectionAlias", "description"] }, + "collectionDelete": { + "method": "DELETE", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections/{collectionAlias}", + "requiredBodyKeys": [] + }, "typeSchemaCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas", @@ -26,6 +31,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}/commands/update-schema", "requiredBodyKeys": [] }, + "typeSchemaDelete": { + "method": "DELETE", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}", + "requiredBodyKeys": [] + }, "ingestionFunctionCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion", diff --git a/docs/testing.md b/docs/testing.md index 5421925..731807f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -102,6 +102,7 @@ - contract-shape assertions for key apply endpoints and payloads: - environment create path + `environmentAlias` payload - collection create path + payload (`collectionAlias`, `description`) + - collection delete endpoint shape - environment rename path + `newEnvironmentAlias` payload - persisted document create payload uses `document` field - persisted document create emits string `description` even when local description is omitted @@ -114,11 +115,12 @@ - webhook delete endpoint shape - type-schema update-schema endpoint receives raw schema JSON body - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) + - type-schema delete endpoint shape - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes ingestion, persisted-document, and webhook update/delete contract-map assertions + - includes collection/type-schema delete plus ingestion/persisted-document/webhook update/delete contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 40a64a7..39f423d 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -223,6 +223,7 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P if (!(await exists(filePath))) return out; const desired = await readJsonFile(filePath); + const desiredAliases = new Set(desired.collections.map((c) => c.alias)); // list existing const listPath = `${envBase(opts)}/collections`; @@ -286,6 +287,24 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P } } + for (const alias of existingAliases.keys()) { + if (desiredAliases.has(alias)) continue; + out.push({ kind: "delete", env: opEnv, resource: "collection", alias }); + if (!opts.planOnly) { + const deletePath = `${envBase(opts)}/collections/${encodeURIComponent(alias)}`; + const deleteRes = await client.delete(deletePath); + if (deleteRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias, + details: `Delete failed (${deleteRes.status}).` + }); + } + } + } + return out; } @@ -317,10 +336,12 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P if (!(await exists(dir))) return out; const files = (await fs.readdir(dir)).filter((f) => f.endsWith(".schema.json")); + const desiredAliases = new Set(); for (const f of files) { const p = path.join(dir, f); const desired = await readJsonFile(p); const alias = desired.alias; + desiredAliases.add(alias); const getPath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}`; const existing = await client.get(getPath); @@ -399,6 +420,46 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P // For now: ignore desc updates unless we confirm the exact command path. } + const listPath = `${envBase(opts)}/type-schemas`; + const listRes = await client.get(listPath); + if (listRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias: "*", + details: `Failed to list type schemas (${listRes.status}).` + }); + return out; + } + + const listItems = + listRes.data?.items ?? + listRes.data?.typeSchemas ?? + listRes.data?.edges?.map((e: any) => e?.node) ?? + listRes.data?.nodes ?? + []; + + for (const item of listItems) { + const alias = item?.typeSchemaAlias ?? item?.alias; + if (typeof alias !== "string" || desiredAliases.has(alias)) continue; + + out.push({ kind: "delete", env: opEnv, resource: "type-schema", alias }); + if (!opts.planOnly) { + const deletePath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}`; + const deleteRes = await client.delete(deletePath); + if (deleteRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Delete failed (${deleteRes.status}).` + }); + } + } + } + return out; } diff --git a/test/commands.generate-apply-flow.test.ts b/test/commands.generate-apply-flow.test.ts index cdb60bb..583aa0e 100644 --- a/test/commands.generate-apply-flow.test.ts +++ b/test/commands.generate-apply-flow.test.ts @@ -105,6 +105,9 @@ test("generate -> apply writes lock/state and reports expected apply operations" if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { return jsonResponse(404, { error: "not found" }); } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [] }); + } if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { return jsonResponse(201, { typeSchemaAlias: "article" }); } diff --git a/test/commands.generate-apply-warn-flow.test.ts b/test/commands.generate-apply-warn-flow.test.ts index 5aaf6de..f145060 100644 --- a/test/commands.generate-apply-warn-flow.test.ts +++ b/test/commands.generate-apply-warn-flow.test.ts @@ -105,6 +105,9 @@ test("generate -> apply warning path skips lock/state writes", async () => { if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { return jsonResponse(404, { error: "not found" }); } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [] }); + } if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { return jsonResponse(201, { typeSchemaAlias: "article" }); } diff --git a/test/commands.generate-flow.test.ts b/test/commands.generate-flow.test.ts index 903d934..2033183 100644 --- a/test/commands.generate-flow.test.ts +++ b/test/commands.generate-flow.test.ts @@ -115,6 +115,9 @@ test("generate -> validate --strict -> plan baseline succeeds with expected oper if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { return jsonResponse(404, { error: "not found" }); } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [] }); + } if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { return jsonResponse(200, { edges: [] }); } diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index c7277a2..5a7d699 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -753,3 +753,105 @@ test("webhook delete calls match contract map", async () => { webhookAlias: "send-on-create" }); }); + +test("collection delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-collection-delete-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "collectionDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + collectionAlias: "content" + }); +}); + +test("type-schema delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-type-schema-delete-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "legacy-schema" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/legacy-schema" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "typeSchemaDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + typeSchemaAlias: "legacy-schema" + }); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index e19dc76..b52c90a 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -124,6 +124,43 @@ test("collection create uses expected endpoint and payload shape", async () => { assert.equal(body.description, "Main content"); }); +test("collection delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-collection-delete-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections") { + if (method === "GET") return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const deleteCall = calls.find( + (c) => c.method === "DELETE" && c.url.includes("/v1/projects/proj/environments/dev/collections/content") + ); + assert.ok(deleteCall); +}); + test("environment rename uses expected endpoint and payload key", async () => { const envDir = await mkEnvDir("compose-contract-rename-env-"); const { calls, restore } = installFetchMock((u, method) => { @@ -904,3 +941,42 @@ test("type-schema create uses expected endpoint and payload shape", async () => assert.equal(body.description, "Software schema"); assert.deepEqual(body.schema, desiredSchema); }); + +test("type-schema delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-delete-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "legacy-schema" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/legacy-schema" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const deleteCall = calls.find( + (c) => + c.method === "DELETE" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/legacy-schema") + ); + assert.ok(deleteCall); +}); From 5bb9b19286f1e25bbb5b996761d1bb677c6c392d Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 09:56:56 -0500 Subject: [PATCH 22/47] Support delete convergence for ingestion functions --- docs/commands.md | 2 +- docs/contracts/apply-contract.json | 5 +++ docs/testing.md | 3 +- src/compose/apply.ts | 19 ++++++++ test/contracts.apply-contract-map.test.ts | 52 ++++++++++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 43 ++++++++++++++++++ 6 files changed, 122 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index d4ab4b9..1aa0473 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -144,7 +144,7 @@ Alias routing semantics: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - collections support create/delete convergence - type schemas support create/update-schema/delete convergence - - ingestion functions support update-by-diff for script and description (via update commands) + - ingestion functions support create/update/delete convergence - persisted documents support create/update/delete convergence - webhooks support create/update/delete convergence - if any warning is emitted, lock/state writes are skipped for safety diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index cf402f6..c50645d 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -51,6 +51,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}/commands/update-description", "requiredBodyKeys": ["newDescription"] }, + "ingestionFunctionDelete": { + "method": "DELETE", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}", + "requiredBodyKeys": [] + }, "persistedDocumentCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents", diff --git a/docs/testing.md b/docs/testing.md index 731807f..c707949 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -110,6 +110,7 @@ - persisted-document delete endpoint shape - ingestion-function create path + payload (`ingestionFunctionAlias`, `description`, `script`) - ingestion-function update commands (`update-script`, `update-description`) payload shape + - ingestion-function delete endpoint shape - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) - webhook update commands (`update-url`, `update-event-types`, `update-collections`, `update-type-schemas`, `update-headers`) payload shapes - webhook delete endpoint shape @@ -120,7 +121,7 @@ - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes collection/type-schema delete plus ingestion/persisted-document/webhook update/delete contract-map assertions + - includes collection/type-schema plus ingestion/persisted-document/webhook update/delete contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 39f423d..a82c890 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -476,6 +476,7 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti if (!(await exists(registryPath))) return out; const reg = await readJsonFile(registryPath); + const desiredAliases = new Set(reg.functions.map((fn) => fn.alias)); // List existing const listPath = `${envBase(opts)}/functions/ingestion`; @@ -602,6 +603,24 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti } } + for (const alias of existingAliases.keys()) { + if (desiredAliases.has(alias)) continue; + out.push({ kind: "delete", env: opEnv, resource: "ingestion-function", alias }); + if (!opts.planOnly) { + const deletePath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}`; + const deleteRes = await client.delete(deletePath); + if (deleteRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Delete failed (${deleteRes.status}).` + }); + } + } + } + return out; } diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index 5a7d699..04dcf47 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -436,6 +436,58 @@ test("ingestion update calls match contract map", async () => { }); }); +test("ingestion delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-ingestion-delete-"); + await fs.mkdir(path.join(envDir, "functions"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "legacy-ingest" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "ingestionFunctionDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + ingestionFunctionAlias: "legacy-ingest" + }); +}); + test("persisted-document update calls match contract map", async () => { const contracts = await readContractMap(); const envDir = await mkEnvDir("compose-contract-map-persisted-update-"); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index b52c90a..e180198 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -580,6 +580,49 @@ test("ingestion-function update uses expected update-script and update-descripti assert.equal(updateDescriptionBody.newDescription, "New description"); }); +test("ingestion-function delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-ingestion-delete-"); + await fs.mkdir(path.join(envDir, "functions"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "legacy-ingest" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest" && + method === "DELETE" + ) { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const deleteCall = calls.find( + (c) => + c.method === "DELETE" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest") + ); + assert.ok(deleteCall); +}); + test("webhook create uses expected endpoint and payload shape", async () => { const envDir = await mkEnvDir("compose-contract-webhook-create-"); await fs.writeFile( From c84a8011f9a2027a4fbaae50e799b9f337c5ea9c Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 11:16:07 -0500 Subject: [PATCH 23/47] Improve parity for updates --- docs/commands.md | 4 +- docs/contracts/apply-contract.json | 15 +++ docs/testing.md | 8 +- src/compose/apply.ts | 92 ++++++++++++- test/contracts.apply-contract-map.test.ts | 146 +++++++++++++++++++++ test/contracts.apply-openapi-shape.test.ts | 138 +++++++++++++++++++ 6 files changed, 392 insertions(+), 11 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 1aa0473..2ad0c69 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -142,8 +142,8 @@ Alias routing semantics: - omitting `--env` applies all configured environments - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - - collections support create/delete convergence - - type schemas support create/update-schema/delete convergence + - collections support create/update-description/delete convergence + - type schemas support create/update-schema/update-description/delete convergence - ingestion functions support create/update/delete convergence - persisted documents support create/update/delete convergence - webhooks support create/update/delete convergence diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index c50645d..1b75480 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -21,6 +21,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections/{collectionAlias}", "requiredBodyKeys": [] }, + "collectionUpdateDescription": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections/{collectionAlias}/commands/update-description", + "requiredBodyKeys": ["newDescription"] + }, "typeSchemaCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas", @@ -31,6 +36,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}/commands/update-schema", "requiredBodyKeys": [] }, + "typeSchemaUpdateDescription": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}/commands/update-description", + "requiredBodyKeys": ["newDescription"] + }, "typeSchemaDelete": { "method": "DELETE", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}", @@ -113,6 +123,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-headers", "requiredBodyKeys": ["newHeaders"] }, + "webhookUpdateDescription": { + "method": "PUT", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/update-description", + "requiredBodyKeys": ["newDescription"] + }, "webhookDelete": { "method": "DELETE", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}", diff --git a/docs/testing.md b/docs/testing.md index c707949..030b857 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -102,6 +102,7 @@ - contract-shape assertions for key apply endpoints and payloads: - environment create path + `environmentAlias` payload - collection create path + payload (`collectionAlias`, `description`) + - collection `update-description` command payload shape (`newDescription`) - collection delete endpoint shape - environment rename path + `newEnvironmentAlias` payload - persisted document create payload uses `document` field @@ -112,16 +113,17 @@ - ingestion-function update commands (`update-script`, `update-description`) payload shape - ingestion-function delete endpoint shape - webhook create path + payload (`webhookAlias`, `url`, `eventTypes`, filters, `customHeaders`) - - webhook update commands (`update-url`, `update-event-types`, `update-collections`, `update-type-schemas`, `update-headers`) payload shapes + - webhook update commands (`update-description`, `update-url`, `update-event-types`, `update-collections`, `update-type-schemas`, `update-headers`) payload shapes - webhook delete endpoint shape - - type-schema update-schema endpoint receives raw schema JSON body + - type-schema `update-schema` endpoint receives raw schema JSON body + - type-schema `update-description` command payload shape (`newDescription`) - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) - type-schema delete endpoint shape - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes collection/type-schema plus ingestion/persisted-document/webhook update/delete contract-map assertions + - includes collection/type-schema/ingestion/persisted-document/webhook update/delete contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: diff --git a/src/compose/apply.ts b/src/compose/apply.ts index a82c890..d6806b6 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -265,11 +265,12 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P for (const c of desired.collections) { const alias = c.alias; const found = existingAliases.get(alias); + const desiredDescription = c.description ?? null; if (!found) { out.push({ kind: "create", env: opEnv, resource: "collection", alias }); if (!opts.planOnly) { const createPath = `${envBase(opts)}/collections`; - const body = { collectionAlias: alias, description: c.description ?? null }; + const body = { collectionAlias: alias, description: desiredDescription }; const res = await client.post(createPath, body); if (res.status >= 400) { out.push({ @@ -282,8 +283,51 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P } } } else { - // description update path varies; for MVP we no-op unless missing - out.push({ kind: "noop", env: opEnv, resource: "collection", alias }); + const getPath = `${envBase(opts)}/collections/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + + const remoteDescription = existing.data?.description ?? existing.data?.collection?.description ?? null; + const descriptionChanged = (remoteDescription ?? null) !== desiredDescription; + + if (!descriptionChanged) { + out.push({ kind: "noop", env: opEnv, resource: "collection", alias }); + continue; + } + + out.push({ + kind: "update", + env: opEnv, + resource: "collection", + alias, + details: "description" + }); + + if (opts.planOnly) continue; + + const updateDescriptionPath = + `${envBase(opts)}/collections/${encodeURIComponent(alias)}/commands/update-description`; + const updateDescriptionRes = await client.put(updateDescriptionPath, { + newDescription: desiredDescription + }); + if (updateDescriptionRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias, + details: `Update description failed (${updateDescriptionRes.status}).` + }); + } } } @@ -416,8 +460,21 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P } } - // description update endpoint name can vary; if present in spec, add later. - // For now: ignore desc updates unless we confirm the exact command path. + if (descChanged) { + const updDescPath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}/commands/update-description`; + const res = await client.put(updDescPath, { + newDescription: desired.description ?? null + }); + if (res.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Update description failed (${res.status}).` + }); + } + } } const listPath = `${envBase(opts)}/type-schemas`; @@ -793,6 +850,7 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): type WebhooksFile = { webhooks: Array<{ alias: string; + description?: string | null; url: string; eventTypes: string[]; collectionAliases?: string[]; @@ -844,6 +902,7 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom const createPath = `${envBase(opts)}/webhooks`; const body: any = { webhookAlias: alias, + description: w.description ?? null, url: w.url, eventTypes: w.eventTypes, collectionAliases: w.collectionAliases ?? [], @@ -888,20 +947,23 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom const remoteHeaders = normalizeStringMap( existing.data?.customHeaders ?? existing.data?.webhook?.customHeaders ?? {} ); + const remoteDescription = existing.data?.description ?? existing.data?.webhook?.description ?? null; + const desiredDescription = w.description ?? null; const desiredUrl = w.url; const desiredEventTypes = normalizeStringArray(w.eventTypes ?? []); const desiredCollections = normalizeStringArray(w.collectionAliases ?? []); const desiredTypeSchemas = normalizeStringArray(w.typeSchemaAliases ?? []); const desiredHeaders = normalizeStringMap(w.customHeaders ?? {}); + const descriptionChanged = (remoteDescription ?? null) !== desiredDescription; const urlChanged = remoteUrl !== desiredUrl; const eventTypesChanged = stringify(remoteEventTypes) !== stringify(desiredEventTypes); const collectionsChanged = stringify(remoteCollections) !== stringify(desiredCollections); const typeSchemasChanged = stringify(remoteTypeSchemas) !== stringify(desiredTypeSchemas); const headersChanged = stringify(remoteHeaders) !== stringify(desiredHeaders); - if (!urlChanged && !eventTypesChanged && !collectionsChanged && !typeSchemasChanged && !headersChanged) { + if (!descriptionChanged && !urlChanged && !eventTypesChanged && !collectionsChanged && !typeSchemasChanged && !headersChanged) { out.push({ kind: "noop", env: opEnv, resource: "webhook", alias }); continue; } @@ -912,6 +974,7 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom resource: "webhook", alias, details: [ + descriptionChanged ? "description" : "", urlChanged ? "url" : "", eventTypesChanged ? "eventTypes" : "", collectionsChanged ? "collections" : "", @@ -924,6 +987,23 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom if (opts.planOnly) continue; + if (descriptionChanged) { + const updateDescriptionPath = + `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-description`; + const updateDescriptionRes = await client.put(updateDescriptionPath, { + newDescription: desiredDescription + }); + if (updateDescriptionRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Update description failed (${updateDescriptionRes.status}).` + }); + } + } + if (urlChanged) { const updateUrlPath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}/commands/update-url`; const updateUrlRes = await client.put(updateUrlPath, { url: desiredUrl }); diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index 04dcf47..d079fbb 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -436,6 +436,143 @@ test("ingestion update calls match contract map", async () => { }); }); +test("collection update-description calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-collection-update-desc-"); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content", description: "New description" }] }, null, 2), + "utf8" + ); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") body = JSON.parse(init.body); + if (init?.body instanceof URLSearchParams) body = init.body.toString(); + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content", description: "Old description" }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/collections/content/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "collectionUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + collectionAlias: "content" + }); +}); + +test("type-schema update-description calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-type-schema-update-desc-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.writeFile( + path.join(envDir, "type-schemas", "article.schema.json"), + JSON.stringify( + { + alias: "article", + description: "New type-schema description", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, + null, + 2 + ), + "utf8" + ); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") body = JSON.parse(init.body); + if (init?.body instanceof URLSearchParams) body = init.body.toString(); + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Old type-schema description", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + assertContractCall(contracts, "typeSchemaUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + typeSchemaAlias: "article" + }); +}); + test("ingestion delete calls match contract map", async () => { const contracts = await readContractMap(); const envDir = await mkEnvDir("compose-contract-map-ingestion-delete-"); @@ -592,6 +729,7 @@ test("webhook update calls match contract map", async () => { webhooks: [ { alias: "send-on-create", + description: "New webhook description", url: "https://example.com/new", eventTypes: ["content.deleted"], collectionAliases: ["articles"], @@ -630,6 +768,7 @@ test("webhook update calls match contract map", async () => { if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { return jsonResponse(200, { webhookAlias: "send-on-create", + description: "Old webhook description", url: "https://example.com/old", eventTypes: ["content.ingested"], collectionAliases: ["content"], @@ -637,6 +776,12 @@ test("webhook update calls match contract map", async () => { customHeaders: { authorization: "Bearer old" } }); } + if ( + u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } if ( u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && method === "PUT" @@ -692,6 +837,7 @@ test("webhook update calls match contract map", async () => { environmentAlias: "dev", webhookAlias: "send-on-create" }; + assertContractCall(contracts, "webhookUpdateDescription", calls, params); assertContractCall(contracts, "webhookUpdateUrl", calls, params); assertContractCall(contracts, "webhookUpdateEventTypes", calls, params); assertContractCall(contracts, "webhookUpdateCollections", calls, params); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index e180198..39c3414 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -124,6 +124,57 @@ test("collection create uses expected endpoint and payload shape", async () => { assert.equal(body.description, "Main content"); }); +test("collection update uses expected update-description command payload", async () => { + const envDir = await mkEnvDir("compose-contract-collection-update-desc-"); + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content", description: "New description" }] }, null, 2), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content", description: "Old description" }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/collections/content/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const updateCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/collections/content/commands/update-description") + ); + assert.ok(updateCall); + const body = JSON.parse(updateCall.bodyText ?? "{}") as Record; + assert.equal(body.newDescription, "New description"); +}); + test("collection delete uses expected endpoint", async () => { const envDir = await mkEnvDir("compose-contract-collection-delete-"); await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); @@ -693,6 +744,7 @@ test("webhook update uses expected command endpoints and payload shapes", async webhooks: [ { alias: "send-on-create", + description: "New webhook description", url: "https://example.com/new", eventTypes: ["content.deleted"], collectionAliases: ["articles"], @@ -717,6 +769,7 @@ test("webhook update uses expected command endpoints and payload shapes", async if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { return jsonResponse(200, { webhookAlias: "send-on-create", + description: "Old webhook description", url: "https://example.com/old", eventTypes: ["content.ingested"], collectionAliases: ["content"], @@ -724,6 +777,12 @@ test("webhook update uses expected command endpoints and payload shapes", async customHeaders: { authorization: "Bearer old" } }); } + if ( + u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } if ( u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && method === "PUT" @@ -774,6 +833,14 @@ test("webhook update uses expected command endpoints and payload shapes", async restore(); } + const updateDescriptionCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description") + ); + assert.ok(updateDescriptionCall); + assert.deepEqual(JSON.parse(updateDescriptionCall.bodyText ?? "{}"), { newDescription: "New webhook description" }); + const updateUrlCall = calls.find( (c) => c.method === "PUT" && @@ -924,6 +991,77 @@ test("type-schema update uses raw schema body at update-schema command endpoint" assert.equal("schema" in body, false); }); +test("type-schema update uses expected update-description command payload", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-update-desc-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + + const desiredSchema = { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + }; + + await fs.writeFile( + path.join(envDir, "type-schemas", "article.schema.json"), + JSON.stringify( + { + alias: "article", + description: "New description", + schema: desiredSchema + }, + null, + 2 + ), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Old description", + schema: desiredSchema + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description" && + method === "PUT" + ) { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const updateCall = calls.find( + (c) => + c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description") + ); + assert.ok(updateCall); + const body = JSON.parse(updateCall.bodyText ?? "{}") as Record; + assert.equal(body.newDescription, "New description"); +}); + test("type-schema create uses expected endpoint and payload shape", async () => { const envDir = await mkEnvDir("compose-contract-type-schema-create-"); await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); From 635574cb97b98b257f05562646d43dda8a26ea29 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 11:50:31 -0500 Subject: [PATCH 24/47] Add negative rename inference protections. --- docs/commands.md | 10 +- docs/contracts/apply-contract.json | 25 ++ docs/testing.md | 14 +- src/compose/apply.ts | 425 ++++++++++++++++++-- test/compose.apply.rename-inference.test.ts | 151 +++++++ test/contracts.apply-contract-map.test.ts | 244 +++++++++++ test/contracts.apply-openapi-shape.test.ts | 253 ++++++++++++ 7 files changed, 1075 insertions(+), 47 deletions(-) create mode 100644 test/compose.apply.rename-inference.test.ts diff --git a/docs/commands.md b/docs/commands.md index 2ad0c69..b82cc62 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -142,11 +142,11 @@ Alias routing semantics: - omitting `--env` applies all configured environments - Notes: - during create/rename convergence, a `404` when listing collections is treated as non-fatal with contextual output - - collections support create/update-description/delete convergence - - type schemas support create/update-schema/update-description/delete convergence - - ingestion functions support create/update/delete convergence - - persisted documents support create/update/delete convergence - - webhooks support create/update/delete convergence + - collections support create/rename/update-description/delete convergence + - type schemas support create/rename/update-schema/update-description/delete convergence + - ingestion functions support create/rename/update/delete convergence + - persisted documents support create/rename/update/delete convergence + - webhooks support create/rename/update/delete convergence - if any warning is emitted, lock/state writes are skipped for safety - Side effects: remote writes + local lock/state updates. - Idempotency: rerun after successful apply should converge to mostly noops. diff --git a/docs/contracts/apply-contract.json b/docs/contracts/apply-contract.json index 1b75480..52c4f8f 100644 --- a/docs/contracts/apply-contract.json +++ b/docs/contracts/apply-contract.json @@ -26,6 +26,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections/{collectionAlias}/commands/update-description", "requiredBodyKeys": ["newDescription"] }, + "collectionRename": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/collections/{collectionAlias}/commands/rename", + "requiredBodyKeys": ["newCollectionAlias"] + }, "typeSchemaCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas", @@ -46,6 +51,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}", "requiredBodyKeys": [] }, + "typeSchemaRename": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/type-schemas/{typeSchemaAlias}/commands/rename", + "requiredBodyKeys": ["newTypeSchemaAlias"] + }, "ingestionFunctionCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion", @@ -66,6 +76,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}", "requiredBodyKeys": [] }, + "ingestionFunctionRename": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/functions/ingestion/{ingestionFunctionAlias}/commands/rename", + "requiredBodyKeys": ["newIngestionFunctionAlias"] + }, "persistedDocumentCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents", @@ -86,6 +101,11 @@ "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}", "requiredBodyKeys": [] }, + "persistedDocumentRename": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/graphql/persisted-documents/{persistedDocumentAlias}/commands/rename", + "requiredBodyKeys": ["newPersistedDocumentAlias"] + }, "webhookCreate": { "method": "POST", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks", @@ -132,6 +152,11 @@ "method": "DELETE", "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}", "requiredBodyKeys": [] + }, + "webhookRename": { + "method": "POST", + "pathTemplate": "/v1/projects/{projectAlias}/environments/{environmentAlias}/webhooks/{webhookAlias}/commands/rename", + "requiredBodyKeys": ["newWebhookAlias"] } } } diff --git a/docs/testing.md b/docs/testing.md index 030b857..0f8e436 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -35,6 +35,12 @@ - verifies environment list failure produces environment warning and short-circuits nested operations - verifies environment rename failure (e.g. `409`) produces warning with status and short-circuits nested operations +- `test/compose.apply.rename-inference.test.ts` + - verifies inferred non-environment rename guardrails: + - ambiguous signature matches do not trigger rename calls + - signature mismatches do not trigger rename calls + - fallback behavior remains create/delete convergence + - `test/commands.plan-exit.test.ts` - verifies plan mode sets `process.exitCode = 1` when warnings are emitted @@ -119,11 +125,17 @@ - type-schema `update-description` command payload shape (`newDescription`) - type-schema create path + payload (`typeSchemaAlias`, `description`, `schema`) - type-schema delete endpoint shape + - non-environment rename commands/payload keys: + - collection `commands/rename` + `newCollectionAlias` + - type schema `commands/rename` + `newTypeSchemaAlias` + - ingestion function `commands/rename` + `newIngestionFunctionAlias` + - persisted document `commands/rename` + `newPersistedDocumentAlias` + - webhook `commands/rename` + `newWebhookAlias` - `test/contracts.apply-contract-map.test.ts` - validates emitted apply write calls against `docs/contracts/apply-contract.json` - catches drift between documented endpoint/payload contract and implementation - - includes collection/type-schema/ingestion/persisted-document/webhook update/delete contract-map assertions + - includes collection/type-schema/ingestion/persisted-document/webhook update/delete/rename contract-map assertions - `test/commands.generate.test.ts` - verifies `compose generate` creates/updates local files for: diff --git a/src/compose/apply.ts b/src/compose/apply.ts index d6806b6..5641f99 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -262,10 +262,62 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P if (typeof alias === "string") existingAliases.set(alias, c); } + const unmatchedDesired = desired.collections.filter((c) => !existingAliases.has(c.alias)); + const unmatchedRemoteAliases = [...existingAliases.keys()].filter((alias) => !desiredAliases.has(alias)); + const desiredRenameCandidates = unmatchedDesired.map((c) => ({ + alias: c.alias, + signature: stringify({ description: c.description ?? null }) + })); + const remoteRenameCandidates: Array<{ alias: string; signature: string }> = []; + for (const alias of unmatchedRemoteAliases) { + const getPath = `${envBase(opts)}/collections/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + remoteRenameCandidates.push({ + alias, + signature: stringify({ description: existing.data?.description ?? existing.data?.collection?.description ?? null }) + }); + } + const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renamedFrom = new Set(renameFromByTo.values()); + for (const c of desired.collections) { const alias = c.alias; const found = existingAliases.get(alias); const desiredDescription = c.description ?? null; + const renameFrom = renameFromByTo.get(alias); + if (renameFrom) { + out.push({ + kind: "update", + env: opEnv, + resource: "collection", + alias, + details: `rename ${renameFrom} -> ${alias}` + }); + if (!opts.planOnly) { + const renamePath = `${envBase(opts)}/collections/${encodeURIComponent(renameFrom)}/commands/rename`; + const renameRes = await client.post(renamePath, { newCollectionAlias: alias }); + if (renameRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "collection", + alias, + details: `Rename failed (${renameRes.status}).` + }); + } + } + continue; + } if (!found) { out.push({ kind: "create", env: opEnv, resource: "collection", alias }); if (!opts.planOnly) { @@ -332,7 +384,7 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P } for (const alias of existingAliases.keys()) { - if (desiredAliases.has(alias)) continue; + if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; out.push({ kind: "delete", env: opEnv, resource: "collection", alias }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/collections/${encodeURIComponent(alias)}`; @@ -380,12 +432,94 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P if (!(await exists(dir))) return out; const files = (await fs.readdir(dir)).filter((f) => f.endsWith(".schema.json")); - const desiredAliases = new Set(); + const desiredEntries: TypeSchemaFile[] = []; for (const f of files) { const p = path.join(dir, f); - const desired = await readJsonFile(p); + desiredEntries.push(await readJsonFile(p)); + } + const desiredAliases = new Set(desiredEntries.map((d) => d.alias)); + + const listPath = `${envBase(opts)}/type-schemas`; + const listRes = await client.get(listPath); + if (listRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias: "*", + details: `Failed to list type schemas (${listRes.status}).` + }); + return out; + } + + const listItems = + listRes.data?.items ?? + listRes.data?.typeSchemas ?? + listRes.data?.edges?.map((e: any) => e?.node) ?? + listRes.data?.nodes ?? + []; + const existingAliases = new Set(); + for (const item of listItems) { + const alias = item?.typeSchemaAlias ?? item?.alias; + if (typeof alias === "string") existingAliases.add(alias); + } + + const unmatchedDesired = desiredEntries.filter((d) => !existingAliases.has(d.alias)); + const unmatchedRemoteAliases = [...existingAliases].filter((alias) => !desiredAliases.has(alias)); + const desiredRenameCandidates = unmatchedDesired.map((d) => ({ + alias: d.alias, + signature: stringify({ schema: d.schema, description: d.description ?? null }) + })); + const remoteRenameCandidates: Array<{ alias: string; signature: string }> = []; + for (const alias of unmatchedRemoteAliases) { + const getPath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + const remoteSchema = existing.data?.schema ?? existing.data?.jsonSchema ?? existing.data?.typeSchema?.schema; + const remoteDesc = existing.data?.description ?? existing.data?.typeSchema?.description ?? null; + remoteRenameCandidates.push({ + alias, + signature: stringify({ schema: remoteSchema, description: remoteDesc ?? null }) + }); + } + const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renamedFrom = new Set(renameFromByTo.values()); + + for (const desired of desiredEntries) { const alias = desired.alias; - desiredAliases.add(alias); + const renameFrom = renameFromByTo.get(alias); + if (renameFrom) { + out.push({ + kind: "update", + env: opEnv, + resource: "type-schema", + alias, + details: `rename ${renameFrom} -> ${alias}` + }); + if (!opts.planOnly) { + const renamePath = `${envBase(opts)}/type-schemas/${encodeURIComponent(renameFrom)}/commands/rename`; + const renameRes = await client.post(renamePath, { newTypeSchemaAlias: alias }); + if (renameRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "type-schema", + alias, + details: `Rename failed (${renameRes.status}).` + }); + } + } + continue; + } const getPath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}`; const existing = await client.get(getPath); @@ -477,29 +611,9 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P } } - const listPath = `${envBase(opts)}/type-schemas`; - const listRes = await client.get(listPath); - if (listRes.status >= 400) { - out.push({ - kind: "warn", - env: opEnv, - resource: "type-schema", - alias: "*", - details: `Failed to list type schemas (${listRes.status}).` - }); - return out; - } - - const listItems = - listRes.data?.items ?? - listRes.data?.typeSchemas ?? - listRes.data?.edges?.map((e: any) => e?.node) ?? - listRes.data?.nodes ?? - []; - for (const item of listItems) { const alias = item?.typeSchemaAlias ?? item?.alias; - if (typeof alias !== "string" || desiredAliases.has(alias)) continue; + if (typeof alias !== "string" || desiredAliases.has(alias) || renamedFrom.has(alias)) continue; out.push({ kind: "delete", env: opEnv, resource: "type-schema", alias }); if (!opts.planOnly) { @@ -533,7 +647,18 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti if (!(await exists(registryPath))) return out; const reg = await readJsonFile(registryPath); - const desiredAliases = new Set(reg.functions.map((fn) => fn.alias)); + const desiredEntries = await Promise.all( + reg.functions.map(async (fn) => { + const scriptPath = path.resolve(path.join(opts.envDir, fn.scriptFile)); + const script = await fs.readFile(scriptPath, "utf8"); + return { + alias: fn.alias, + description: fn.description ?? "", + script + }; + }) + ); + const desiredAliases = new Set(desiredEntries.map((fn) => fn.alias)); // List existing const listPath = `${envBase(opts)}/functions/ingestion`; @@ -562,10 +687,65 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti if (typeof alias === "string") existingAliases.set(alias, fn); } - for (const fn of reg.functions) { + const unmatchedDesired = desiredEntries.filter((fn) => !existingAliases.has(fn.alias)); + const unmatchedRemoteAliases = [...existingAliases.keys()].filter((alias) => !desiredAliases.has(alias)); + const desiredRenameCandidates = unmatchedDesired.map((fn) => ({ + alias: fn.alias, + signature: stringify({ description: fn.description, script: fn.script }) + })); + const remoteRenameCandidates: Array<{ alias: string; signature: string }> = []; + for (const alias of unmatchedRemoteAliases) { + const getPath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + remoteRenameCandidates.push({ + alias, + signature: stringify({ + description: String(existing.data?.description ?? existing.data?.ingestionFunction?.description ?? ""), + script: String(existing.data?.script ?? existing.data?.ingestionFunction?.script ?? "") + }) + }); + } + const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renamedFrom = new Set(renameFromByTo.values()); + + for (const fn of desiredEntries) { const alias = fn.alias; - const scriptPath = path.resolve(path.join(opts.envDir, fn.scriptFile)); - const script = await fs.readFile(scriptPath, "utf8"); + const script = fn.script; + const desiredDescription = fn.description; + const renameFrom = renameFromByTo.get(alias); + if (renameFrom) { + out.push({ + kind: "update", + env: opEnv, + resource: "ingestion-function", + alias, + details: `rename ${renameFrom} -> ${alias}` + }); + if (!opts.planOnly) { + const renamePath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(renameFrom)}/commands/rename`; + const renameRes = await client.post(renamePath, { newIngestionFunctionAlias: alias }); + if (renameRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "ingestion-function", + alias, + details: `Rename failed (${renameRes.status}).` + }); + } + } + continue; + } const found = existingAliases.get(alias); if (!found) { @@ -574,7 +754,7 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti const createPath = `${envBase(opts)}/functions/ingestion`; const body = { ingestionFunctionAlias: alias, - description: fn.description ?? null, + description: desiredDescription, script }; const res = await client.post(createPath, body); @@ -606,8 +786,6 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti const remoteDescription = String( existing.data?.description ?? existing.data?.ingestionFunction?.description ?? "" ); - const desiredDescription = fn.description ?? ""; - const scriptChanged = remoteScript !== script; const descriptionChanged = remoteDescription !== desiredDescription; @@ -661,7 +839,7 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti } for (const alias of existingAliases.keys()) { - if (desiredAliases.has(alias)) continue; + if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; out.push({ kind: "delete", env: opEnv, resource: "ingestion-function", alias }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}`; @@ -694,7 +872,18 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): if (!(await exists(registryPath))) return out; const reg = await readJsonFile(registryPath); - const desiredAliases = new Set(reg.documents.map((d) => d.alias)); + const desiredEntries = await Promise.all( + reg.documents.map(async (d) => { + const queryPath = path.resolve(path.join(opts.envDir, d.queryFile)); + const query = await fs.readFile(queryPath, "utf8"); + return { + alias: d.alias, + description: d.description ?? "", + document: query + }; + }) + ); + const desiredAliases = new Set(desiredEntries.map((d) => d.alias)); const listPath = `${envBase(opts)}/graphql/persisted-documents`; const existingRes = await client.get(listPath); @@ -723,10 +912,65 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): if (typeof alias === "string") existingAliases.add(alias); } - for (const d of reg.documents) { + const unmatchedDesired = desiredEntries.filter((d) => !existingAliases.has(d.alias)); + const unmatchedRemoteAliases = [...existingAliases].filter((alias) => !desiredAliases.has(alias)); + const desiredRenameCandidates = unmatchedDesired.map((d) => ({ + alias: d.alias, + signature: stringify({ description: d.description, document: d.document }) + })); + const remoteRenameCandidates: Array<{ alias: string; signature: string }> = []; + for (const alias of unmatchedRemoteAliases) { + const getPath = `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + remoteRenameCandidates.push({ + alias, + signature: stringify({ + description: String(existing.data?.description ?? existing.data?.persistedDocument?.description ?? ""), + document: String(existing.data?.document ?? existing.data?.query ?? existing.data?.persistedDocument?.document ?? "") + }) + }); + } + const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renamedFrom = new Set(renameFromByTo.values()); + + for (const d of desiredEntries) { const alias = d.alias; - const queryPath = path.resolve(path.join(opts.envDir, d.queryFile)); - const query = await fs.readFile(queryPath, "utf8"); + const query = d.document; + const renameFrom = renameFromByTo.get(alias); + if (renameFrom) { + out.push({ + kind: "update", + env: opEnv, + resource: "persisted-doc", + alias, + details: `rename ${renameFrom} -> ${alias}` + }); + if (!opts.planOnly) { + const renamePath = + `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(renameFrom)}/commands/rename`; + const renameRes = await client.post(renamePath, { newPersistedDocumentAlias: alias }); + if (renameRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "persisted-doc", + alias, + details: `Rename failed (${renameRes.status}).` + }); + } + } + continue; + } if (!existingAliases.has(alias)) { out.push({ kind: "create", env: opEnv, resource: "persisted-doc", alias }); @@ -734,7 +978,7 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): const createPath = `${envBase(opts)}/graphql/persisted-documents`; const body: any = { persistedDocumentAlias: alias, - description: d.description ?? "", + description: d.description, document: query }; const res = await client.post(createPath, body); @@ -768,7 +1012,7 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): const remoteDescription = String( existing.data?.description ?? existing.data?.persistedDocument?.description ?? "" ); - const desiredDescription = d.description ?? ""; + const desiredDescription = d.description; const documentChanged = remoteDocument !== query; const descriptionChanged = remoteDescription !== desiredDescription; @@ -825,7 +1069,7 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): } for (const alias of existingAliases) { - if (desiredAliases.has(alias)) continue; + if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; out.push({ kind: "delete", env: opEnv, resource: "persisted-doc", alias }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}`; @@ -894,8 +1138,78 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom if (typeof alias === "string") existingAliases.add(alias); } + const unmatchedDesired = desired.webhooks.filter((w) => !existingAliases.has(w.alias)); + const unmatchedRemoteAliases = [...existingAliases].filter((alias) => !desiredAliases.has(alias)); + const desiredRenameCandidates = unmatchedDesired.map((w) => ({ + alias: w.alias, + signature: stringify({ + description: w.description ?? null, + url: w.url, + eventTypes: normalizeStringArray(w.eventTypes ?? []), + collectionAliases: normalizeStringArray(w.collectionAliases ?? []), + typeSchemaAliases: normalizeStringArray(w.typeSchemaAliases ?? []), + customHeaders: normalizeStringMap(w.customHeaders ?? {}) + }) + })); + const remoteRenameCandidates: Array<{ alias: string; signature: string }> = []; + for (const alias of unmatchedRemoteAliases) { + const getPath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}`; + const existing = await client.get(getPath); + if (existing.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Failed to read (${existing.status}).` + }); + continue; + } + remoteRenameCandidates.push({ + alias, + signature: stringify({ + description: existing.data?.description ?? existing.data?.webhook?.description ?? null, + url: String(existing.data?.url ?? existing.data?.webhook?.url ?? ""), + eventTypes: normalizeStringArray(existing.data?.eventTypes ?? existing.data?.webhook?.eventTypes ?? []), + collectionAliases: normalizeStringArray( + existing.data?.collectionAliases ?? existing.data?.webhook?.collectionAliases ?? [] + ), + typeSchemaAliases: normalizeStringArray( + existing.data?.typeSchemaAliases ?? existing.data?.webhook?.typeSchemaAliases ?? [] + ), + customHeaders: normalizeStringMap(existing.data?.customHeaders ?? existing.data?.webhook?.customHeaders ?? {}) + }) + }); + } + const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renamedFrom = new Set(renameFromByTo.values()); + for (const w of desired.webhooks) { const alias = w.alias; + const renameFrom = renameFromByTo.get(alias); + if (renameFrom) { + out.push({ + kind: "update", + env: opEnv, + resource: "webhook", + alias, + details: `rename ${renameFrom} -> ${alias}` + }); + if (!opts.planOnly) { + const renamePath = `${envBase(opts)}/webhooks/${encodeURIComponent(renameFrom)}/commands/rename`; + const renameRes = await client.post(renamePath, { newWebhookAlias: alias }); + if (renameRes.status >= 400) { + out.push({ + kind: "warn", + env: opEnv, + resource: "webhook", + alias, + details: `Rename failed (${renameRes.status}).` + }); + } + } + continue; + } if (!existingAliases.has(alias)) { out.push({ kind: "create", env: opEnv, resource: "webhook", alias }); if (!opts.planOnly) { @@ -1088,7 +1402,7 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom } for (const alias of existingAliases) { - if (desiredAliases.has(alias)) continue; + if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; out.push({ kind: "delete", env: opEnv, resource: "webhook", alias }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}`; @@ -1121,3 +1435,32 @@ function normalizeStringMap(value: unknown): Record { } return out; } + +function computeRenamePairs( + desired: Array<{ alias: string; signature: string }>, + remote: Array<{ alias: string; signature: string }> +): Map { + const desiredBySignature = new Map(); + const remoteBySignature = new Map(); + + for (const d of desired) { + const arr = desiredBySignature.get(d.signature) ?? []; + arr.push(d.alias); + desiredBySignature.set(d.signature, arr); + } + + for (const r of remote) { + const arr = remoteBySignature.get(r.signature) ?? []; + arr.push(r.alias); + remoteBySignature.set(r.signature, arr); + } + + const out = new Map(); + for (const [signature, desiredAliases] of desiredBySignature.entries()) { + const remoteAliases = remoteBySignature.get(signature) ?? []; + if (desiredAliases.length === 1 && remoteAliases.length === 1) { + out.set(desiredAliases[0], remoteAliases[0]); + } + } + return out; +} diff --git a/test/compose.apply.rename-inference.test.ts b/test/compose.apply.rename-inference.test.ts new file mode 100644 index 0000000..13d2291 --- /dev/null +++ b/test/compose.apply.rename-inference.test.ts @@ -0,0 +1,151 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; + +type FetchCall = { + method: string; + url: string; + bodyText?: string; +}; + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} + +async function makeCollectionsEnvDir( + collections: Array<{ alias: string; description?: string | null }> +): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-")); + await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections }, null, 2), "utf8"); + return dir; +} + +test("collection rename inference skips ambiguous matches and falls back to create/delete", async () => { + const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "Same description" }]); + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content-old-a" } }, { node: { collectionAlias: "content-old-b" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-a" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old-a", description: "Same description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-b" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old-b", description: "Same description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { + return jsonResponse(201, { collectionAlias: "content-new" }); + } + if ( + (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-a" || + u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-b") && + method === "DELETE" + ) { + return new Response(null, { status: 204 }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); + assert.equal(ops.filter((o) => o.kind === "delete" && o.resource === "collection").length, 2); + } finally { + globalThis.fetch = oldFetch; + } +}); + +test("collection rename inference skips when signatures do not match and falls back to create/delete", async () => { + const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "New description" }]); + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Old description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { + return jsonResponse(201, { collectionAlias: "content-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); + assert.equal( + ops.some((o) => o.kind === "delete" && o.resource === "collection" && o.alias === "content-old"), + true + ); + } finally { + globalThis.fetch = oldFetch; + } +}); diff --git a/test/contracts.apply-contract-map.test.ts b/test/contracts.apply-contract-map.test.ts index d079fbb..f96ab01 100644 --- a/test/contracts.apply-contract-map.test.ts +++ b/test/contracts.apply-contract-map.test.ts @@ -1053,3 +1053,247 @@ test("type-schema delete calls match contract map", async () => { typeSchemaAlias: "legacy-schema" }); }); + +test("non-environment rename calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-rename-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content-new", description: "Main content" }] }, null, 2), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "type-schemas", "article-new.schema.json"), + JSON.stringify( + { + alias: "article-new", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion.json"), + JSON.stringify( + { + functions: [ + { + alias: "map-content-new", + description: "Maps content", + scriptFile: "functions/ingestion/map-content-new.js" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion", "map-content-new.js"), + "export default (x) => x;", + "utf8" + ); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { + documents: [ + { + alias: "doc-new", + description: "Main query", + queryFile: "graphql/persisted/doc-new.gql" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-new.gql"), "query { ping }", "utf8"); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "hook-new", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + const oldFetch = globalThis.fetch; + const calls: CapturedCall[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body: unknown; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/collections/content-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { collectionAlias: "content-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article-old", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { typeSchemaAlias: "article-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content-old" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old" && + method === "GET" + ) { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content-old", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { ingestionFunctionAlias: "map-content-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-old" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old" && + method === "GET" + ) { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old", + description: "Main query", + document: "query { ping }" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { persistedDocumentAlias: "doc-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename" && method === "POST") { + return jsonResponse(200, { webhookAlias: "hook-new" }); + } + + return jsonResponse(200, { edges: [] }); + }) as typeof fetch; + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + globalThis.fetch = oldFetch; + } + + const params = { projectAlias: "proj", environmentAlias: "dev" }; + assertContractCall(contracts, "collectionRename", calls, { + ...params, + collectionAlias: "content-old" + }); + assertContractCall(contracts, "typeSchemaRename", calls, { + ...params, + typeSchemaAlias: "article-old" + }); + assertContractCall(contracts, "ingestionFunctionRename", calls, { + ...params, + ingestionFunctionAlias: "map-content-old" + }); + assertContractCall(contracts, "persistedDocumentRename", calls, { + ...params, + persistedDocumentAlias: "doc-old" + }); + assertContractCall(contracts, "webhookRename", calls, { + ...params, + webhookAlias: "hook-old" + }); +}); diff --git a/test/contracts.apply-openapi-shape.test.ts b/test/contracts.apply-openapi-shape.test.ts index 39c3414..09a9981 100644 --- a/test/contracts.apply-openapi-shape.test.ts +++ b/test/contracts.apply-openapi-shape.test.ts @@ -1161,3 +1161,256 @@ test("type-schema delete uses expected endpoint", async () => { ); assert.ok(deleteCall); }); + +test("non-environment rename commands use expected endpoints and payload keys", async () => { + const envDir = await mkEnvDir("compose-contract-entity-rename-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify({ collections: [{ alias: "content-new", description: "Main content" }] }, null, 2), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "type-schemas", "article-new.schema.json"), + JSON.stringify( + { + alias: "article-new", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion.json"), + JSON.stringify( + { + functions: [ + { + alias: "map-content-new", + description: "Maps content", + scriptFile: "functions/ingestion/map-content-new.js" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile( + path.join(envDir, "functions", "ingestion", "map-content-new.js"), + "export default (x) => x;", + "utf8" + ); + await fs.writeFile( + path.join(envDir, "graphql", "persisted.json"), + JSON.stringify( + { + documents: [ + { + alias: "doc-new", + description: "Main query", + queryFile: "graphql/persisted/doc-new.gql" + } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-new.gql"), "query { ping }", "utf8"); + await fs.writeFile( + path.join(envDir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "hook-new", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/collections/content-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { collectionAlias: "content-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article-old", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { typeSchemaAlias: "article-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content-old" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old" && + method === "GET" + ) { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content-old", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { ingestionFunctionAlias: "map-content-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-old" } }] }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old" && + method === "GET" + ) { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old", + description: "Main query", + document: "query { ping }" + }); + } + if ( + u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" && + method === "POST" + ) { + return jsonResponse(200, { persistedDocumentAlias: "doc-new" }); + } + + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename" && method === "POST") { + return jsonResponse(200, { webhookAlias: "hook-new" }); + } + + return jsonResponse(200, { edges: [] }); + }); + + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } finally { + restore(); + } + + const collectionRenameCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/collections/content-old/commands/rename") + ); + assert.ok(collectionRenameCall); + assert.deepEqual(JSON.parse(collectionRenameCall.bodyText ?? "{}"), { newCollectionAlias: "content-new" }); + + const typeSchemaRenameCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename") + ); + assert.ok(typeSchemaRenameCall); + assert.deepEqual(JSON.parse(typeSchemaRenameCall.bodyText ?? "{}"), { newTypeSchemaAlias: "article-new" }); + + const ingestionRenameCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename") + ); + assert.ok(ingestionRenameCall); + assert.deepEqual(JSON.parse(ingestionRenameCall.bodyText ?? "{}"), { + newIngestionFunctionAlias: "map-content-new" + }); + + const persistedRenameCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes( + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" + ) + ); + assert.ok(persistedRenameCall); + assert.deepEqual(JSON.parse(persistedRenameCall.bodyText ?? "{}"), { + newPersistedDocumentAlias: "doc-new" + }); + + const webhookRenameCall = calls.find( + (c) => + c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename") + ); + assert.ok(webhookRenameCall); + assert.deepEqual(JSON.parse(webhookRenameCall.bodyText ?? "{}"), { newWebhookAlias: "hook-new" }); +}); From 5de803c72371f185b2b20abc83860a4ab001ec7b Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 11:54:00 -0500 Subject: [PATCH 25/47] Add desired-side ambiguity test for renames --- docs/testing.md | 2 +- test/compose.apply.rename-inference.test.ts | 69 +++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index 0f8e436..e3ef391 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -37,7 +37,7 @@ - `test/compose.apply.rename-inference.test.ts` - verifies inferred non-environment rename guardrails: - - ambiguous signature matches do not trigger rename calls + - ambiguous signature matches do not trigger rename calls (remote-side or desired-side ambiguity) - signature mismatches do not trigger rename calls - fallback behavior remains create/delete convergence diff --git a/test/compose.apply.rename-inference.test.ts b/test/compose.apply.rename-inference.test.ts index 13d2291..444e106 100644 --- a/test/compose.apply.rename-inference.test.ts +++ b/test/compose.apply.rename-inference.test.ts @@ -149,3 +149,72 @@ test("collection rename inference skips when signatures do not match and falls b globalThis.fetch = oldFetch; } }); + +test("collection rename inference skips when desired signatures are ambiguous and falls back to create/delete", async () => { + const envDir = await makeCollectionsEnvDir([ + { alias: "content-new-a", description: "Same description" }, + { alias: "content-new-b", description: "Same description" } + ]); + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Same description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { + return jsonResponse(201, { collectionAlias: "created" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); + assert.equal( + ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new-a"), + true + ); + assert.equal( + ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new-b"), + true + ); + assert.equal( + ops.some((o) => o.kind === "delete" && o.resource === "collection" && o.alias === "content-old"), + true + ); + } finally { + globalThis.fetch = oldFetch; + } +}); From a8dcac69bcb8aa9525b89532e7cf8fe4771cd52f Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 11:57:17 -0500 Subject: [PATCH 26/47] Add rename inference ambiguity test for persisted docs --- docs/testing.md | 1 + test/compose.apply.rename-inference.test.ts | 107 ++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/docs/testing.md b/docs/testing.md index e3ef391..df4a899 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -39,6 +39,7 @@ - verifies inferred non-environment rename guardrails: - ambiguous signature matches do not trigger rename calls (remote-side or desired-side ambiguity) - signature mismatches do not trigger rename calls + - includes persisted-document ambiguity coverage (file-backed signature matching path) - fallback behavior remains create/delete convergence - `test/commands.plan-exit.test.ts` diff --git a/test/compose.apply.rename-inference.test.ts b/test/compose.apply.rename-inference.test.ts index 444e106..d90efdc 100644 --- a/test/compose.apply.rename-inference.test.ts +++ b/test/compose.apply.rename-inference.test.ts @@ -26,6 +26,32 @@ async function makeCollectionsEnvDir( return dir; } +async function makePersistedEnvDir( + docs: Array<{ alias: string; description?: string | null; query: string }> +): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-persisted-")); + await fs.mkdir(path.join(dir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile( + path.join(dir, "graphql", "persisted.json"), + JSON.stringify( + { + documents: docs.map((d) => ({ + alias: d.alias, + description: d.description ?? "", + queryFile: `graphql/persisted/${d.alias}.gql` + })) + }, + null, + 2 + ), + "utf8" + ); + for (const d of docs) { + await fs.writeFile(path.join(dir, "graphql", "persisted", `${d.alias}.gql`), d.query, "utf8"); + } + return dir; +} + test("collection rename inference skips ambiguous matches and falls back to create/delete", async () => { const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "Same description" }]); const calls: FetchCall[] = []; @@ -218,3 +244,84 @@ test("collection rename inference skips when desired signatures are ambiguous an globalThis.fetch = oldFetch; } }); + +test("persisted-doc rename inference skips ambiguous remote matches and falls back to create/delete", async () => { + const envDir = await makePersistedEnvDir([ + { alias: "doc-new", description: "Shared description", query: "query { ping }" } + ]); + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { persistedDocumentAlias: "doc-old-a" } }, { node: { persistedDocumentAlias: "doc-old-b" } }] + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-a" && + method === "GET" + ) { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old-a", + description: "Shared description", + document: "query { ping }" + }); + } + if ( + u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-b" && + method === "GET" + ) { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old-b", + description: "Shared description", + document: "query { ping }" + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "POST") { + return jsonResponse(201, { persistedDocumentAlias: "doc-new" }); + } + if ( + (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-a" || + u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-b") && + method === "DELETE" + ) { + return new Response(null, { status: 204 }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + assert.equal(calls.some((c) => c.url.includes("/graphql/persisted-documents/") && c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "persisted-doc"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "persisted-doc" && o.alias === "doc-new"), true); + assert.equal(ops.filter((o) => o.kind === "delete" && o.resource === "persisted-doc").length, 2); + } finally { + globalThis.fetch = oldFetch; + } +}); From c105bf123359bb72db97ff7dabc3295e58778e0b Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 11:59:36 -0500 Subject: [PATCH 27/47] Add rename ambiguity coverage for webhooks --- docs/testing.md | 1 + test/compose.apply.rename-inference.test.ts | 103 ++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/docs/testing.md b/docs/testing.md index df4a899..2fc706d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -40,6 +40,7 @@ - ambiguous signature matches do not trigger rename calls (remote-side or desired-side ambiguity) - signature mismatches do not trigger rename calls - includes persisted-document ambiguity coverage (file-backed signature matching path) + - includes webhook ambiguity coverage (rich signature matching path) - fallback behavior remains create/delete convergence - `test/commands.plan-exit.test.ts` diff --git a/test/compose.apply.rename-inference.test.ts b/test/compose.apply.rename-inference.test.ts index d90efdc..fe8724d 100644 --- a/test/compose.apply.rename-inference.test.ts +++ b/test/compose.apply.rename-inference.test.ts @@ -325,3 +325,106 @@ test("persisted-doc rename inference skips ambiguous remote matches and falls ba globalThis.fetch = oldFetch; } }); + +test("webhook rename inference skips ambiguous remote matches and falls back to create/delete", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-webhook-")); + await fs.writeFile( + path.join(dir, "webhooks.json"), + JSON.stringify( + { + webhooks: [ + { + alias: "hook-new", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, + null, + 2 + ), + "utf8" + ); + + const calls: FetchCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText: string | undefined; + if (typeof init?.body === "string") bodyText = init.body; + if (init?.body instanceof URLSearchParams) bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { webhookAlias: "hook-old-a" } }, { node: { webhookAlias: "hook-old-b" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-a" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old-a", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-b" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old-b", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "POST") { + return jsonResponse(201, { webhookAlias: "hook-new" }); + } + if ( + (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-a" || + u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-b") && + method === "DELETE" + ) { + return new Response(null, { status: 204 }); + } + + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir: dir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + + assert.equal(calls.some((c) => c.url.includes("/webhooks/") && c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "webhook"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "webhook" && o.alias === "hook-new"), true); + assert.equal(ops.filter((o) => o.kind === "delete" && o.resource === "webhook").length, 2); + } finally { + globalThis.fetch = oldFetch; + } +}); From 3938be2acc4278cdd07e081253c20e4f740cfcb7 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 12:03:49 -0500 Subject: [PATCH 28/47] Add test for clear output around renames. --- docs/testing.md | 3 + test/commands.apply.output-snapshots.test.ts | 159 +++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/docs/testing.md b/docs/testing.md index 2fc706d..f8ad9a2 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -100,6 +100,9 @@ - snapshot-style assertions for per-environment summary lines - snapshot-style assertions for environment aggregate summary line - snapshot-style assertions for final plan/apply summary lines (including `delete` count) + - snapshot-style assertions for rename messaging clarity: + - inferred rename prints explicit `UPDATE ... — rename -> ` + - ambiguous rename fallback prints `CREATE`/`DELETE` operations instead of rename update - `test/commands.apply-multi-env.test.ts` - verifies plan/apply run across all configured environments when `--env` is omitted diff --git a/test/commands.apply.output-snapshots.test.ts b/test/commands.apply.output-snapshots.test.ts index b827c8d..45513fb 100644 --- a/test/commands.apply.output-snapshots.test.ts +++ b/test/commands.apply.output-snapshots.test.ts @@ -45,6 +45,30 @@ async function createComposeRoot(): Promise { return root; } +async function createComposeRootWithCollections( + collections: Array<{ alias: string; description?: string | null }> +): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-output-snapshot-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + return root; +} + function installMockFetch(): () => void { const oldFetch = globalThis.fetch; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { @@ -74,6 +98,26 @@ function installMockFetch(): () => void { }; } +function installMockFetchWithHandler( + handler: (u: URL, method: string) => Response +): () => void { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + return handler(u, method); + }) as typeof fetch; + + return () => { + globalThis.fetch = oldFetch; + }; +} + function captureLogs(): { output: string[]; restore: () => void } { const originalLog = console.log; const output: string[] = []; @@ -189,3 +233,118 @@ test("apply output operation lines and summary match expected snapshot", async ( "Apply complete. create=1, delete=0, update=0, noop=1, warn=0" ); }); + +test("plan output shows inferred collection rename as update with explicit rename details", async () => { + const root = await createComposeRootWithCollections([{ alias: "content-new", description: "Main content" }]); + const restoreFetch = installMockFetchWithHandler((u, method) => { + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + + assert.deepEqual(operationLines(output), [ + "NOOP [env=dev] environment:dev", + "UPDATE [env=dev] collection:content-new — rename content-old -> content-new" + ]); + assert.equal( + perEnvironmentRunSummaryLine(output), + "Plan env dev: create=0, delete=0, update=1, noop=1, warn=0" + ); + assert.equal( + finalSummaryLine(output), + "Plan complete. create=0, delete=0, update=1, noop=1, warn=0" + ); +}); + +test("plan output for ambiguous collection rename fallback shows create/delete and no rename update line", async () => { + const root = await createComposeRootWithCollections([{ alias: "content-new", description: "Main content" }]); + const restoreFetch = installMockFetchWithHandler((u, method) => { + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content-old-a" } }, { node: { collectionAlias: "content-old-b" } }] + }); + } + if ( + (u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old-a" || + u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old-b") && + method === "GET" + ) { + return jsonResponse(200, { description: "Main content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + + const opLines = operationLines(output); + assert.equal( + opLines.some((line) => line.includes("UPDATE [env=dev] collection:content-new — rename")), + false + ); + assert.deepEqual(opLines, [ + "NOOP [env=dev] environment:dev", + "CREATE [env=dev] collection:content-new", + "DELETE [env=dev] collection:content-old-a", + "DELETE [env=dev] collection:content-old-b" + ]); + assert.equal( + perEnvironmentRunSummaryLine(output), + "Plan env dev: create=1, delete=2, update=0, noop=1, warn=0" + ); + assert.equal( + finalSummaryLine(output), + "Plan complete. create=1, delete=2, update=0, noop=1, warn=0" + ); +}); From 9ef56ceac4f9bd5fa1710e36d9eb23391f8ab619 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 12:11:57 -0500 Subject: [PATCH 29/47] Add rename analysis for ambiguous remote candidates. --- docs/testing.md | 1 + src/compose/apply.ts | 50 ++++++++++++++------ test/commands.apply.output-snapshots.test.ts | 4 +- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index f8ad9a2..e924ba5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -103,6 +103,7 @@ - snapshot-style assertions for rename messaging clarity: - inferred rename prints explicit `UPDATE ... — rename -> ` - ambiguous rename fallback prints `CREATE`/`DELETE` operations instead of rename update + - fallback delete lines include `— no unique rename match` context - `test/commands.apply-multi-env.test.ts` - verifies plan/apply run across all configured environments when `--env` is omitted diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 5641f99..452ab79 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -287,7 +287,9 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P signature: stringify({ description: existing.data?.description ?? existing.data?.collection?.description ?? null }) }); } - const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renameAnalysis = computeRenameAnalysis(desiredRenameCandidates, remoteRenameCandidates); + const renameFromByTo = renameAnalysis.renameFromByTo; + const ambiguousRemoteAliases = renameAnalysis.ambiguousRemoteAliases; const renamedFrom = new Set(renameFromByTo.values()); for (const c of desired.collections) { @@ -385,7 +387,8 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P for (const alias of existingAliases.keys()) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; - out.push({ kind: "delete", env: opEnv, resource: "collection", alias }); + const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; + out.push({ kind: "delete", env: opEnv, resource: "collection", alias, details }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/collections/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -491,7 +494,9 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P signature: stringify({ schema: remoteSchema, description: remoteDesc ?? null }) }); } - const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renameAnalysis = computeRenameAnalysis(desiredRenameCandidates, remoteRenameCandidates); + const renameFromByTo = renameAnalysis.renameFromByTo; + const ambiguousRemoteAliases = renameAnalysis.ambiguousRemoteAliases; const renamedFrom = new Set(renameFromByTo.values()); for (const desired of desiredEntries) { @@ -615,7 +620,8 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P const alias = item?.typeSchemaAlias ?? item?.alias; if (typeof alias !== "string" || desiredAliases.has(alias) || renamedFrom.has(alias)) continue; - out.push({ kind: "delete", env: opEnv, resource: "type-schema", alias }); + const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; + out.push({ kind: "delete", env: opEnv, resource: "type-schema", alias, details }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -715,7 +721,9 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti }) }); } - const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renameAnalysis = computeRenameAnalysis(desiredRenameCandidates, remoteRenameCandidates); + const renameFromByTo = renameAnalysis.renameFromByTo; + const ambiguousRemoteAliases = renameAnalysis.ambiguousRemoteAliases; const renamedFrom = new Set(renameFromByTo.values()); for (const fn of desiredEntries) { @@ -840,7 +848,8 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti for (const alias of existingAliases.keys()) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; - out.push({ kind: "delete", env: opEnv, resource: "ingestion-function", alias }); + const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; + out.push({ kind: "delete", env: opEnv, resource: "ingestion-function", alias, details }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -940,7 +949,9 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): }) }); } - const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renameAnalysis = computeRenameAnalysis(desiredRenameCandidates, remoteRenameCandidates); + const renameFromByTo = renameAnalysis.renameFromByTo; + const ambiguousRemoteAliases = renameAnalysis.ambiguousRemoteAliases; const renamedFrom = new Set(renameFromByTo.values()); for (const d of desiredEntries) { @@ -1070,7 +1081,8 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): for (const alias of existingAliases) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; - out.push({ kind: "delete", env: opEnv, resource: "persisted-doc", alias }); + const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; + out.push({ kind: "delete", env: opEnv, resource: "persisted-doc", alias, details }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -1181,7 +1193,9 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom }) }); } - const renameFromByTo = computeRenamePairs(desiredRenameCandidates, remoteRenameCandidates); + const renameAnalysis = computeRenameAnalysis(desiredRenameCandidates, remoteRenameCandidates); + const renameFromByTo = renameAnalysis.renameFromByTo; + const ambiguousRemoteAliases = renameAnalysis.ambiguousRemoteAliases; const renamedFrom = new Set(renameFromByTo.values()); for (const w of desired.webhooks) { @@ -1403,7 +1417,8 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom for (const alias of existingAliases) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; - out.push({ kind: "delete", env: opEnv, resource: "webhook", alias }); + const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; + out.push({ kind: "delete", env: opEnv, resource: "webhook", alias, details }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -1436,10 +1451,10 @@ function normalizeStringMap(value: unknown): Record { return out; } -function computeRenamePairs( +function computeRenameAnalysis( desired: Array<{ alias: string; signature: string }>, remote: Array<{ alias: string; signature: string }> -): Map { +): { renameFromByTo: Map; ambiguousRemoteAliases: Set } { const desiredBySignature = new Map(); const remoteBySignature = new Map(); @@ -1455,12 +1470,17 @@ function computeRenamePairs( remoteBySignature.set(r.signature, arr); } - const out = new Map(); + const renameFromByTo = new Map(); + const ambiguousRemoteAliases = new Set(); for (const [signature, desiredAliases] of desiredBySignature.entries()) { const remoteAliases = remoteBySignature.get(signature) ?? []; if (desiredAliases.length === 1 && remoteAliases.length === 1) { - out.set(desiredAliases[0], remoteAliases[0]); + renameFromByTo.set(desiredAliases[0], remoteAliases[0]); + continue; + } + if (desiredAliases.length > 0 && remoteAliases.length > 0) { + for (const alias of remoteAliases) ambiguousRemoteAliases.add(alias); } } - return out; + return { renameFromByTo, ambiguousRemoteAliases }; } diff --git a/test/commands.apply.output-snapshots.test.ts b/test/commands.apply.output-snapshots.test.ts index 45513fb..b243dbc 100644 --- a/test/commands.apply.output-snapshots.test.ts +++ b/test/commands.apply.output-snapshots.test.ts @@ -336,8 +336,8 @@ test("plan output for ambiguous collection rename fallback shows create/delete a assert.deepEqual(opLines, [ "NOOP [env=dev] environment:dev", "CREATE [env=dev] collection:content-new", - "DELETE [env=dev] collection:content-old-a", - "DELETE [env=dev] collection:content-old-b" + "DELETE [env=dev] collection:content-old-a — no unique rename match", + "DELETE [env=dev] collection:content-old-b — no unique rename match" ]); assert.equal( perEnvironmentRunSummaryLine(output), From 0295d47f1bf76e5d262586ec7afd0a51d44a6833 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 12:44:10 -0500 Subject: [PATCH 30/47] Add assertions to catch ambiguous rename fallbacks for collections, persisted docs, and webhooks. --- docs/testing.md | 1 + test/compose.apply.rename-inference.test.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index e924ba5..51cc83d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -41,6 +41,7 @@ - signature mismatches do not trigger rename calls - includes persisted-document ambiguity coverage (file-backed signature matching path) - includes webhook ambiguity coverage (rich signature matching path) + - asserts ambiguous-fallback delete ops carry `details: "no unique rename match"` - fallback behavior remains create/delete convergence - `test/commands.plan-exit.test.ts` diff --git a/test/compose.apply.rename-inference.test.ts b/test/compose.apply.rename-inference.test.ts index fe8724d..2a72645 100644 --- a/test/compose.apply.rename-inference.test.ts +++ b/test/compose.apply.rename-inference.test.ts @@ -111,7 +111,9 @@ test("collection rename inference skips ambiguous matches and falls back to crea assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); - assert.equal(ops.filter((o) => o.kind === "delete" && o.resource === "collection").length, 2); + const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "collection"); + assert.equal(deleteOps.length, 2); + assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); } finally { globalThis.fetch = oldFetch; } @@ -320,7 +322,9 @@ test("persisted-doc rename inference skips ambiguous remote matches and falls ba assert.equal(calls.some((c) => c.url.includes("/graphql/persisted-documents/") && c.url.includes("/commands/rename")), false); assert.equal(ops.some((o) => o.kind === "update" && o.resource === "persisted-doc"), false); assert.equal(ops.some((o) => o.kind === "create" && o.resource === "persisted-doc" && o.alias === "doc-new"), true); - assert.equal(ops.filter((o) => o.kind === "delete" && o.resource === "persisted-doc").length, 2); + const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "persisted-doc"); + assert.equal(deleteOps.length, 2); + assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); } finally { globalThis.fetch = oldFetch; } @@ -423,7 +427,9 @@ test("webhook rename inference skips ambiguous remote matches and falls back to assert.equal(calls.some((c) => c.url.includes("/webhooks/") && c.url.includes("/commands/rename")), false); assert.equal(ops.some((o) => o.kind === "update" && o.resource === "webhook"), false); assert.equal(ops.some((o) => o.kind === "create" && o.resource === "webhook" && o.alias === "hook-new"), true); - assert.equal(ops.filter((o) => o.kind === "delete" && o.resource === "webhook").length, 2); + const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "webhook"); + assert.equal(deleteOps.length, 2); + assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); } finally { globalThis.fetch = oldFetch; } From ce1c334a21033f4a3d92ce1b5f5667c2e59c929f Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Thu, 12 Feb 2026 13:02:01 -0500 Subject: [PATCH 31/47] Improve status command output --- docs/commands.md | 1 + docs/testing.md | 3 ++ src/commands/status.ts | 15 ++++++- test/commands.status-multi-env.test.ts | 55 ++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index b82cc62..f6cadb3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -114,6 +114,7 @@ Alias routing semantics: - Status: implemented - Responsibility: - compare local desired state vs `compose.lock.json` + - include local state identity context (`remoteAlias`) and pending-rename signal when `remoteAlias != alias` - no API calls - Side effects: none. - Idempotency: fully idempotent. diff --git a/docs/testing.md b/docs/testing.md index 51cc83d..c4917b0 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -62,6 +62,9 @@ - `test/commands.status-multi-env.test.ts` - verifies `status` without `--env` reports all configured environments - verifies mixed multi-env state (`UNCHANGED` + `NO LOCK`) sets non-zero exit with `--failOnChanges` + - verifies pending-rename identity context is surfaced: + - JSON includes `remoteAlias` and `renamePending` + - human output includes `Identity: pending rename ( -> )` - `test/commands.pull.test.ts` - verifies `pull` writes local IaC files from remote responses for implemented entities diff --git a/src/commands/status.ts b/src/commands/status.ts index 7796c15..aea831f 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import type { CommandModule } from "yargs"; import YAML from "yaml"; import { computeDesiredHashes, readLock } from "../compose/lock.js"; +import { findEnvironmentByAlias, readComposeState } from "../compose/state.js"; type Args = { dir: string; @@ -35,6 +36,8 @@ type GroupResult = { type EnvStatus = { env: string; envDir: string; + remoteAlias?: string; + renamePending?: boolean; hasLock: boolean; lastAppliedAt?: string; lastAppliedCliVersion?: string; @@ -72,6 +75,7 @@ export const statusCommand: CommandModule<{}, Args> = { const composeYamlPath = path.join(rootDir, "umbraco-compose.yaml"); const cfg = await readComposeConfig(composeYamlPath); + const state = await readComposeState(rootDir); const envNames = Object.keys(cfg.environments ?? {}); if (envNames.length === 0) { @@ -89,6 +93,8 @@ export const statusCommand: CommandModule<{}, Args> = { } const envDir = path.resolve(rootDir, envCfg.dir); + const envState = state ? findEnvironmentByAlias(state, envName) : undefined; + const remoteAlias = envState?.remoteAlias; const desired = await computeDesiredHashes(envDir); const lock = await readLock(envDir); @@ -104,6 +110,8 @@ export const statusCommand: CommandModule<{}, Args> = { results.push({ env: envName, envDir, + ...(remoteAlias && { remoteAlias }), + ...(remoteAlias && remoteAlias !== envName && { renamePending: true }), hasLock: !!lock, ...(lock?.lastApplied?.at && { lastAppliedAt: lock.lastApplied.at @@ -169,6 +177,11 @@ function printHuman(project: string, envs: EnvStatus[]) { for (const e of envs) { console.log(`Environment: ${e.env}`); console.log(`Dir: ${e.envDir}`); + if (e.renamePending && e.remoteAlias) { + console.log(`Identity: pending rename (${e.remoteAlias} -> ${e.env})`); + } else if (e.remoteAlias) { + console.log(`Identity: remote alias ${e.remoteAlias}`); + } if (!e.hasLock) { console.log("Lock: (none)"); } else { @@ -226,4 +239,4 @@ async function readComposeConfig(filePath: string): Promise { } return doc as ComposeConfig; -} \ No newline at end of file +} diff --git a/test/commands.status-multi-env.test.ts b/test/commands.status-multi-env.test.ts index 5173d4d..fbff439 100644 --- a/test/commands.status-multi-env.test.ts +++ b/test/commands.status-multi-env.test.ts @@ -6,6 +6,7 @@ import path from "node:path"; import YAML from "yaml"; import { statusCommand } from "../src/commands/status.js"; import { computeDesiredHashes, type LockFile } from "../src/compose/lock.js"; +import { ensureStateForAliases, markEnvironmentRemoteAlias, writeComposeState } from "../src/compose/state.js"; type ComposeConfig = { version: number; @@ -18,6 +19,8 @@ type StatusJson = { results: Array<{ env: string; hasLock: boolean; + remoteAlias?: string; + renamePending?: boolean; groups: Record; }>; }; @@ -100,6 +103,33 @@ async function runStatusJson(args: { } } +async function runStatusHuman(args: { + root: string; + env?: string; + failOnChanges: boolean; +}): Promise<{ output: string[]; exitCode: number | undefined }> { + const originalLog = console.log; + const out: string[] = []; + console.log = (...values: unknown[]) => { + out.push(values.map((v) => String(v)).join(" ")); + }; + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: args.root, + env: args.env, + json: false, + failOnChanges: args.failOnChanges + }); + return { output: out, exitCode: process.exitCode }; + } finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} + test("status without --env returns results for all configured environments", async () => { const { root, devDir, prodDir } = await createFixture(); await writeMatchingLock(devDir); @@ -127,3 +157,28 @@ test("status across multiple environments sets exitCode=1 when any environment h assert.equal(prod.groups.collections?.status, "NO LOCK"); assert.equal(result.exitCode, 1); }); + +test("status includes state identity context for pending rename in json and human output", async () => { + const { root, devDir } = await createFixture(); + await writeMatchingLock(devDir); + + const aliases = ["dev", "prod"]; + const state = ensureStateForAliases(null, "test-project", aliases).state; + const devState = state.environments.find((e) => e.alias === "dev"); + assert.ok(devState); + const marked = markEnvironmentRemoteAlias(state, devState.id, "old-dev"); + assert.equal(marked, true); + await writeComposeState(root, state); + + const jsonResult = await runStatusJson({ root, failOnChanges: false }); + const devJson = jsonResult.data.results.find((r) => r.env === "dev"); + assert.ok(devJson); + assert.equal(devJson.remoteAlias, "old-dev"); + assert.equal(devJson.renamePending, true); + + const humanResult = await runStatusHuman({ root, env: "dev", failOnChanges: false }); + assert.equal( + humanResult.output.some((line) => line.includes("Identity: pending rename (old-dev -> dev)")), + true + ); +}); From 493a3c514f9adf18a30b283698d007ec25295cb9 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 08:25:22 -0500 Subject: [PATCH 32/47] Resolve the active remote environment alias first before local alias --- docs/commands.md | 1 + docs/testing.md | 1 + src/commands/pull.ts | 55 ++++++++++++++++++++++------ test/commands.pull.test.ts | 73 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index f6cadb3..37c2fa7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -94,6 +94,7 @@ Alias routing semantics: - persisted document registry + query files - webhooks - Notes: + - resolves active remote environment alias from `compose.state.json.remoteAlias` first, then local alias - stale generated files in managed folders are removed when no longer present remotely - updates `compose.state.json.remoteAlias` for pulled environment - does not write `compose.lock.json` diff --git a/docs/testing.md b/docs/testing.md index c4917b0..6f5f419 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -70,6 +70,7 @@ - verifies `pull` writes local IaC files from remote responses for implemented entities - verifies stale managed files are removed during pull - verifies `compose.state.json.remoteAlias` is updated for pulled environment + - verifies pending-rename alias routing: pull uses state `remoteAlias` when local alias differs - verifies pull fails when target environment is missing remotely - `test/commands.pull-hardening.test.ts` diff --git a/src/commands/pull.ts b/src/commands/pull.ts index 437fe67..1f08183 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -111,6 +111,9 @@ export async function runPull(args: Args): Promise { const envDir = path.resolve(rootDir, envCfg.dir); await fs.mkdir(envDir, { recursive: true }); + const state = ensureStateForAliases(await readComposeState(rootDir), cfg.project, Object.keys(cfg.environments)).state; + const envState = findEnvironmentByAlias(state, args.env); + const stateRemoteAlias = envState?.remoteAlias; const client = new ManagementClient({ baseUrl: envCfg.managementBaseUrl, @@ -121,12 +124,24 @@ export async function runPull(args: Args): Promise { }) }); - const remoteEnvExists = await ensureRemoteEnvironmentExists(client, cfg.project, args.env); - if (!remoteEnvExists) { - throw new Error(`Environment "${args.env}" was not found remotely for project "${cfg.project}".`); + const resolvedRemoteEnvAlias = await resolveRemoteEnvironmentAlias( + client, + cfg.project, + args.env, + stateRemoteAlias + ); + if (!resolvedRemoteEnvAlias) { + throw new Error( + `Environment "${args.env}" was not found remotely for project "${cfg.project}" (checked aliases: ${[ + args.env, + ...(stateRemoteAlias ? [stateRemoteAlias] : []) + ] + .filter((v, i, arr) => arr.indexOf(v) === i) + .join(", ")}).` + ); } - const snapshot = await fetchSnapshot(client, cfg.project, args.env); + const snapshot = await fetchSnapshot(client, cfg.project, resolvedRemoteEnvAlias); const stagingDir = path.join(envDir, `.__pull_staging__${Date.now()}_${Math.random().toString(36).slice(2)}`); try { @@ -136,14 +151,16 @@ export async function runPull(args: Args): Promise { await fs.rm(stagingDir, { recursive: true, force: true }); } - const state = ensureStateForAliases(await readComposeState(rootDir), cfg.project, Object.keys(cfg.environments)).state; - const envState = findEnvironmentByAlias(state, args.env); if (envState) { - markEnvironmentRemoteAlias(state, envState.id, args.env); + markEnvironmentRemoteAlias(state, envState.id, resolvedRemoteEnvAlias); } await writeComposeState(rootDir, state); - console.log(`Pulled remote state for env "${args.env}" into ${envDir}`); + const aliasMsg = + resolvedRemoteEnvAlias === args.env + ? `"${args.env}"` + : `"${resolvedRemoteEnvAlias}" (mapped to local "${args.env}")`; + console.log(`Pulled remote state for env ${aliasMsg} into ${envDir}`); } async function fetchSnapshot(client: ManagementClient, project: string, env: string): Promise { @@ -360,13 +377,31 @@ async function replaceFile(stagedFile: string, targetFile: string): Promise { +async function listRemoteEnvironmentAliases(client: ManagementClient, project: string): Promise> { const res = await client.get(`/v1/projects/${encodeURIComponent(project)}/environments`); if (res.status >= 400) { throw new Error(`Failed to list environments (${res.status}).`); } const items = asNodes(res.data, ["environmentAlias", "alias"]); - return items.some((i) => String(i.environmentAlias ?? i.alias) === env); + const out = new Set(); + for (const item of items) out.add(String(item.environmentAlias ?? item.alias)); + return out; +} + +async function resolveRemoteEnvironmentAlias( + client: ManagementClient, + project: string, + localAlias: string, + stateRemoteAlias?: string +): Promise { + const remoteAliases = await listRemoteEnvironmentAliases(client, project); + const candidates = [stateRemoteAlias, localAlias].filter((v, i, arr): v is string => { + return typeof v === "string" && v.length > 0 && arr.indexOf(v) === i; + }); + for (const alias of candidates) { + if (remoteAliases.has(alias)) return alias; + } + return null; } function asNodes(data: any, aliasKeys: string[]): any[] { diff --git a/test/commands.pull.test.ts b/test/commands.pull.test.ts index 5d8d0f3..d4cbfbf 100644 --- a/test/commands.pull.test.ts +++ b/test/commands.pull.test.ts @@ -200,6 +200,79 @@ test("pull fails when target environment is not present remotely", async () => { } }); +test("pull uses state remoteAlias when local alias differs during pending rename", async () => { + const { root, envDir } = await createFixture(); + + const composePath = path.join(root, "umbraco-compose.yaml"); + const cfgText = await fs.readFile(composePath, "utf8"); + const cfg = YAML.parse(cfgText) as ComposeConfig; + cfg.environments = { + "dev-renamed": { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + }; + await fs.writeFile(composePath, YAML.stringify(cfg), "utf8"); + + const state = await readComposeState(root); + assert.ok(state); + state.environments[0].alias = "dev-renamed"; + state.environments[0].remoteAlias = "dev"; + await writeComposeState(root, state); + + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content", description: "Main content" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await pullCommand.handler({ + dir: root, + env: "dev-renamed", + clientId: "client", + clientSecret: "secret" + }); + } finally { + globalThis.fetch = oldFetch; + } + + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")) as { + collections: Array<{ alias: string }>; + }; + assert.equal(collections.collections[0]?.alias, "content"); + + const after = await readComposeState(root); + assert.ok(after); + const envState = after.environments.find((e) => e.alias === "dev-renamed"); + assert.ok(envState); + assert.equal(envState.remoteAlias, "dev"); +}); + async function exists(p: string): Promise { try { await fs.stat(p); From b5bebce08f2555da666527c30a2773ea3b4ede68 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 08:28:29 -0500 Subject: [PATCH 33/47] Ensure pulled environment alias routing parity. --- docs/testing.md | 1 + test/contracts.pull-contract-map.test.ts | 106 ++++++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index 6f5f419..79fb195 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -82,6 +82,7 @@ - `test/contracts.pull-contract-map.test.ts` - validates emitted pull read calls against `docs/contracts/pull-contract.json` - catches drift between documented pull endpoint paths and implementation + - verifies alias-routing parity: pull contract paths use state `remoteAlias` when local alias differs - `test/commands.clone.test.ts` - verifies clone bootstraps local config and delegates to pull for local file materialization diff --git a/test/contracts.pull-contract-map.test.ts b/test/contracts.pull-contract-map.test.ts index a664a4f..c304b63 100644 --- a/test/contracts.pull-contract-map.test.ts +++ b/test/contracts.pull-contract-map.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import os from "node:os"; import YAML from "yaml"; import { pullCommand } from "../src/commands/pull.js"; -import { createInitialState, writeComposeState } from "../src/compose/state.js"; +import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; type ContractMap = { version: number; @@ -143,3 +143,107 @@ test("pull emitted read calls match pull contract map", async () => { assertContractCall(contracts, "webhookList", calls, baseParams); assertContractCall(contracts, "webhookGet", calls, baseParams); }); + +test("pull contract-map calls use state remoteAlias path when local alias differs", async () => { + const contracts = await readContractMap(); + const { root } = await createFixture(); + + const composePath = path.join(root, "umbraco-compose.yaml"); + const cfgText = await fs.readFile(composePath, "utf8"); + const cfg = YAML.parse(cfgText) as ComposeConfig; + cfg.environments = { + "dev-renamed": { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + }; + await fs.writeFile(composePath, YAML.stringify(cfg), "utf8"); + + const state = await readComposeState(root); + assert.ok(state); + state.environments[0].alias = "dev-renamed"; + state.environments[0].remoteAlias = "dev"; + await writeComposeState(root, state); + + const calls: CapturedCall[] = []; + const oldFetch = globalThis.fetch; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + calls.push({ method, pathname: u.pathname }); + + if (u.pathname === "/v1/auth/token") return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(200, { typeSchemaAlias: "article", schema: {}, description: null }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/fn-a") { + return jsonResponse(200, { ingestionFunctionAlias: "fn-a", script: "export default () => null;" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/doc-a") { + return jsonResponse(200, { persistedDocumentAlias: "doc-a", document: "query { __typename }" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-a" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/hook-a") { + return jsonResponse(200, { + webhookAlias: "hook-a", + url: "https://example.com/hook", + eventTypes: [], + collectionAliases: [], + typeSchemaAliases: [], + customHeaders: {} + }); + } + return jsonResponse(404, { error: "unexpected path" }); + }) as typeof fetch; + + try { + await pullCommand.handler({ + dir: root, + env: "dev-renamed", + clientId: "client", + clientSecret: "secret" + }); + } finally { + globalThis.fetch = oldFetch; + } + + const baseParams = { + projectAlias: "test-project", + environmentAlias: "dev", + typeSchemaAlias: "article", + ingestionFunctionAlias: "fn-a", + persistedDocumentAlias: "doc-a", + webhookAlias: "hook-a" + }; + + assertContractCall(contracts, "environmentList", calls, baseParams); + assertContractCall(contracts, "collectionList", calls, baseParams); + assertContractCall(contracts, "typeSchemaList", calls, baseParams); + assertContractCall(contracts, "typeSchemaGet", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionList", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionGet", calls, baseParams); + assertContractCall(contracts, "persistedDocumentList", calls, baseParams); + assertContractCall(contracts, "persistedDocumentGet", calls, baseParams); + assertContractCall(contracts, "webhookList", calls, baseParams); + assertContractCall(contracts, "webhookGet", calls, baseParams); +}); From e2089b57cf98eb70a89500bd5b2958f4a1aff6ce Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 08:34:49 -0500 Subject: [PATCH 34/47] Improve validate output to warn when entities have identical rename signatures --- docs/commands.md | 3 + docs/testing.md | 4 + src/commands/validate.ts | 151 +++++++++++++++++++++ test/commands.validate-rename-risk.test.ts | 117 ++++++++++++++++ 4 files changed, 275 insertions(+) create mode 100644 test/commands.validate-rename-risk.test.ts diff --git a/docs/commands.md b/docs/commands.md index 37c2fa7..5ed3f14 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -79,6 +79,9 @@ Alias routing semantics: - Status: implemented - Responsibility: - local structural and schema checks without API calls +- Notes: + - emits rename-risk warnings when multiple local entities share identical rename-signatures (can make non-environment rename inference ambiguous) + - `--strict` promotes these warnings to exit-code failure - Side effects: none. - Idempotency: fully idempotent. diff --git a/docs/testing.md b/docs/testing.md index 79fb195..cd1ae6d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -59,6 +59,10 @@ - verifies `status --failOnChanges` sets `process.exitCode = 1` when lock-backed drift exists - verifies `validate --strict` sets `process.exitCode = 1` when warnings exist +- `test/commands.validate-rename-risk.test.ts` + - verifies validate emits rename-risk warnings for duplicate local rename-signatures + - verifies `validate --strict` fails when those warnings are present + - `test/commands.status-multi-env.test.ts` - verifies `status` without `--env` reports all configured environments - verifies mixed multi-env state (`UNCHANGED` + `NO LOCK`) sets non-zero exit with `--failOnChanges` diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 81b73a8..473a1ba 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -138,6 +138,9 @@ async function validateEnv(envName: string, envDir: string, issues: Issue[]) { // Validate schema files await validateTypeSchemas(path.join(envDir, "type-schemas"), issues); + + // Heuristic warnings for rename-inference ambiguity risk. + await validateRenameInferenceRisks(envDir, issues); } async function validateCollections(filePath: string, issues: Issue[]) { @@ -345,6 +348,132 @@ async function validateTypeSchemas(dirPath: string, issues: Issue[]) { } } +async function validateRenameInferenceRisks(envDir: string, issues: Issue[]) { + // Collections: signature is currently description-only for rename inference. + const collectionsFile = await readJsonQuiet(path.join(envDir, "collections.json")); + if (collectionsFile && Array.isArray(collectionsFile.collections)) { + const entries = collectionsFile.collections + .filter((c: any) => typeof c?.alias === "string") + .map((c: any) => ({ + alias: String(c.alias), + signature: JSON.stringify({ description: c.description ?? null }) + })); + pushRenameRiskWarnings("collections", entries, path.join(envDir, "collections.json"), issues); + } + + // Type schemas: signature uses description + schema. + const typeSchemaDir = path.join(envDir, "type-schemas"); + if (await exists(typeSchemaDir)) { + const entries: Array<{ alias: string; signature: string }> = []; + const files = await fs.readdir(typeSchemaDir, { withFileTypes: true }); + for (const f of files) { + if (!f.isFile() || !f.name.endsWith(".schema.json")) continue; + const obj = await readJsonQuiet(path.join(typeSchemaDir, f.name)); + if (!obj || typeof obj.alias !== "string") continue; + entries.push({ + alias: obj.alias, + signature: JSON.stringify({ description: obj.description ?? null, schema: obj.schema ?? null }) + }); + } + pushRenameRiskWarnings("type-schemas", entries, typeSchemaDir, issues); + } + + // Ingestion functions: signature uses description + script. + const ingestionRegistryPath = path.join(envDir, "functions", "ingestion.json"); + const ingestionReg = await readJsonQuiet(ingestionRegistryPath); + if (ingestionReg && Array.isArray(ingestionReg.functions)) { + const entries: Array<{ alias: string; signature: string }> = []; + for (const fn of ingestionReg.functions) { + if (typeof fn?.alias !== "string") continue; + if (typeof fn?.scriptFile !== "string") continue; + const scriptPath = path.resolve(path.join(envDir, fn.scriptFile)); + if (!(await exists(scriptPath))) continue; + const script = await fs.readFile(scriptPath, "utf8"); + entries.push({ + alias: fn.alias, + signature: JSON.stringify({ description: fn.description ?? "", script }) + }); + } + pushRenameRiskWarnings("ingestion-functions", entries, ingestionRegistryPath, issues); + } + + // Persisted docs: signature uses description + document. + const persistedRegistryPath = path.join(envDir, "graphql", "persisted.json"); + const persistedReg = await readJsonQuiet(persistedRegistryPath); + if (persistedReg && Array.isArray(persistedReg.documents)) { + const entries: Array<{ alias: string; signature: string }> = []; + for (const d of persistedReg.documents) { + if (typeof d?.alias !== "string") continue; + if (typeof d?.queryFile !== "string") continue; + const queryPath = path.resolve(path.join(envDir, d.queryFile)); + if (!(await exists(queryPath))) continue; + const document = await fs.readFile(queryPath, "utf8"); + entries.push({ + alias: d.alias, + signature: JSON.stringify({ description: d.description ?? "", document }) + }); + } + pushRenameRiskWarnings("persisted-docs", entries, persistedRegistryPath, issues); + } + + // Webhooks: signature uses url + description + normalized filters/headers. + const webhooksPath = path.join(envDir, "webhooks.json"); + const webhooks = await readJsonQuiet(webhooksPath); + if (webhooks && Array.isArray(webhooks.webhooks)) { + const entries = webhooks.webhooks + .filter((w: any) => typeof w?.alias === "string") + .map((w: any) => ({ + alias: String(w.alias), + signature: JSON.stringify({ + description: w.description ?? null, + url: String(w.url ?? ""), + eventTypes: normalizeStringArray(w.eventTypes), + collectionAliases: normalizeStringArray(w.collectionAliases), + typeSchemaAliases: normalizeStringArray(w.typeSchemaAliases), + customHeaders: normalizeStringMap(w.customHeaders) + }) + })); + pushRenameRiskWarnings("webhooks", entries, webhooksPath, issues); + } +} + +function pushRenameRiskWarnings( + entityName: string, + entries: Array<{ alias: string; signature: string }>, + file: string, + issues: Issue[] +) { + const groups = findDuplicateSignatureAliasGroups(entries); + if (groups.length === 0) return; + const rendered = groups.map((g) => `[${g.join(", ")}]`).join("; "); + issues.push({ + level: "warn", + file, + message: + `${entityName} contain entries with identical rename-signatures; ` + + `rename inference may be ambiguous: ${rendered}` + }); +} + +function findDuplicateSignatureAliasGroups( + entries: Array<{ alias: string; signature: string }> +): string[][] { + const bySignature = new Map(); + for (const entry of entries) { + const list = bySignature.get(entry.signature) ?? []; + list.push(entry.alias); + bySignature.set(entry.signature, list); + } + const groups: string[][] = []; + for (const aliases of bySignature.values()) { + if (aliases.length <= 1) continue; + aliases.sort((a, b) => a.localeCompare(b)); + groups.push(aliases); + } + groups.sort((a, b) => a.join(",").localeCompare(b.join(","))); + return groups; +} + async function readComposeConfig(filePath: string, issues: Issue[]): Promise { if (!(await exists(filePath))) { issues.push({ level: "error", file: filePath, message: "Missing umbraco-compose.yaml" }); @@ -406,6 +535,16 @@ async function readJson(filePath: string, issues: Issue[]): Promise } } +async function readJsonQuiet(filePath: string): Promise { + if (!(await exists(filePath))) return null; + try { + const text = await fs.readFile(filePath, "utf8"); + return JSON.parse(text); + } catch { + return null; + } +} + async function exists(p: string): Promise { try { await fs.stat(p); @@ -420,6 +559,18 @@ function isKebab(s: string): boolean { return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(s); } +function normalizeStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.map((v) => String(v)).sort((a, b) => a.localeCompare(b)); +} + +function normalizeStringMap(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) out[k] = String(v); + return out; +} + function finish(issues: Issue[], strict: boolean) { const errors = issues.filter((i) => i.level === "error"); const warnings = issues.filter((i) => i.level === "warn"); diff --git a/test/commands.validate-rename-risk.test.ts b/test/commands.validate-rename-risk.test.ts new file mode 100644 index 0000000..6a550d9 --- /dev/null +++ b/test/commands.validate-rename-risk.test.ts @@ -0,0 +1,117 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { validateCommand } from "../src/commands/validate.js"; + +type ComposeConfig = { + version: number; + project: string; + environments: Record; +}; + +async function createFixtureWithCollectionRenameRisk(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-validate-rename-risk-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + + const cfg: ComposeConfig = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + + await fs.writeFile( + path.join(envDir, "collections.json"), + JSON.stringify( + { + collections: [ + { alias: "content-a", description: "Same description" }, + { alias: "content-b", description: "Same description" } + ] + }, + null, + 2 + ), + "utf8" + ); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + + // Keep schema checks green so test isolates rename-risk warning behavior. + await fs.writeFile( + path.join(envDir, "type-schemas", "article.schema.json"), + JSON.stringify( + { + alias: "article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: {} + } + }, + null, + 2 + ), + "utf8" + ); + + return root; +} + +async function runValidateCapture(args: { + root: string; + strict: boolean; +}): Promise<{ output: string[]; exitCode: number | undefined }> { + const originalLog = console.log; + const output: string[] = []; + console.log = (...values: unknown[]) => { + output.push(values.map((v) => String(v)).join(" ")); + }; + + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await validateCommand.handler({ + dir: args.root, + strict: args.strict + }); + return { output, exitCode: process.exitCode }; + } finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} + +test("validate warns on duplicate rename-signatures in non-strict mode without failing", async () => { + const root = await createFixtureWithCollectionRenameRisk(); + const result = await runValidateCapture({ root, strict: false }); + + assert.equal( + result.output.some((line) => line.includes("identical rename-signatures")), + true + ); + assert.equal(result.exitCode, 0); +}); + +test("validate --strict fails when duplicate rename-signature warnings are present", async () => { + const root = await createFixtureWithCollectionRenameRisk(); + const result = await runValidateCapture({ root, strict: true }); + + assert.equal( + result.output.some((line) => line.includes("identical rename-signatures")), + true + ); + assert.equal(result.exitCode, 1); +}); From a1b81f212435b2121cdbaa1ad7f4a7c8b27697fa Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 08:38:26 -0500 Subject: [PATCH 35/47] Add guidance for how to remediate duplicate rename signatures --- src/commands/validate.ts | 13 ++++++++++++- test/commands.validate-rename-risk.test.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 473a1ba..4c118da 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -446,12 +446,23 @@ function pushRenameRiskWarnings( const groups = findDuplicateSignatureAliasGroups(entries); if (groups.length === 0) return; const rendered = groups.map((g) => `[${g.join(", ")}]`).join("; "); + const guidanceByEntity: Record = { + collections: "ensure collection descriptions are distinct for entities you may rename", + "type-schemas": "ensure schema+description combinations are distinct", + "ingestion-functions": "ensure script+description combinations are distinct", + "persisted-docs": "ensure document+description combinations are distinct", + webhooks: "ensure url/description/events/filters/headers combinations are distinct" + }; + const guidance = + guidanceByEntity[entityName] ?? + "ensure rename-signature inputs are distinct"; issues.push({ level: "warn", file, message: `${entityName} contain entries with identical rename-signatures; ` + - `rename inference may be ambiguous: ${rendered}` + `rename inference may be ambiguous: ${rendered}. ` + + `Resolve by: ${guidance}, or apply the intended create/delete explicitly.` }); } diff --git a/test/commands.validate-rename-risk.test.ts b/test/commands.validate-rename-risk.test.ts index 6a550d9..6557c78 100644 --- a/test/commands.validate-rename-risk.test.ts +++ b/test/commands.validate-rename-risk.test.ts @@ -102,6 +102,10 @@ test("validate warns on duplicate rename-signatures in non-strict mode without f result.output.some((line) => line.includes("identical rename-signatures")), true ); + assert.equal( + result.output.some((line) => line.includes("Resolve by: ensure collection descriptions are distinct")), + true + ); assert.equal(result.exitCode, 0); }); @@ -113,5 +117,9 @@ test("validate --strict fails when duplicate rename-signature warnings are prese result.output.some((line) => line.includes("identical rename-signatures")), true ); + assert.equal( + result.output.some((line) => line.includes("apply the intended create/delete explicitly")), + true + ); assert.equal(result.exitCode, 1); }); From 46499688f4448b35d74fcb437bad709104392b17 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 08:39:36 -0500 Subject: [PATCH 36/47] Fix grammar in validation message. --- src/commands/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 4c118da..8ca2abc 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -460,7 +460,7 @@ function pushRenameRiskWarnings( level: "warn", file, message: - `${entityName} contain entries with identical rename-signatures; ` + + `${entityName} contains entries with identical rename-signatures; ` + `rename inference may be ambiguous: ${rendered}. ` + `Resolve by: ${guidance}, or apply the intended create/delete explicitly.` }); From b3c15333532a2cc321396e6de6cee243dcaee9de Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 09:34:07 -0500 Subject: [PATCH 37/47] Create freeze checklist for v.1.0.0 release. --- docs/commands.md | 15 ++++++++++++ docs/release-v1-checklist.md | 45 ++++++++++++++++++++++++++++++++++++ docs/testing.md | 3 +++ 3 files changed, 63 insertions(+) create mode 100644 docs/release-v1-checklist.md diff --git a/docs/commands.md b/docs/commands.md index 5ed3f14..1724b99 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -29,6 +29,21 @@ This document defines the intended command behavior for the core workflow: - Idempotency: - rerunning the same command with unchanged inputs should produce no new side effects +## Recommended CI Workflow +Use this baseline pipeline for pull-request validation and main-branch deploy: + +1. `npm ci` +2. `npm run build` +3. `npm test` +4. `compose validate --dir ./compose --strict` +5. `compose plan --dir ./compose` +6. On approved/protected branch only: `compose apply --dir ./compose` + +Notes: +- keep `plan` output as a build artifact or PR comment for review +- gate `apply` behind branch protection and required approvals +- treat any non-zero exit as a deployment block + ## Environment Identity Model - Local state file: `compose.state.json` at project root. - Purpose: provide stable local environment IDs so alias changes are handled as rename, not create. diff --git a/docs/release-v1-checklist.md b/docs/release-v1-checklist.md new file mode 100644 index 0000000..7da361b --- /dev/null +++ b/docs/release-v1-checklist.md @@ -0,0 +1,45 @@ +# v1.0.0 Release Checklist + +## Scope Freeze +- Confirm command scope for `v1.0.0`: + - `init`, `generate`, `validate`, `clone`, `pull`, `status`, `plan`, `apply`, `env rename` +- Confirm command/behavior docs are current: + - `docs/commands.md` + - `docs/testing.md` + - `docs/contracts/apply-contract.json` + - `docs/contracts/pull-contract.json` + +## Quality Gates +- `npm run build` passes. +- `npm test` passes. +- Validate strict mode check passes on a representative project: + - `compose validate --dir ./compose --strict` +- Plan check passes on a representative project: + - `compose plan --dir ./compose` + +## Regression Focus +- Environment identity and alias routing: + - pending rename uses `remoteAlias` correctly in `plan`, `apply`, `pull`, and `status` +- Non-environment rename inference: + - unique matches infer rename + - ambiguous matches safely fall back to create/delete with contextual output +- Lock/state safety: + - lock/state writes skipped when warnings occur during apply +- Contract drift checks: + - apply and pull contract-map tests remain green + +## CI/CD Readiness +- CI runs build + tests on every PR. +- CI captures plan output for review. +- Apply is restricted to protected branch and approved runs. +- Secret handling is configured for OAuth credentials in CI. + +## Release Prep +- Bump version in `package.json` from `0.1.0` to `1.0.0`. +- Generate/update changelog summary for `v1.0.0`. +- Tag release (`v1.0.0`) and publish artifacts as needed. + +## Post-Release +- Run one smoke pipeline against a non-production environment. +- Confirm expected exit codes and operation output formatting. +- Capture follow-up issues as `v1.0.x` patch candidates. diff --git a/docs/testing.md b/docs/testing.md index cd1ae6d..c155e45 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -204,3 +204,6 @@ These tests protect the highest-risk behavior we recently fixed: - command-level tests for multi-environment apply/plan orchestration once batching is implemented - staged pull rollback edge-case tests (filesystem errors during commit) - clone idempotency and `--force` behavior tests for pre-existing compose files + +## Release Readiness +- Use `docs/release-v1-checklist.md` as the source of truth for `v1.0.0` cutover criteria. From b65635226c46891fc9c9e8a4ffabb30d8fc0e4ab Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 09:53:10 -0500 Subject: [PATCH 38/47] Update instructions for CI usage --- docs/commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 1724b99..e8aecb8 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -35,9 +35,9 @@ Use this baseline pipeline for pull-request validation and main-branch deploy: 1. `npm ci` 2. `npm run build` 3. `npm test` -4. `compose validate --dir ./compose --strict` -5. `compose plan --dir ./compose` -6. On approved/protected branch only: `compose apply --dir ./compose` +4. `node dist/index.js compose validate --dir ./compose --strict` +5. `node dist/index.js compose plan --dir ./compose` +6. On approved/protected branch only: `node dist/index.js compose apply --dir ./compose` Notes: - keep `plan` output as a build artifact or PR comment for review From d4b0ab7693548fbdbc1bff31ed2833472e92d7ce Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 09:55:37 -0500 Subject: [PATCH 39/47] Add basic CI workflow for PRs and pushes to main --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e529ef7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Validate (if compose config exists) + shell: bash + run: | + if [[ -f "compose/umbraco-compose.yaml" ]]; then + node dist/index.js validate --dir ./compose --strict + else + echo "Skipping validate: compose/umbraco-compose.yaml not found" + fi + + - name: Plan (if compose config exists) + shell: bash + run: | + if [[ -f "compose/umbraco-compose.yaml" ]]; then + node dist/index.js plan --dir ./compose + else + echo "Skipping plan: compose/umbraco-compose.yaml not found" + fi From 15fee0b08652b8bcdbde2a9449edd2b662eb2ff9 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 09:57:24 -0500 Subject: [PATCH 40/47] Add deployment action configuration. --- .github/workflows/deploy.yml | 63 ++++++++++++++++++++++++++++++++++++ docs/release-v1-checklist.md | 2 ++ 2 files changed, 65 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..297c130 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,63 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + compose_dir: + description: "Compose root directory" + required: true + default: "./compose" + environment_alias: + description: "Environment alias to apply (e.g. dev, prod)" + required: true + default: "dev" + +concurrency: + group: deploy-${{ github.ref }}-${{ inputs.environment_alias }} + cancel-in-progress: false + +jobs: + apply: + runs-on: ubuntu-latest + environment: ${{ inputs.environment_alias }} + + steps: + - name: Require main branch + if: github.ref != 'refs/heads/main' + run: | + echo "Deploy workflow may only run from main. Current ref: $GITHUB_REF" + exit 1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Validate + run: node dist/index.js validate --dir "${{ inputs.compose_dir }}" --strict + + - name: Plan + run: node dist/index.js plan --dir "${{ inputs.compose_dir }}" --env "${{ inputs.environment_alias }}" + env: + COMPOSE_MGMT_CLIENT_ID: ${{ secrets.COMPOSE_MGMT_CLIENT_ID }} + COMPOSE_MGMT_CLIENT_SECRET: ${{ secrets.COMPOSE_MGMT_CLIENT_SECRET }} + COMPOSE_MGMT_SCOPE: ${{ secrets.COMPOSE_MGMT_SCOPE }} + COMPOSE_MGMT_AUDIENCE: ${{ secrets.COMPOSE_MGMT_AUDIENCE }} + + - name: Apply + run: node dist/index.js apply --dir "${{ inputs.compose_dir }}" --env "${{ inputs.environment_alias }}" + env: + COMPOSE_MGMT_CLIENT_ID: ${{ secrets.COMPOSE_MGMT_CLIENT_ID }} + COMPOSE_MGMT_CLIENT_SECRET: ${{ secrets.COMPOSE_MGMT_CLIENT_SECRET }} + COMPOSE_MGMT_SCOPE: ${{ secrets.COMPOSE_MGMT_SCOPE }} + COMPOSE_MGMT_AUDIENCE: ${{ secrets.COMPOSE_MGMT_AUDIENCE }} diff --git a/docs/release-v1-checklist.md b/docs/release-v1-checklist.md index 7da361b..9efa01d 100644 --- a/docs/release-v1-checklist.md +++ b/docs/release-v1-checklist.md @@ -32,6 +32,8 @@ - CI runs build + tests on every PR. - CI captures plan output for review. - Apply is restricted to protected branch and approved runs. +- Deploy workflow is manual (`workflow_dispatch`) and main-branch-only. +- GitHub Environments are configured with required reviewers for deploy targets. - Secret handling is configured for OAuth credentials in CI. ## Release Prep From a1817e9d09b7672ae543538c54185f6bea034ebf Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 09:58:58 -0500 Subject: [PATCH 41/47] Add documentation for running CI workflows. --- docs/ci.md | 50 ++++++++++++++++++++++++++++++++++++ docs/release-v1-checklist.md | 1 + 2 files changed, 51 insertions(+) create mode 100644 docs/ci.md diff --git a/docs/ci.md b/docs/ci.md new file mode 100644 index 0000000..e9ab77b --- /dev/null +++ b/docs/ci.md @@ -0,0 +1,50 @@ +# CI/CD Setup + +## Workflows +- CI workflow: `.github/workflows/ci.yml` + - runs on PRs and pushes to `main` + - executes build + tests + - runs validate/plan when `compose/umbraco-compose.yaml` exists + +- Deploy workflow: `.github/workflows/deploy.yml` + - manual trigger only (`workflow_dispatch`) + - main-branch-only guard + - runs validate -> plan -> apply for the selected environment alias + +## Required Repository Secrets +Configure these in repository settings: +- `COMPOSE_MGMT_CLIENT_ID` +- `COMPOSE_MGMT_CLIENT_SECRET` + +Optional: +- `COMPOSE_MGMT_SCOPE` +- `COMPOSE_MGMT_AUDIENCE` + +## GitHub Environment Protection +Create GitHub Environments that match deploy aliases (for example `dev`, `prod`) and enable: +- required reviewers +- optional wait timer +- environment-scoped secrets if needed + +The deploy workflow binds `environment: ${{ inputs.environment_alias }}`, so protections apply automatically when names match. + +## Recommended Branch Protection +For `main`: +- require PR review approvals +- require status checks to pass: + - CI / build-test +- restrict direct pushes + +## Running Deploy +1. Open GitHub Actions -> `Deploy`. +2. Click `Run workflow`. +3. Choose `main` branch. +4. Set: + - `compose_dir` (usually `./compose`) + - `environment_alias` (for example `dev` or `prod`) +5. Confirm environment approval prompt if configured. + +## Operational Notes +- Deploy concurrency is keyed by branch + environment alias to avoid overlapping applies for the same target. +- Keep plan output from deploy runs for audit trail and incident review. +- Treat non-zero exits as failed deployment attempts; do not auto-retry without reviewing warnings/errors. diff --git a/docs/release-v1-checklist.md b/docs/release-v1-checklist.md index 9efa01d..950fb4a 100644 --- a/docs/release-v1-checklist.md +++ b/docs/release-v1-checklist.md @@ -35,6 +35,7 @@ - Deploy workflow is manual (`workflow_dispatch`) and main-branch-only. - GitHub Environments are configured with required reviewers for deploy targets. - Secret handling is configured for OAuth credentials in CI. +- CI/CD operational setup is documented in `docs/ci.md`. ## Release Prep - Bump version in `package.json` from `0.1.0` to `1.0.0`. From c45c8d77a8b2befadce841f7e1312fc879dd8da5 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 10:07:47 -0500 Subject: [PATCH 42/47] Update docs with info about typical workflows. --- docs/ci.md | 11 +++++++++ docs/commands.md | 4 ++++ docs/workflow.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 docs/workflow.md diff --git a/docs/ci.md b/docs/ci.md index e9ab77b..4662428 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -11,6 +11,17 @@ - main-branch-only guard - runs validate -> plan -> apply for the selected environment alias +## Are Workflow Files Auto-Created? +Short answer: no, not automatically by GitHub. + +How they appear in a repo: +- they must exist as committed files under `.github/workflows/` +- they can be copied from this project, a template repository, or scaffolding logic in your own tooling + +Current behavior in this CLI: +- `compose init` does not scaffold workflow files today +- users should copy/add `ci.yml` and `deploy.yml` (or use a repo template) when setting up a new canonical repo + ## Required Repository Secrets Configure these in repository settings: - `COMPOSE_MGMT_CLIENT_ID` diff --git a/docs/commands.md b/docs/commands.md index e8aecb8..5d591cf 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -13,6 +13,10 @@ This document defines the intended command behavior for the core workflow: - `compose apply` - `compose env rename` +Related docs: +- `docs/workflow.md` (end-to-end usage flow) +- `docs/ci.md` (CI/deploy setup and protections) + ## Shared Behavior - Compose project root: defaults to `./compose` and must contain `umbraco-compose.yaml`. - Exit codes: diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000..a8b5d09 --- /dev/null +++ b/docs/workflow.md @@ -0,0 +1,61 @@ +# Typical User Workflow + +This page explains how teams usually use Compose CLI in practice. + +## Path A: Start From Existing Remote Project +Use this when the platform already has state and you want to adopt GitOps. + +1. Bootstrap locally: + - `compose clone --dir ./compose --project --env ` +2. Review/edit generated files: + - optional `compose generate ` for new resources + - manual edits in `compose/env//...` +3. Validate and preview: + - `compose validate --dir ./compose --strict` + - `compose plan --dir ./compose` +4. Open PR to canonical repository. +5. CI runs build/tests + validate + plan. +6. After approval/merge, deploy workflow runs: + - `validate -> plan -> apply` + +Outcome: +- canonical repo becomes source of truth for the remote state. + +## Path B: Start New Project Locally +Use this when no remote state exists yet. + +1. Initialize local project: + - `compose init --dir ./compose --project --env dev` +2. Add resources: + - `compose generate ...` + - manual edits to schemas/functions/queries/webhooks +3. Validate and preview: + - `compose validate --dir ./compose --strict` + - `compose plan --dir ./compose` +4. Push branch + open PR to canonical repository. +5. CI validates and publishes plan output for review. +6. Approved deploy runs `compose apply` to create/update remote state. + +Outcome: +- remote state is created from repo-managed IaC files. + +## How Rename Safety Works +- Environment rename: + - tracked via `compose.state.json` (`alias` + `remoteAlias`) + - `plan` reads old remote alias path, `apply` renames then writes using local alias path +- Non-environment rename: + - inferred only on unique one-to-one signature matches + - ambiguous matches fall back to create/delete with explicit context + +## CI/CD Interaction Model +- PR CI: + - non-destructive checks (`build`, `test`, `validate`, `plan`) +- Deploy workflow (manual/guarded): + - destructive step (`apply`) behind branch + environment protections +- Exit code policy: + - non-zero exits block progression in CI/CD + +## What Teams Usually Tell Reviewers +- “`validate` is local lint/shape checks.” +- “`plan` is the review artifact for what will change remotely.” +- “`apply` is only run in guarded CI, not ad-hoc from laptops.” From 6a1a5227f84a13d6cbcd18db61ba9c1c34c68009 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 10:16:20 -0500 Subject: [PATCH 43/47] Update config for builds --- test/commands.apply-multi-env.test.d.ts | 2 + test/commands.apply-multi-env.test.d.ts.map | 1 + test/commands.apply-multi-env.test.js | 163 +++ test/commands.apply-multi-env.test.js.map | 1 + test/commands.apply.auth-failures.test.d.ts | 2 + ...commands.apply.auth-failures.test.d.ts.map | 1 + test/commands.apply.auth-failures.test.js | 84 ++ test/commands.apply.auth-failures.test.js.map | 1 + ...ommands.apply.create-env-failure.test.d.ts | 2 + ...nds.apply.create-env-failure.test.d.ts.map | 1 + .../commands.apply.create-env-failure.test.js | 85 ++ ...mands.apply.create-env-failure.test.js.map | 1 + .../commands.apply.output-snapshots.test.d.ts | 2 + ...mands.apply.output-snapshots.test.d.ts.map | 1 + test/commands.apply.output-snapshots.test.js | 277 +++++ ...ommands.apply.output-snapshots.test.js.map | 1 + ...nds.apply.partial-failure-writes.test.d.ts | 2 + ...apply.partial-failure-writes.test.d.ts.map | 1 + ...mands.apply.partial-failure-writes.test.js | 90 ++ ...s.apply.partial-failure-writes.test.js.map | 1 + test/commands.apply.test.d.ts | 2 + test/commands.apply.test.d.ts.map | 1 + test/commands.apply.test.js | 135 ++ test/commands.apply.test.js.map | 1 + test/commands.clone.test.d.ts | 2 + test/commands.clone.test.d.ts.map | 1 + test/commands.clone.test.js | 82 ++ test/commands.clone.test.js.map | 1 + test/commands.env-rename.test.d.ts | 2 + test/commands.env-rename.test.d.ts.map | 1 + test/commands.env-rename.test.js | 148 +++ test/commands.env-rename.test.js.map | 1 + test/commands.generate-apply-flow.test.d.ts | 2 + ...commands.generate-apply-flow.test.d.ts.map | 1 + test/commands.generate-apply-flow.test.js | 170 +++ test/commands.generate-apply-flow.test.js.map | 1 + ...ommands.generate-apply-warn-flow.test.d.ts | 2 + ...nds.generate-apply-warn-flow.test.d.ts.map | 1 + .../commands.generate-apply-warn-flow.test.js | 163 +++ ...mands.generate-apply-warn-flow.test.js.map | 1 + test/commands.generate-flow.test.d.ts | 2 + test/commands.generate-flow.test.d.ts.map | 1 + test/commands.generate-flow.test.js | 150 +++ test/commands.generate-flow.test.js.map | 1 + test/commands.generate-status-flow.test.d.ts | 2 + ...ommands.generate-status-flow.test.d.ts.map | 1 + test/commands.generate-status-flow.test.js | 147 +++ .../commands.generate-status-flow.test.js.map | 1 + test/commands.generate.test.d.ts | 2 + test/commands.generate.test.d.ts.map | 1 + test/commands.generate.test.js | 275 +++++ test/commands.generate.test.js.map | 1 + test/commands.plan-exit.test.d.ts | 2 + test/commands.plan-exit.test.d.ts.map | 1 + test/commands.plan-exit.test.js | 72 ++ test/commands.plan-exit.test.js.map | 1 + test/commands.pull-hardening.test.d.ts | 2 + test/commands.pull-hardening.test.d.ts.map | 1 + test/commands.pull-hardening.test.js | 216 ++++ test/commands.pull-hardening.test.js.map | 1 + test/commands.pull.test.d.ts | 2 + test/commands.pull.test.d.ts.map | 1 + test/commands.pull.test.js | 241 ++++ test/commands.pull.test.js.map | 1 + test/commands.status-multi-env.test.d.ts | 2 + test/commands.status-multi-env.test.d.ts.map | 1 + test/commands.status-multi-env.test.js | 140 +++ test/commands.status-multi-env.test.js.map | 1 + test/commands.status-validate-exit.test.d.ts | 2 + ...ommands.status-validate-exit.test.d.ts.map | 1 + test/commands.status-validate-exit.test.js | 87 ++ .../commands.status-validate-exit.test.js.map | 1 + test/commands.validate-rename-risk.test.d.ts | 2 + ...ommands.validate-rename-risk.test.d.ts.map | 1 + test/commands.validate-rename-risk.test.js | 79 ++ .../commands.validate-rename-risk.test.js.map | 1 + ....apply.environment-alias-routing.test.d.ts | 2 + ...ly.environment-alias-routing.test.d.ts.map | 1 + ...se.apply.environment-alias-routing.test.js | 129 ++ ...pply.environment-alias-routing.test.js.map | 1 + test/compose.apply.failures.test.d.ts | 2 + test/compose.apply.failures.test.d.ts.map | 1 + test/compose.apply.failures.test.js | 96 ++ test/compose.apply.failures.test.js.map | 1 + test/compose.apply.rename-inference.test.d.ts | 2 + ...mpose.apply.rename-inference.test.d.ts.map | 1 + test/compose.apply.rename-inference.test.js | 370 ++++++ ...compose.apply.rename-inference.test.js.map | 1 + test/compose.management-client.test.d.ts | 2 + test/compose.management-client.test.d.ts.map | 1 + test/compose.management-client.test.js | 74 ++ test/compose.management-client.test.js.map | 1 + test/compose.oauth-base.test.d.ts | 2 + test/compose.oauth-base.test.d.ts.map | 1 + test/compose.oauth-base.test.js | 93 ++ test/compose.oauth-base.test.js.map | 1 + test/compose.state.test.d.ts | 2 + test/compose.state.test.d.ts.map | 1 + test/compose.state.test.js | 49 + test/compose.state.test.js.map | 1 + test/contracts.apply-contract-map.test.d.ts | 2 + ...contracts.apply-contract-map.test.d.ts.map | 1 + test/contracts.apply-contract-map.test.js | 1089 +++++++++++++++++ test/contracts.apply-contract-map.test.js.map | 1 + test/contracts.apply-openapi-shape.test.d.ts | 2 + ...ontracts.apply-openapi-shape.test.d.ts.map | 1 + test/contracts.apply-openapi-shape.test.js | 1071 ++++++++++++++++ .../contracts.apply-openapi-shape.test.js.map | 1 + test/contracts.pull-contract-map.test.d.ts | 2 + .../contracts.pull-contract-map.test.d.ts.map | 1 + test/contracts.pull-contract-map.test.js | 216 ++++ test/contracts.pull-contract-map.test.js.map | 1 + tsconfig.json | 8 +- 113 files changed, 6108 insertions(+), 3 deletions(-) create mode 100644 test/commands.apply-multi-env.test.d.ts create mode 100644 test/commands.apply-multi-env.test.d.ts.map create mode 100644 test/commands.apply-multi-env.test.js create mode 100644 test/commands.apply-multi-env.test.js.map create mode 100644 test/commands.apply.auth-failures.test.d.ts create mode 100644 test/commands.apply.auth-failures.test.d.ts.map create mode 100644 test/commands.apply.auth-failures.test.js create mode 100644 test/commands.apply.auth-failures.test.js.map create mode 100644 test/commands.apply.create-env-failure.test.d.ts create mode 100644 test/commands.apply.create-env-failure.test.d.ts.map create mode 100644 test/commands.apply.create-env-failure.test.js create mode 100644 test/commands.apply.create-env-failure.test.js.map create mode 100644 test/commands.apply.output-snapshots.test.d.ts create mode 100644 test/commands.apply.output-snapshots.test.d.ts.map create mode 100644 test/commands.apply.output-snapshots.test.js create mode 100644 test/commands.apply.output-snapshots.test.js.map create mode 100644 test/commands.apply.partial-failure-writes.test.d.ts create mode 100644 test/commands.apply.partial-failure-writes.test.d.ts.map create mode 100644 test/commands.apply.partial-failure-writes.test.js create mode 100644 test/commands.apply.partial-failure-writes.test.js.map create mode 100644 test/commands.apply.test.d.ts create mode 100644 test/commands.apply.test.d.ts.map create mode 100644 test/commands.apply.test.js create mode 100644 test/commands.apply.test.js.map create mode 100644 test/commands.clone.test.d.ts create mode 100644 test/commands.clone.test.d.ts.map create mode 100644 test/commands.clone.test.js create mode 100644 test/commands.clone.test.js.map create mode 100644 test/commands.env-rename.test.d.ts create mode 100644 test/commands.env-rename.test.d.ts.map create mode 100644 test/commands.env-rename.test.js create mode 100644 test/commands.env-rename.test.js.map create mode 100644 test/commands.generate-apply-flow.test.d.ts create mode 100644 test/commands.generate-apply-flow.test.d.ts.map create mode 100644 test/commands.generate-apply-flow.test.js create mode 100644 test/commands.generate-apply-flow.test.js.map create mode 100644 test/commands.generate-apply-warn-flow.test.d.ts create mode 100644 test/commands.generate-apply-warn-flow.test.d.ts.map create mode 100644 test/commands.generate-apply-warn-flow.test.js create mode 100644 test/commands.generate-apply-warn-flow.test.js.map create mode 100644 test/commands.generate-flow.test.d.ts create mode 100644 test/commands.generate-flow.test.d.ts.map create mode 100644 test/commands.generate-flow.test.js create mode 100644 test/commands.generate-flow.test.js.map create mode 100644 test/commands.generate-status-flow.test.d.ts create mode 100644 test/commands.generate-status-flow.test.d.ts.map create mode 100644 test/commands.generate-status-flow.test.js create mode 100644 test/commands.generate-status-flow.test.js.map create mode 100644 test/commands.generate.test.d.ts create mode 100644 test/commands.generate.test.d.ts.map create mode 100644 test/commands.generate.test.js create mode 100644 test/commands.generate.test.js.map create mode 100644 test/commands.plan-exit.test.d.ts create mode 100644 test/commands.plan-exit.test.d.ts.map create mode 100644 test/commands.plan-exit.test.js create mode 100644 test/commands.plan-exit.test.js.map create mode 100644 test/commands.pull-hardening.test.d.ts create mode 100644 test/commands.pull-hardening.test.d.ts.map create mode 100644 test/commands.pull-hardening.test.js create mode 100644 test/commands.pull-hardening.test.js.map create mode 100644 test/commands.pull.test.d.ts create mode 100644 test/commands.pull.test.d.ts.map create mode 100644 test/commands.pull.test.js create mode 100644 test/commands.pull.test.js.map create mode 100644 test/commands.status-multi-env.test.d.ts create mode 100644 test/commands.status-multi-env.test.d.ts.map create mode 100644 test/commands.status-multi-env.test.js create mode 100644 test/commands.status-multi-env.test.js.map create mode 100644 test/commands.status-validate-exit.test.d.ts create mode 100644 test/commands.status-validate-exit.test.d.ts.map create mode 100644 test/commands.status-validate-exit.test.js create mode 100644 test/commands.status-validate-exit.test.js.map create mode 100644 test/commands.validate-rename-risk.test.d.ts create mode 100644 test/commands.validate-rename-risk.test.d.ts.map create mode 100644 test/commands.validate-rename-risk.test.js create mode 100644 test/commands.validate-rename-risk.test.js.map create mode 100644 test/compose.apply.environment-alias-routing.test.d.ts create mode 100644 test/compose.apply.environment-alias-routing.test.d.ts.map create mode 100644 test/compose.apply.environment-alias-routing.test.js create mode 100644 test/compose.apply.environment-alias-routing.test.js.map create mode 100644 test/compose.apply.failures.test.d.ts create mode 100644 test/compose.apply.failures.test.d.ts.map create mode 100644 test/compose.apply.failures.test.js create mode 100644 test/compose.apply.failures.test.js.map create mode 100644 test/compose.apply.rename-inference.test.d.ts create mode 100644 test/compose.apply.rename-inference.test.d.ts.map create mode 100644 test/compose.apply.rename-inference.test.js create mode 100644 test/compose.apply.rename-inference.test.js.map create mode 100644 test/compose.management-client.test.d.ts create mode 100644 test/compose.management-client.test.d.ts.map create mode 100644 test/compose.management-client.test.js create mode 100644 test/compose.management-client.test.js.map create mode 100644 test/compose.oauth-base.test.d.ts create mode 100644 test/compose.oauth-base.test.d.ts.map create mode 100644 test/compose.oauth-base.test.js create mode 100644 test/compose.oauth-base.test.js.map create mode 100644 test/compose.state.test.d.ts create mode 100644 test/compose.state.test.d.ts.map create mode 100644 test/compose.state.test.js create mode 100644 test/compose.state.test.js.map create mode 100644 test/contracts.apply-contract-map.test.d.ts create mode 100644 test/contracts.apply-contract-map.test.d.ts.map create mode 100644 test/contracts.apply-contract-map.test.js create mode 100644 test/contracts.apply-contract-map.test.js.map create mode 100644 test/contracts.apply-openapi-shape.test.d.ts create mode 100644 test/contracts.apply-openapi-shape.test.d.ts.map create mode 100644 test/contracts.apply-openapi-shape.test.js create mode 100644 test/contracts.apply-openapi-shape.test.js.map create mode 100644 test/contracts.pull-contract-map.test.d.ts create mode 100644 test/contracts.pull-contract-map.test.d.ts.map create mode 100644 test/contracts.pull-contract-map.test.js create mode 100644 test/contracts.pull-contract-map.test.js.map diff --git a/test/commands.apply-multi-env.test.d.ts b/test/commands.apply-multi-env.test.d.ts new file mode 100644 index 0000000..7ebc986 --- /dev/null +++ b/test/commands.apply-multi-env.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.apply-multi-env.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply-multi-env.test.d.ts.map b/test/commands.apply-multi-env.test.d.ts.map new file mode 100644 index 0000000..c0bfbc6 --- /dev/null +++ b/test/commands.apply-multi-env.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply-multi-env.test.d.ts","sourceRoot":"","sources":["commands.apply-multi-env.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply-multi-env.test.js b/test/commands.apply-multi-env.test.js new file mode 100644 index 0000000..f806b3b --- /dev/null +++ b/test/commands.apply-multi-env.test.js @@ -0,0 +1,163 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-multi-env-")); + const devDir = path.join(root, "env", "dev"); + const prodDir = path.join(root, "env", "prod"); + await fs.mkdir(devDir, { recursive: true }); + await fs.mkdir(prodDir, { recursive: true }); + await fs.writeFile(path.join(devDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(devDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(prodDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(prodDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + }, + prod: { + dir: "./env/prod", + description: "Prod", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return { root, devDir, prodDir }; +} +function installMockFetch() { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { + edges: [{ node: { environmentAlias: "dev" } }, { node: { environmentAlias: "prod" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/prod/collections") { + return jsonResponse(500, { error: "prod unavailable" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/prod/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(200, { edges: [] }); + }); + return () => { + globalThis.fetch = oldFetch; + }; +} +test("plan without --env evaluates all configured environments and warns if any environment warns", async () => { + const { root } = await createFixture(); + const restoreFetch = installMockFetch(); + const oldExitCode = process.exitCode; + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((a) => String(a)).join(" ")); + }; + let observedExitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + observedExitCode = process.exitCode; + } + finally { + restoreFetch(); + console.log = originalLog; + process.exitCode = oldExitCode; + } + assert.equal(output.some((line) => line.includes("[env=dev] collection:content")), true); + assert.equal(output.some((line) => line.includes("[env=prod] collection:*")), true); + assert.equal(output.some((line) => line.includes("Plan env dev: create=1, delete=0, update=0, noop=1, warn=0")), true); + assert.equal(output.some((line) => line.includes("Plan env prod: create=0, delete=0, update=0, noop=1, warn=1")), true); + assert.equal(output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), true); + assert.equal(output.some((line) => line.includes("Plan complete.")), true); + assert.equal(observedExitCode, 1); +}); +test("apply without --env writes lock/state for successful environments and skips failed environments", async () => { + const { root, devDir, prodDir } = await createFixture(); + const restoreFetch = installMockFetch(); + const oldExitCode = process.exitCode; + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((a) => String(a)).join(" ")); + }; + let observedExitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + observedExitCode = process.exitCode; + } + finally { + restoreFetch(); + console.log = originalLog; + process.exitCode = oldExitCode; + } + assert.equal(await exists(path.join(devDir, "compose.lock.json")), true); + assert.equal(await exists(path.join(prodDir, "compose.lock.json")), false); + const state = await readComposeState(root); + assert.ok(state); + const dev = state.environments.find((e) => e.alias === "dev"); + const prod = state.environments.find((e) => e.alias === "prod"); + assert.ok(dev); + assert.ok(prod); + assert.equal(dev.remoteAlias, "dev"); + assert.equal(prod.remoteAlias, undefined); + assert.equal(output.some((line) => line.includes("Apply env dev: create=1, delete=0, update=0, noop=1, warn=0")), true); + assert.equal(output.some((line) => line.includes("Apply env prod: create=0, delete=0, update=0, noop=1, warn=1")), true); + assert.equal(output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), true); + assert.equal(observedExitCode, 1); +}); +//# sourceMappingURL=commands.apply-multi-env.test.js.map \ No newline at end of file diff --git a/test/commands.apply-multi-env.test.js.map b/test/commands.apply-multi-env.test.js.map new file mode 100644 index 0000000..46d872c --- /dev/null +++ b/test/commands.apply-multi-env.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply-multi-env.test.js","sourceRoot":"","sources":["commands.apply-multi-env.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.d.ts b/test/commands.apply.auth-failures.test.d.ts new file mode 100644 index 0000000..361f5f6 --- /dev/null +++ b/test/commands.apply.auth-failures.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.apply.auth-failures.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.d.ts.map b/test/commands.apply.auth-failures.test.d.ts.map new file mode 100644 index 0000000..1f8fa50 --- /dev/null +++ b/test/commands.apply.auth-failures.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.auth-failures.test.d.ts","sourceRoot":"","sources":["commands.apply.auth-failures.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.js b/test/commands.apply.auth-failures.test.js new file mode 100644 index 0000000..8d92922 --- /dev/null +++ b/test/commands.apply.auth-failures.test.js @@ -0,0 +1,84 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createComposeRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-auth-fail-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return root; +} +test("runApply fails clearly when token endpoint returns non-200", async () => { + const root = await createComposeRoot(); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return new Response("bad client", { status: 401, headers: { "content-type": "text/plain" } }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await assert.rejects(() => runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }), /OAuth token request failed \(401\): bad client/); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("runApply fails clearly when token endpoint response lacks access_token", async () => { + const root = await createComposeRoot(); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { token_type: "Bearer", expires_in: 3600 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await assert.rejects(() => runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }), /OAuth token response missing access_token/); + } + finally { + globalThis.fetch = oldFetch; + } +}); +//# sourceMappingURL=commands.apply.auth-failures.test.js.map \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.js.map b/test/commands.apply.auth-failures.test.js.map new file mode 100644 index 0000000..0f60d74 --- /dev/null +++ b/test/commands.apply.auth-failures.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.auth-failures.test.js","sourceRoot":"","sources":["commands.apply.auth-failures.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.d.ts b/test/commands.apply.create-env-failure.test.d.ts new file mode 100644 index 0000000..090984b --- /dev/null +++ b/test/commands.apply.create-env-failure.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.apply.create-env-failure.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.d.ts.map b/test/commands.apply.create-env-failure.test.d.ts.map new file mode 100644 index 0000000..df7ff2a --- /dev/null +++ b/test/commands.apply.create-env-failure.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.create-env-failure.test.d.ts","sourceRoot":"","sources":["commands.apply.create-env-failure.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.js b/test/commands.apply.create-env-failure.test.js new file mode 100644 index 0000000..41ec976 --- /dev/null +++ b/test/commands.apply.create-env-failure.test.js @@ -0,0 +1,85 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createComposeRootForMissingEnv() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-create-env-fail-")); + const envDir = path.join(root, "env", "new-env"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const cfg = { + version: 1, + project: "test-project", + environments: { + "new-env": { + dir: "./env/new-env", + description: "New Env", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return { root, envDir }; +} +test("apply create-environment failure sets exitCode=1 and skips lock/state writes", async () => { + const { root, envDir } = await createComposeRootForMissingEnv(); + const oldFetch = globalThis.fetch; + const oldExitCode = process.exitCode; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(500, { error: "server-error" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "new-env", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assert.equal(process.exitCode, 1); + process.exitCode = oldExitCode; + const lockPath = path.join(envDir, "compose.lock.json"); + const lockExists = await exists(lockPath); + assert.equal(lockExists, false); + const state = await readComposeState(root); + assert.equal(state, null); +}); +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +//# sourceMappingURL=commands.apply.create-env-failure.test.js.map \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.js.map b/test/commands.apply.create-env-failure.test.js.map new file mode 100644 index 0000000..785af3a --- /dev/null +++ b/test/commands.apply.create-env-failure.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.create-env-failure.test.js","sourceRoot":"","sources":["commands.apply.create-env-failure.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.d.ts b/test/commands.apply.output-snapshots.test.d.ts new file mode 100644 index 0000000..e54e9fa --- /dev/null +++ b/test/commands.apply.output-snapshots.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.apply.output-snapshots.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.d.ts.map b/test/commands.apply.output-snapshots.test.d.ts.map new file mode 100644 index 0000000..f78ddbb --- /dev/null +++ b/test/commands.apply.output-snapshots.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.output-snapshots.test.d.ts","sourceRoot":"","sources":["commands.apply.output-snapshots.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.js b/test/commands.apply.output-snapshots.test.js new file mode 100644 index 0000000..0d541a4 --- /dev/null +++ b/test/commands.apply.output-snapshots.test.js @@ -0,0 +1,277 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createComposeRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-output-snapshot-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + return root; +} +async function createComposeRootWithCollections(collections) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-output-snapshot-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + return root; +} +function installMockFetch() { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + return () => { + globalThis.fetch = oldFetch; + }; +} +function installMockFetchWithHandler(handler) { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + return handler(u, method); + }); + return () => { + globalThis.fetch = oldFetch; + }; +} +function captureLogs() { + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + return { + output, + restore: () => { + console.log = originalLog; + } + }; +} +function operationLines(output) { + return output.filter((line) => /^(CREATE|DELETE|UPDATE|NOOP|WARN)\s+\[env=/.test(line)); +} +function finalSummaryLine(output) { + const line = output.find((l) => /(Plan|Apply) complete\./.test(l)); + assert.ok(line); + return line; +} +function environmentSummaryLine(output) { + const line = output.find((l) => /^Environments processed: /.test(l)); + assert.ok(line); + return line; +} +function perEnvironmentRunSummaryLine(output) { + const line = output.find((l) => /^(Plan|Apply) env /.test(l)); + assert.ok(line); + return line; +} +test("plan output operation lines and summary match expected snapshot", async () => { + const root = await createComposeRoot(); + const restoreFetch = installMockFetch(); + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } + finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + assert.deepEqual(operationLines(output), [ + "NOOP [env=dev] environment:dev", + "CREATE [env=dev] collection:content" + ]); + assert.equal(perEnvironmentRunSummaryLine(output), "Plan env dev: create=1, delete=0, update=0, noop=1, warn=0"); + assert.equal(environmentSummaryLine(output), "Environments processed: total=1, succeeded=1, warned=0"); + assert.equal(finalSummaryLine(output), "Plan complete. create=1, delete=0, update=0, noop=1, warn=0"); +}); +test("apply output operation lines and summary match expected snapshot", async () => { + const root = await createComposeRoot(); + const restoreFetch = installMockFetch(); + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } + finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + assert.deepEqual(operationLines(output), [ + "NOOP [env=dev] environment:dev", + "CREATE [env=dev] collection:content" + ]); + assert.equal(perEnvironmentRunSummaryLine(output), "Apply env dev: create=1, delete=0, update=0, noop=1, warn=0"); + assert.equal(environmentSummaryLine(output), "Environments processed: total=1, succeeded=1, warned=0"); + assert.equal(finalSummaryLine(output), "Apply complete. create=1, delete=0, update=0, noop=1, warn=0"); +}); +test("plan output shows inferred collection rename as update with explicit rename details", async () => { + const root = await createComposeRootWithCollections([{ alias: "content-new", description: "Main content" }]); + const restoreFetch = installMockFetchWithHandler((u, method) => { + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } + finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + assert.deepEqual(operationLines(output), [ + "NOOP [env=dev] environment:dev", + "UPDATE [env=dev] collection:content-new — rename content-old -> content-new" + ]); + assert.equal(perEnvironmentRunSummaryLine(output), "Plan env dev: create=0, delete=0, update=1, noop=1, warn=0"); + assert.equal(finalSummaryLine(output), "Plan complete. create=0, delete=0, update=1, noop=1, warn=0"); +}); +test("plan output for ambiguous collection rename fallback shows create/delete and no rename update line", async () => { + const root = await createComposeRootWithCollections([{ alias: "content-new", description: "Main content" }]); + const restoreFetch = installMockFetchWithHandler((u, method) => { + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content-old-a" } }, { node: { collectionAlias: "content-old-b" } }] + }); + } + if ((u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old-a" || + u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old-b") && + method === "GET") { + return jsonResponse(200, { description: "Main content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + const { output, restore } = captureLogs(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } + finally { + restoreFetch(); + restore(); + process.exitCode = oldExitCode; + } + const opLines = operationLines(output); + assert.equal(opLines.some((line) => line.includes("UPDATE [env=dev] collection:content-new — rename")), false); + assert.deepEqual(opLines, [ + "NOOP [env=dev] environment:dev", + "CREATE [env=dev] collection:content-new", + "DELETE [env=dev] collection:content-old-a — no unique rename match", + "DELETE [env=dev] collection:content-old-b — no unique rename match" + ]); + assert.equal(perEnvironmentRunSummaryLine(output), "Plan env dev: create=1, delete=2, update=0, noop=1, warn=0"); + assert.equal(finalSummaryLine(output), "Plan complete. create=1, delete=2, update=0, noop=1, warn=0"); +}); +//# sourceMappingURL=commands.apply.output-snapshots.test.js.map \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.js.map b/test/commands.apply.output-snapshots.test.js.map new file mode 100644 index 0000000..6207c08 --- /dev/null +++ b/test/commands.apply.output-snapshots.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.output-snapshots.test.js","sourceRoot":"","sources":["commands.apply.output-snapshots.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.d.ts b/test/commands.apply.partial-failure-writes.test.d.ts new file mode 100644 index 0000000..e274027 --- /dev/null +++ b/test/commands.apply.partial-failure-writes.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.apply.partial-failure-writes.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.d.ts.map b/test/commands.apply.partial-failure-writes.test.d.ts.map new file mode 100644 index 0000000..f14d7bb --- /dev/null +++ b/test/commands.apply.partial-failure-writes.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.partial-failure-writes.test.d.ts","sourceRoot":"","sources":["commands.apply.partial-failure-writes.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.js b/test/commands.apply.partial-failure-writes.test.js new file mode 100644 index 0000000..c2eb945 --- /dev/null +++ b/test/commands.apply.partial-failure-writes.test.js @@ -0,0 +1,90 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createComposeRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-partial-warn-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return { root, envDir }; +} +test("apply skips lock/state writes when nested resource create emits warning", async () => { + const { root, envDir } = await createComposeRoot(); + const oldFetch = globalThis.fetch; + const oldExitCode = process.exitCode; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(500, { error: "collection-create-fail" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assert.equal(process.exitCode, 1); + process.exitCode = oldExitCode; + const lockExists = await exists(path.join(envDir, "compose.lock.json")); + assert.equal(lockExists, false); + const state = await readComposeState(root); + assert.equal(state, null); +}); +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +//# sourceMappingURL=commands.apply.partial-failure-writes.test.js.map \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.js.map b/test/commands.apply.partial-failure-writes.test.js.map new file mode 100644 index 0000000..0d9e007 --- /dev/null +++ b/test/commands.apply.partial-failure-writes.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.partial-failure-writes.test.js","sourceRoot":"","sources":["commands.apply.partial-failure-writes.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.test.d.ts b/test/commands.apply.test.d.ts new file mode 100644 index 0000000..51c3e0f --- /dev/null +++ b/test/commands.apply.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.apply.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.test.d.ts.map b/test/commands.apply.test.d.ts.map new file mode 100644 index 0000000..c0cf00d --- /dev/null +++ b/test/commands.apply.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.test.d.ts","sourceRoot":"","sources":["commands.apply.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.test.js b/test/commands.apply.test.js new file mode 100644 index 0000000..97e1614 --- /dev/null +++ b/test/commands.apply.test.js @@ -0,0 +1,135 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; +async function createComposeRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-cmd-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Development", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + return { root, envDir }; +} +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +function installMockFetch(calls) { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") { + bodyText = init.body; + } + else if (init?.body instanceof URLSearchParams) { + bodyText = init.body.toString(); + } + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + return () => { + globalThis.fetch = oldFetch; + }; +} +test("runApply prints operation lines with explicit environment context", async () => { + const { root } = await createComposeRoot(); + const calls = []; + const restoreFetch = installMockFetch(calls); + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + const originalExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + } + finally { + restoreFetch(); + console.log = originalLog; + process.exitCode = originalExitCode; + } + assert.equal(output.some((line) => line.includes("[env=dev] environment:dev")), true); + assert.equal(output.some((line) => line.includes("[env=dev] collection:content")), true); + assert.equal(calls.length > 0, true); +}); +test("runApply writes compose.lock.json and updates compose.state.json after successful apply", async () => { + const { root, envDir } = await createComposeRoot(); + const calls = []; + const restoreFetch = installMockFetch(calls); + const originalExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + } + finally { + restoreFetch(); + process.exitCode = originalExitCode; + } + const lockPath = path.join(envDir, "compose.lock.json"); + const lockText = await fs.readFile(lockPath, "utf8"); + const lock = JSON.parse(lockText); + assert.equal(lock.version, 1); + assert.equal(lock.project, "test-project"); + assert.equal(lock.environment, "dev"); + assert.equal(typeof lock.lastApplied?.at, "string"); + assert.equal(typeof lock.resources.collections?.hash, "string"); + assert.equal(typeof lock.resources.webhooks?.hash, "string"); + const state = await readComposeState(root); + assert.ok(state); + const envState = state.environments.find((e) => e.alias === "dev"); + assert.ok(envState); + assert.equal(envState.remoteAlias, "dev"); +}); +//# sourceMappingURL=commands.apply.test.js.map \ No newline at end of file diff --git a/test/commands.apply.test.js.map b/test/commands.apply.test.js.map new file mode 100644 index 0000000..84b4f5a --- /dev/null +++ b/test/commands.apply.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.apply.test.js","sourceRoot":"","sources":["commands.apply.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAc3D,KAAK,UAAU,iBAAiB;IAC9B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,MAAM,GAAG,GAAkB;QACzB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,cAAc;QACvB,YAAY,EAAE;YACZ,GAAG,EAAE;gBACH,GAAG,EAAE,WAAW;gBAChB,WAAW,EAAE,aAAa;gBAC1B,iBAAiB,EAAE,4BAA4B;aAChD;SACF;KACF,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACrC,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAChE,MAAM,CACP,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,EAClC,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EACzC,MAAM,CACP,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,IAAa;IACjD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAkB;IAC1C,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,KAAwB,EAAE,IAAkB,EAAE,EAAE;QACzE,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QACpG,IAAI,QAA4B,CAAC;QACjC,IAAI,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YACnC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,CAAC;aAAM,IAAI,IAAI,EAAE,IAAI,YAAY,eAAe,EAAE,CAAC;YACjD,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEtC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,CAAC,QAAQ,KAAK,gBAAgB,EAAE,CAAC;YACpC,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,CAAC,CAAC,QAAQ,KAAK,wCAAwC,EAAE,CAAC;YAC5D,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/E,CAAC;QAED,IAAI,CAAC,CAAC,QAAQ,KAAK,wDAAwD,EAAE,CAAC;YAC5E,IAAI,MAAM,KAAK,KAAK;gBAAE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YAC9D,IAAI,MAAM,KAAK,MAAM;gBAAE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC,CAAC;QAClF,CAAC;QAED,IAAI,CAAC,CAAC,QAAQ,KAAK,qDAAqD,EAAE,CAAC;YACzE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;QAED,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACzD,CAAC,CAAiB,CAAC;IAEnB,OAAO,GAAG,EAAE;QACV,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC;AAED,IAAI,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;IACnF,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAE7C,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;QACnC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC;IAEF,MAAM,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC1C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IAErB,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC;YACb,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,KAAK;YACV,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;YACtB,IAAI,EAAE,IAAI;YACV,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,YAAY,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,GAAG,WAAW,CAAC;QAC1B,OAAO,CAAC,QAAQ,GAAG,gBAAgB,CAAC;IACtC,CAAC;IAED,MAAM,CAAC,KAAK,CACV,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,2BAA2B,CAAC,CAAC,EACjE,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CACV,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,8BAA8B,CAAC,CAAC,EACpE,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yFAAyF,EAAE,KAAK,IAAI,EAAE;IACzG,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,iBAAiB,EAAE,CAAC;IACnD,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAE7C,MAAM,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC1C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IAErB,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC;YACb,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,KAAK;YACV,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;YACtB,IAAI,EAAE,KAAK;YACX,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,YAAY,EAAE,CAAC;QACf,OAAO,CAAC,QAAQ,GAAG,gBAAgB,CAAC;IACtC,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAM/B,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC9B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACtC,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAChE,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAE7D,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACjB,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IACnE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IACpB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;AAC5C,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/test/commands.clone.test.d.ts b/test/commands.clone.test.d.ts new file mode 100644 index 0000000..12c6731 --- /dev/null +++ b/test/commands.clone.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.clone.test.d.ts.map \ No newline at end of file diff --git a/test/commands.clone.test.d.ts.map b/test/commands.clone.test.d.ts.map new file mode 100644 index 0000000..d45f1ad --- /dev/null +++ b/test/commands.clone.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.clone.test.d.ts","sourceRoot":"","sources":["commands.clone.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.clone.test.js b/test/commands.clone.test.js new file mode 100644 index 0000000..5114198 --- /dev/null +++ b/test/commands.clone.test.js @@ -0,0 +1,82 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { cloneCommand } from "../src/commands/clone.js"; +import { readComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +test("clone scaffolds project and pulls remote env state", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-clone-")); + const targetDir = path.join(root, "cloned-compose"); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/my-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/collections") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Main content" } }] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/my-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await cloneCommand.handler({ + dir: targetDir, + project: "my-project", + env: "dev", + baseUrl: "https://management.example", + clientId: "client", + clientSecret: "secret", + force: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + const composeYamlPath = path.join(targetDir, "umbraco-compose.yaml"); + const cfg = YAML.parse(await fs.readFile(composeYamlPath, "utf8")); + assert.equal(cfg.project, "my-project"); + assert.equal(cfg.environments.dev?.dir, "./env/dev"); + const collectionsPath = path.join(targetDir, "env", "dev", "collections.json"); + const collections = JSON.parse(await fs.readFile(collectionsPath, "utf8")); + assert.equal(collections.collections[0]?.alias, "content"); + const state = await readComposeState(targetDir); + assert.ok(state); + assert.equal(state.environments.find((e) => e.alias === "dev")?.remoteAlias, "dev"); +}); +test("clone fails when target directory is non-empty without --force", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-clone-nonempty-")); + await fs.writeFile(path.join(root, "keep.txt"), "keep\n", "utf8"); + await assert.rejects(() => cloneCommand.handler({ + dir: root, + project: "my-project", + env: "dev", + baseUrl: "https://management.example", + clientId: "client", + clientSecret: "secret", + force: false + }), /target directory is not empty/); +}); +//# sourceMappingURL=commands.clone.test.js.map \ No newline at end of file diff --git a/test/commands.clone.test.js.map b/test/commands.clone.test.js.map new file mode 100644 index 0000000..4b7cec7 --- /dev/null +++ b/test/commands.clone.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.clone.test.js","sourceRoot":"","sources":["commands.clone.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.env-rename.test.d.ts b/test/commands.env-rename.test.d.ts new file mode 100644 index 0000000..9318cf2 --- /dev/null +++ b/test/commands.env-rename.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.env-rename.test.d.ts.map \ No newline at end of file diff --git a/test/commands.env-rename.test.d.ts.map b/test/commands.env-rename.test.d.ts.map new file mode 100644 index 0000000..5e10345 --- /dev/null +++ b/test/commands.env-rename.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.env-rename.test.d.ts","sourceRoot":"","sources":["commands.env-rename.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.env-rename.test.js b/test/commands.env-rename.test.js new file mode 100644 index 0000000..eb41b7c --- /dev/null +++ b/test/commands.env-rename.test.js @@ -0,0 +1,148 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { envRenameCommand } from "../src/commands/env-rename.js"; +import { createInitialState, markEnvironmentRemoteAlias, readComposeState, writeComposeState } from "../src/compose/state.js"; +async function writeComposeConfig(rootDir, cfg) { + await fs.writeFile(path.join(rootDir, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); +} +async function readComposeConfig(rootDir) { + const text = await fs.readFile(path.join(rootDir, "umbraco-compose.yaml"), "utf8"); + return YAML.parse(text); +} +function baseConfig() { + return { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + }, + prod: { + dir: "./env/prod", + description: "Prod", + managementBaseUrl: "https://management.example" + } + } + }; +} +test("env rename updates umbraco-compose.yaml and compose.state.json while preserving env identity", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-")); + await writeComposeConfig(root, baseConfig()); + const state = createInitialState("test-project", ["dev", "prod"]); + const dev = state.environments.find((e) => e.alias === "dev"); + assert.ok(dev); + markEnvironmentRemoteAlias(state, dev.id, "dev"); + await writeComposeState(root, state); + await envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: false + }); + const cfgAfter = await readComposeConfig(root); + assert.ok(cfgAfter.environments.development); + assert.equal(cfgAfter.environments.dev, undefined); + assert.equal(cfgAfter.environments.development?.dir, "./env/dev"); + const stateAfter = await readComposeState(root); + assert.ok(stateAfter); + const renamed = stateAfter.environments.find((e) => e.alias === "development"); + assert.ok(renamed); + assert.equal(renamed.id, dev.id); + assert.equal(renamed.remoteAlias, "dev"); +}); +test("env rename with --moveDir renames default env directory path and folder", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-move-")); + await writeComposeConfig(root, baseConfig()); + await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); + await fs.mkdir(path.join(root, "env", "dev"), { recursive: true }); + await fs.writeFile(path.join(root, "env", "dev", "collections.json"), "{\"collections\":[]}\n", "utf8"); + await envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: true + }); + const cfgAfter = await readComposeConfig(root); + assert.equal(cfgAfter.environments.development?.dir, "./env/development"); + const oldExists = await exists(path.join(root, "env", "dev")); + const newExists = await exists(path.join(root, "env", "development")); + assert.equal(oldExists, false); + assert.equal(newExists, true); +}); +test("env rename fails when target alias already exists and leaves files unchanged", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-fail-")); + const cfg = baseConfig(); + await writeComposeConfig(root, cfg); + const state = createInitialState("test-project", ["dev", "prod"]); + await writeComposeState(root, state); + const beforeYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const beforeState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + await assert.rejects(() => envRenameCommand.handler({ + dir: root, + from: "dev", + to: "prod", + moveDir: false + })); + const afterYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const afterState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + assert.equal(afterYaml, beforeYaml); + assert.equal(afterState, beforeState); +}); +test("env rename --moveDir fails on destination collision and leaves config/state unchanged", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-collision-")); + await writeComposeConfig(root, baseConfig()); + await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); + await fs.mkdir(path.join(root, "env", "dev"), { recursive: true }); + await fs.mkdir(path.join(root, "env", "development"), { recursive: true }); + const beforeYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const beforeState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + await assert.rejects(() => envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: true + }), /destination already exists/); + const afterYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); + const afterState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); + assert.equal(afterYaml, beforeYaml); + assert.equal(afterState, beforeState); +}); +test("env rename --moveDir succeeds when source directory is missing", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-missing-src-")); + await writeComposeConfig(root, baseConfig()); + await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); + const oldExistsBefore = await exists(path.join(root, "env", "dev")); + assert.equal(oldExistsBefore, false); + await envRenameCommand.handler({ + dir: root, + from: "dev", + to: "development", + moveDir: true + }); + const cfgAfter = await readComposeConfig(root); + assert.equal(cfgAfter.environments.development?.dir, "./env/development"); + assert.equal(cfgAfter.environments.dev, undefined); + const stateAfter = await readComposeState(root); + assert.ok(stateAfter); + assert.ok(stateAfter.environments.find((e) => e.alias === "development")); + const oldExists = await exists(path.join(root, "env", "dev")); + const newExists = await exists(path.join(root, "env", "development")); + assert.equal(oldExists, false); + assert.equal(newExists, false); +}); +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +//# sourceMappingURL=commands.env-rename.test.js.map \ No newline at end of file diff --git a/test/commands.env-rename.test.js.map b/test/commands.env-rename.test.js.map new file mode 100644 index 0000000..395439a --- /dev/null +++ b/test/commands.env-rename.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.env-rename.test.js","sourceRoot":"","sources":["commands.env-rename.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.d.ts b/test/commands.generate-apply-flow.test.d.ts new file mode 100644 index 0000000..309fb42 --- /dev/null +++ b/test/commands.generate-apply-flow.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.generate-apply-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.d.ts.map b/test/commands.generate-apply-flow.test.d.ts.map new file mode 100644 index 0000000..7ffd0f3 --- /dev/null +++ b/test/commands.generate-apply-flow.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-apply-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-apply-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.js b/test/commands.generate-apply-flow.test.js new file mode 100644 index 0000000..2088203 --- /dev/null +++ b/test/commands.generate-apply-flow.test.js @@ -0,0 +1,170 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-apply-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} +test("generate -> apply writes lock/state and reports expected apply operations", async () => { + const { root, envDir } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Map content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "Webhook" + }); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "article" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { persistedDocumentAlias: "software-query" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + assert.equal(process.exitCode, 0); + } + finally { + globalThis.fetch = oldFetch; + console.log = originalLog; + process.exitCode = oldExitCode; + } + const lockPath = path.join(envDir, "compose.lock.json"); + const lockExists = await exists(lockPath); + assert.equal(lockExists, true); + const state = await readComposeState(root); + assert.ok(state); + const envState = state.environments.find((e) => e.alias === "dev"); + assert.ok(envState); + assert.equal(envState.remoteAlias, "dev"); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] collection:content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] type-schema:article")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] ingestion-function:map-content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); + assert.equal(output.some((line) => line.includes("Apply complete. create=5, delete=0, update=0, noop=1, warn=0")), true); +}); +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +//# sourceMappingURL=commands.generate-apply-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.js.map b/test/commands.generate-apply-flow.test.js.map new file mode 100644 index 0000000..091caf9 --- /dev/null +++ b/test/commands.generate-apply-flow.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-apply-flow.test.js","sourceRoot":"","sources":["commands.generate-apply-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.d.ts b/test/commands.generate-apply-warn-flow.test.d.ts new file mode 100644 index 0000000..7eb6304 --- /dev/null +++ b/test/commands.generate-apply-warn-flow.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.generate-apply-warn-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.d.ts.map b/test/commands.generate-apply-warn-flow.test.d.ts.map new file mode 100644 index 0000000..71d7ca6 --- /dev/null +++ b/test/commands.generate-apply-warn-flow.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-apply-warn-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-apply-warn-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.js b/test/commands.generate-apply-warn-flow.test.js new file mode 100644 index 0000000..a8b56e6 --- /dev/null +++ b/test/commands.generate-apply-warn-flow.test.js @@ -0,0 +1,163 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { runApply } from "../src/commands/apply.js"; +import { readComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-apply-warn-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} +test("generate -> apply warning path skips lock/state writes", async () => { + const { root, envDir } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Map content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "Webhook" + }); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "article" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(500, { error: "persisted-create-fail" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + assert.equal(process.exitCode, 1); + } + finally { + globalThis.fetch = oldFetch; + console.log = originalLog; + process.exitCode = oldExitCode; + } + const lockExists = await exists(path.join(envDir, "compose.lock.json")); + assert.equal(lockExists, false); + const state = await readComposeState(root); + assert.equal(state, null); + assert.equal(output.some((line) => line.includes("WARN") && line.includes("[env=dev] persisted-doc:software-query")), true); + assert.equal(output.some((line) => line.includes("Skipped lock/state writes because warnings were emitted.")), true); + assert.equal(output.some((line) => line.includes("Apply complete. create=5, delete=0, update=0, noop=1, warn=1")), true); +}); +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +//# sourceMappingURL=commands.generate-apply-warn-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.js.map b/test/commands.generate-apply-warn-flow.test.js.map new file mode 100644 index 0000000..c00adac --- /dev/null +++ b/test/commands.generate-apply-warn-flow.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-apply-warn-flow.test.js","sourceRoot":"","sources":["commands.generate-apply-warn-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-flow.test.d.ts b/test/commands.generate-flow.test.d.ts new file mode 100644 index 0000000..8c7996d --- /dev/null +++ b/test/commands.generate-flow.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.generate-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-flow.test.d.ts.map b/test/commands.generate-flow.test.d.ts.map new file mode 100644 index 0000000..1dad42b --- /dev/null +++ b/test/commands.generate-flow.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-flow.test.js b/test/commands.generate-flow.test.js new file mode 100644 index 0000000..821086d --- /dev/null +++ b/test/commands.generate-flow.test.js @@ -0,0 +1,150 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { validateCommand } from "../src/commands/validate.js"; +import { runApply } from "../src/commands/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-flow-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} +test("generate -> validate --strict -> plan baseline succeeds with expected operations", async () => { + const { root } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Map content" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "Webhook" + }); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await validateCommand.handler({ + dir: root, + strict: true + }); + assert.equal(process.exitCode, 0); + } + finally { + process.exitCode = oldExitCode; + } + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + const originalLog = console.log; + const output = []; + console.log = (...args) => { + output.push(args.map((x) => String(x)).join(" ")); + }; + const oldExitCode2 = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + assert.equal(process.exitCode, 0); + } + finally { + globalThis.fetch = oldFetch; + console.log = originalLog; + process.exitCode = oldExitCode2; + } + assert.equal(output.some((line) => line.includes("NOOP [env=dev] environment:dev")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] collection:content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] type-schema:article")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] ingestion-function:map-content")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); + assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); + assert.equal(output.some((line) => line.includes("Plan complete. create=5, delete=0, update=0, noop=1, warn=0")), true); +}); +//# sourceMappingURL=commands.generate-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-flow.test.js.map b/test/commands.generate-flow.test.js.map new file mode 100644 index 0000000..3993b46 --- /dev/null +++ b/test/commands.generate-flow.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-flow.test.js","sourceRoot":"","sources":["commands.generate-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.d.ts b/test/commands.generate-status-flow.test.d.ts new file mode 100644 index 0000000..d055fb4 --- /dev/null +++ b/test/commands.generate-status-flow.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.generate-status-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.d.ts.map b/test/commands.generate-status-flow.test.d.ts.map new file mode 100644 index 0000000..b33ce3f --- /dev/null +++ b/test/commands.generate-status-flow.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-status-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-status-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.js b/test/commands.generate-status-flow.test.js new file mode 100644 index 0000000..7e6f926 --- /dev/null +++ b/test/commands.generate-status-flow.test.js @@ -0,0 +1,147 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +import { statusCommand } from "../src/commands/status.js"; +import { runApply } from "../src/commands/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-status-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} +async function runStatusJson(args) { + const originalLog = console.log; + const out = []; + console.log = (...values) => { + out.push(values.map((v) => String(v)).join(" ")); + }; + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: args.root, + env: "dev", + json: true, + failOnChanges: args.failOnChanges + }); + const jsonText = out.find((line) => line.trim().startsWith("{")); + assert.ok(jsonText, "status command did not emit JSON output"); + return { + data: JSON.parse(jsonText), + exitCode: process.exitCode + }; + } + finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} +test("generate -> status reports NO LOCK and sets non-zero exit with --failOnChanges", async () => { + const { root } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + const result = await runStatusJson({ root, failOnChanges: true }); + const groups = result.data.results[0]?.groups; + assert.ok(groups); + assert.equal(groups.collections?.status, "NO LOCK"); + assert.equal(groups.webhooks?.status, "NO LOCK"); + assert.equal(result.exitCode, 1); +}); +test("generate -> apply -> status transitions from UNCHANGED to CHANGED after local edit", async () => { + const { root, envDir } = await createFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Content" + }); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(200, { edges: [] }); + }); + const oldExit = process.exitCode; + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: false, + requireClean: false + }); + assert.equal(process.exitCode, 0); + } + finally { + globalThis.fetch = oldFetch; + process.exitCode = oldExit; + } + const unchanged = await runStatusJson({ root, failOnChanges: true }); + const unchangedGroups = unchanged.data.results[0]?.groups; + assert.ok(unchangedGroups); + assert.equal(unchangedGroups.collections?.status, "UNCHANGED"); + assert.equal(unchanged.exitCode, 0); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "Content updated" }] }, null, 2) + "\n", "utf8"); + const changed = await runStatusJson({ root, failOnChanges: true }); + const changedGroups = changed.data.results[0]?.groups; + assert.ok(changedGroups); + assert.equal(changedGroups.collections?.status, "CHANGED"); + assert.equal(changed.exitCode, 1); +}); +//# sourceMappingURL=commands.generate-status-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.js.map b/test/commands.generate-status-flow.test.js.map new file mode 100644 index 0000000..6bbabc0 --- /dev/null +++ b/test/commands.generate-status-flow.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate-status-flow.test.js","sourceRoot":"","sources":["commands.generate-status-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate.test.d.ts b/test/commands.generate.test.d.ts new file mode 100644 index 0000000..dac4efe --- /dev/null +++ b/test/commands.generate.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.generate.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate.test.d.ts.map b/test/commands.generate.test.d.ts.map new file mode 100644 index 0000000..b054a0d --- /dev/null +++ b/test/commands.generate.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate.test.d.ts","sourceRoot":"","sources":["commands.generate.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate.test.js b/test/commands.generate.test.js new file mode 100644 index 0000000..dd023dc --- /dev/null +++ b/test/commands.generate.test.js @@ -0,0 +1,275 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { generateCommand } from "../src/commands/generate.js"; +async function createGenerateFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return { root, envDir }; +} +test("generate collection appends a collection entry", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Main content" + }); + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); + assert.equal(collections.collections.length, 1); + assert.equal(collections.collections[0]?.alias, "content"); + assert.equal(collections.collections[0]?.description, "Main content"); +}); +test("generate type-schema creates schema file", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article", + description: "Article schema" + }); + const schemaPath = path.join(envDir, "type-schemas", "article.schema.json"); + const schema = JSON.parse(await fs.readFile(schemaPath, "utf8")); + assert.equal(schema.alias, "article"); + assert.equal(schema.description, "Article schema"); + assert.equal(schema.schema.$schema, "https://umbracocompose.com/v1/schema"); + assert.ok(Array.isArray(schema.schema.allOf)); +}); +test("generate ingestion-function creates script and updates registry", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content", + description: "Maps content" + }); + const registry = JSON.parse(await fs.readFile(path.join(envDir, "functions", "ingestion.json"), "utf8")); + assert.equal(registry.functions.length, 1); + assert.equal(registry.functions[0]?.alias, "map-content"); + assert.equal(registry.functions[0]?.scriptFile, "functions/ingestion/map-content.js"); + const script = await fs.readFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "utf8"); + assert.equal(script.includes("export default function"), true); +}); +test("generate persisted-doc creates query file and updates registry", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query", + description: "Software query" + }); + const registry = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")); + assert.equal(registry.documents.length, 1); + assert.equal(registry.documents[0]?.alias, "software-query"); + assert.equal(registry.documents[0]?.description, "Software query"); + assert.equal(registry.documents[0]?.queryFile, "graphql/persisted/software-query.gql"); + const query = await fs.readFile(path.join(envDir, "graphql", "persisted", "software-query.gql"), "utf8"); + assert.equal(query.includes("query Example"), true); +}); +test("generate webhook creates webhook entry with default URL", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-create", + description: "My webhook" + }); + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); + assert.equal(webhooks.webhooks.length, 1); + assert.equal(webhooks.webhooks[0]?.alias, "send-on-create"); + assert.equal(webhooks.webhooks[0]?.description, "My webhook"); + assert.equal(webhooks.webhooks[0]?.url, "https://example.com/webhook"); + assert.deepEqual(webhooks.webhooks[0]?.eventTypes, ["content.ingested"]); + assert.deepEqual(webhooks.webhooks[0]?.collectionAliases, []); + assert.deepEqual(webhooks.webhooks[0]?.typeSchemaAliases, []); + assert.deepEqual(webhooks.webhooks[0]?.customHeaders, {}); +}); +test("generate webhook accepts explicit URL", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "send-on-update", + description: "URL override", + url: "https://hooks.example.com/compose" + }); + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); + assert.equal(webhooks.webhooks[0]?.alias, "send-on-update"); + assert.equal(webhooks.webhooks[0]?.url, "https://hooks.example.com/compose"); +}); +test("generate rejects duplicate aliases", async () => { + const { root } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Main content" + }); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + description: "Duplicate" + })); +}); +test("generate rejects --url for non-webhook entities", async () => { + const { root, envDir } = await createGenerateFixture(); + const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "content", + url: "https://hooks.example.com/compose" + }), /--url is only supported for entity=webhook/); + const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + assert.equal(after, before); +}); +test("generate rejects invalid alias format", async () => { + const { root, envDir } = await createGenerateFixture(); + const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "Not_Kebab" + }), /must be kebab-case/); + const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + assert.equal(after, before); +}); +test("generate rejects reserved alias names", async () => { + const { root, envDir } = await createGenerateFixture(); + const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "con" + }), /Reserved filename/); + const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); + assert.equal(after, before); +}); +test("generate type-schema rejects when schema file already exists", async () => { + const { root, envDir } = await createGenerateFixture(); + const schemaPath = path.join(envDir, "type-schemas", "article.schema.json"); + await fs.writeFile(schemaPath, JSON.stringify({ + alias: "article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [], + properties: {} + } + }, null, 2) + "\n", "utf8"); + const before = await fs.readFile(schemaPath, "utf8"); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "article" + }), /already exists/); + const after = await fs.readFile(schemaPath, "utf8"); + assert.equal(after, before); +}); +test("generate ingestion-function rejects when script file already exists", async () => { + const { root, envDir } = await createGenerateFixture(); + const scriptPath = path.join(envDir, "functions", "ingestion", "map-content.js"); + await fs.writeFile(scriptPath, "export default () => null;\n", "utf8"); + const registryPath = path.join(envDir, "functions", "ingestion.json"); + const beforeRegistry = await fs.readFile(registryPath, "utf8"); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "ingestion-function", + alias: "map-content" + }), /script already exists/); + const afterRegistry = await fs.readFile(registryPath, "utf8"); + assert.equal(afterRegistry, beforeRegistry); +}); +test("generate persisted-doc rejects when query file already exists", async () => { + const { root, envDir } = await createGenerateFixture(); + const queryPath = path.join(envDir, "graphql", "persisted", "software-query.gql"); + await fs.writeFile(queryPath, "query Existing { __typename }\n", "utf8"); + const registryPath = path.join(envDir, "graphql", "persisted.json"); + const beforeRegistry = await fs.readFile(registryPath, "utf8"); + await assert.rejects(() => generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "software-query" + }), /query already exists/); + const afterRegistry = await fs.readFile(registryPath, "utf8"); + assert.equal(afterRegistry, beforeRegistry); +}); +test("generate allows same alias across different entity types", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "collection", + alias: "shared-alias", + description: "Shared collection alias" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "type-schema", + alias: "shared-alias", + description: "Shared schema alias" + }); + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); + const schema = JSON.parse(await fs.readFile(path.join(envDir, "type-schemas", "shared-alias.schema.json"), "utf8")); + assert.equal(collections.collections.some((c) => c.alias === "shared-alias"), true); + assert.equal(schema.alias, "shared-alias"); +}); +test("generate allows same alias for webhook and persisted-doc", async () => { + const { root, envDir } = await createGenerateFixture(); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "webhook", + alias: "shared", + description: "Webhook alias" + }); + await generateCommand.handler({ + dir: root, + env: "dev", + entity: "persisted-doc", + alias: "shared", + description: "Persisted alias" + }); + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); + const persisted = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")); + assert.equal(webhooks.webhooks.some((w) => w.alias === "shared"), true); + assert.equal(persisted.documents.some((d) => d.alias === "shared"), true); +}); +//# sourceMappingURL=commands.generate.test.js.map \ No newline at end of file diff --git a/test/commands.generate.test.js.map b/test/commands.generate.test.js.map new file mode 100644 index 0000000..4799550 --- /dev/null +++ b/test/commands.generate.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.generate.test.js","sourceRoot":"","sources":["commands.generate.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.plan-exit.test.d.ts b/test/commands.plan-exit.test.d.ts new file mode 100644 index 0000000..e127170 --- /dev/null +++ b/test/commands.plan-exit.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.plan-exit.test.d.ts.map \ No newline at end of file diff --git a/test/commands.plan-exit.test.d.ts.map b/test/commands.plan-exit.test.d.ts.map new file mode 100644 index 0000000..559d42e --- /dev/null +++ b/test/commands.plan-exit.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.plan-exit.test.d.ts","sourceRoot":"","sources":["commands.plan-exit.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.plan-exit.test.js b/test/commands.plan-exit.test.js new file mode 100644 index 0000000..a77deb8 --- /dev/null +++ b/test/commands.plan-exit.test.js @@ -0,0 +1,72 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { runApply } from "../src/commands/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createComposeRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-plan-exit-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(envDir, { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return root; +} +test("plan mode sets process.exitCode=1 when warnings are produced", async () => { + const root = await createComposeRoot(); + const oldFetch = globalThis.fetch; + const oldExitCode = process.exitCode; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(500, { error: "unavailable" }); + } + if (method === "GET") + return jsonResponse(200, { edges: [] }); + return jsonResponse(404, { error: "unexpected path" }); + }); + process.exitCode = 0; + try { + await runApply({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret", + plan: true, + requireClean: false + }); + assert.equal(process.exitCode, 1); + } + finally { + globalThis.fetch = oldFetch; + process.exitCode = oldExitCode; + } +}); +//# sourceMappingURL=commands.plan-exit.test.js.map \ No newline at end of file diff --git a/test/commands.plan-exit.test.js.map b/test/commands.plan-exit.test.js.map new file mode 100644 index 0000000..2dd3cf1 --- /dev/null +++ b/test/commands.plan-exit.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.plan-exit.test.js","sourceRoot":"","sources":["commands.plan-exit.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.pull-hardening.test.d.ts b/test/commands.pull-hardening.test.d.ts new file mode 100644 index 0000000..4c8ae5b --- /dev/null +++ b/test/commands.pull-hardening.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.pull-hardening.test.d.ts.map \ No newline at end of file diff --git a/test/commands.pull-hardening.test.d.ts.map b/test/commands.pull-hardening.test.d.ts.map new file mode 100644 index 0000000..38826e5 --- /dev/null +++ b/test/commands.pull-hardening.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.pull-hardening.test.d.ts","sourceRoot":"","sources":["commands.pull-hardening.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.pull-hardening.test.js b/test/commands.pull-hardening.test.js new file mode 100644 index 0000000..4d8a06c --- /dev/null +++ b/test/commands.pull-hardening.test.js @@ -0,0 +1,216 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import crypto from "node:crypto"; +import YAML from "yaml"; +import { pullCommand } from "../src/commands/pull.js"; +import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-hardening-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await writeComposeState(root, createInitialState("test-project", ["dev"])); + return { root, envDir }; +} +function installSuccessfulPullFetch() { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Main content" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content", description: "Maps content" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/map-content") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "software-query", description: "Software query" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/software-query") { + return jsonResponse(200, { + persistedDocumentAlias: "software-query", + description: "Software query", + document: "query Software { content { items { __typename } } }" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/send-on-create") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + description: "Webhook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { "x-api-key": "abc123" } + }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + return () => { + globalThis.fetch = oldFetch; + }; +} +async function snapshotDir(root) { + const files = await listFiles(root); + const parts = []; + for (const rel of files) { + const abs = path.join(root, rel); + const text = await fs.readFile(abs, "utf8"); + parts.push({ + file: rel, + hash: sha(text) + }); + } + parts.sort((a, b) => a.file.localeCompare(b.file)); + return sha(JSON.stringify(parts)); +} +test("pull is idempotent for unchanged remote responses", async () => { + const { root, envDir } = await createFixture(); + const restoreFetch = installSuccessfulPullFetch(); + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + const first = await snapshotDir(envDir); + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + const second = await snapshotDir(envDir); + assert.equal(second, first); + } + finally { + restoreFetch(); + } +}); +test("pull partial failure keeps previous state remoteAlias unchanged and does not partially mutate local files", async () => { + const { root, envDir } = await createFixture(); + // First do a successful pull to set baseline and state + const restoreSuccess = installSuccessfulPullFetch(); + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + } + finally { + restoreSuccess(); + } + const stateBefore = await readComposeState(root); + assert.ok(stateBefore); + const remoteBefore = stateBefore.environments.find((e) => e.alias === "dev")?.remoteAlias; + assert.equal(remoteBefore, "dev"); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Changed before fail" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(500, { error: "type-schema-list-fail" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await assert.rejects(() => pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }), /Failed to list type schemas \(500\)\./); + } + finally { + globalThis.fetch = oldFetch; + } + const stateAfter = await readComposeState(root); + assert.ok(stateAfter); + const remoteAfter = stateAfter.environments.find((e) => e.alias === "dev")?.remoteAlias; + // state is not advanced during failed pull + assert.equal(remoteAfter, "dev"); + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); + assert.equal(collections.collections[0]?.description, "Main content"); +}); +async function listFiles(root) { + const out = []; + async function walk(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const abs = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(abs); + } + else if (entry.isFile()) { + out.push(path.relative(root, abs)); + } + } + } + await walk(root); + return out; +} +function sha(s) { + return crypto.createHash("sha256").update(s).digest("hex"); +} +//# sourceMappingURL=commands.pull-hardening.test.js.map \ No newline at end of file diff --git a/test/commands.pull-hardening.test.js.map b/test/commands.pull-hardening.test.js.map new file mode 100644 index 0000000..2c62c81 --- /dev/null +++ b/test/commands.pull-hardening.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.pull-hardening.test.js","sourceRoot":"","sources":["commands.pull-hardening.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAQlG,SAAS,YAAY,CAAC,MAAc,EAAE,IAAa;IACjD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,aAAa;IAC1B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/E,MAAM,GAAG,GAAkB;QACzB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,cAAc;QACvB,YAAY,EAAE;YACZ,GAAG,EAAE;gBACH,GAAG,EAAE,WAAW;gBAChB,WAAW,EAAE,KAAK;gBAClB,iBAAiB,EAAE,4BAA4B;aAChD;SACF;KACF,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACzF,MAAM,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,CAAC,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,0BAA0B;IACjC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,KAAwB,EAAE,EAAE;QACrD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QACpG,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvB,IAAI,CAAC,CAAC,QAAQ,KAAK,gBAAgB;YAAE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3G,IAAI,CAAC,CAAC,QAAQ,KAAK,wCAAwC,EAAE,CAAC;YAC5D,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,wDAAwD,EAAE,CAAC;YAC5E,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/G,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,yDAAyD,EAAE,CAAC;YAC7E,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,iEAAiE,EAAE,CAAC;YACrF,OAAO,YAAY,CAAC,GAAG,EAAE;gBACvB,eAAe,EAAE,SAAS;gBAC1B,WAAW,EAAE,gBAAgB;gBAC7B,MAAM,EAAE;oBACN,OAAO,EAAE,sCAAsC;oBAC/C,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,oCAAoC,EAAE,CAAC;oBACvD,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;iBAC1C;aACF,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,gEAAgE,EAAE,CAAC;YACpF,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,sBAAsB,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC1H,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,4EAA4E,EAAE,CAAC;YAChG,OAAO,YAAY,CAAC,GAAG,EAAE;gBACvB,sBAAsB,EAAE,aAAa;gBACrC,WAAW,EAAE,cAAc;gBAC3B,MAAM,EAAE,0BAA0B;aACnC,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,wEAAwE,EAAE,CAAC;YAC5F,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/H,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,uFAAuF,EAAE,CAAC;YAC3G,OAAO,YAAY,CAAC,GAAG,EAAE;gBACvB,sBAAsB,EAAE,gBAAgB;gBACxC,WAAW,EAAE,gBAAgB;gBAC7B,QAAQ,EAAE,qDAAqD;aAChE,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,qDAAqD,EAAE,CAAC;YACzE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,oEAAoE,EAAE,CAAC;YACxF,OAAO,YAAY,CAAC,GAAG,EAAE;gBACvB,YAAY,EAAE,gBAAgB;gBAC9B,WAAW,EAAE,SAAS;gBACtB,GAAG,EAAE,0BAA0B;gBAC/B,UAAU,EAAE,CAAC,kBAAkB,CAAC;gBAChC,iBAAiB,EAAE,CAAC,SAAS,CAAC;gBAC9B,iBAAiB,EAAE,CAAC,SAAS,CAAC;gBAC9B,aAAa,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE;aACzC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACzD,CAAC,CAAiB,CAAC;IAEnB,OAAO,GAAG,EAAE;QACV,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAY;IACrC,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,KAAK,GAA0C,EAAE,CAAC;IACxD,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC5C,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,GAAG;YACT,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC;SAChB,CAAC,CAAC;IACL,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,OAAO,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;IACnE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;IAC/C,MAAM,YAAY,GAAG,0BAA0B,EAAE,CAAC;IAElD,IAAI,CAAC;QACH,MAAM,WAAW,CAAC,OAAO,CAAC;YACxB,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,KAAK;YACV,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QAExC,MAAM,WAAW,CAAC,OAAO,CAAC;YACxB,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,KAAK;YACV,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QAEzC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9B,CAAC;YAAS,CAAC;QACT,YAAY,EAAE,CAAC;IACjB,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2GAA2G,EAAE,KAAK,IAAI,EAAE;IAC3H,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;IAC/C,uDAAuD;IACvD,MAAM,cAAc,GAAG,0BAA0B,EAAE,CAAC;IACpD,IAAI,CAAC;QACH,MAAM,WAAW,CAAC,OAAO,CAAC;YACxB,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,KAAK;YACV,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,cAAc,EAAE,CAAC;IACnB,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC;IACvB,MAAM,YAAY,GAAG,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,WAAW,CAAC;IAC1F,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IAElC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,KAAwB,EAAE,EAAE;QACrD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QACpG,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,CAAC,QAAQ,KAAK,gBAAgB;YAAE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3G,IAAI,CAAC,CAAC,QAAQ,KAAK,wCAAwC;YAAE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC1I,IAAI,CAAC,CAAC,QAAQ,KAAK,wDAAwD,EAAE,CAAC;YAC5E,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,qBAAqB,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACtH,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,yDAAyD,EAAE,CAAC;YAC7E,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACzD,CAAC,CAAiB,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CACH,WAAW,CAAC,OAAO,CAAC;YAClB,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,KAAK;YACV,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,EACJ,uCAAuC,CACxC,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;IACtB,MAAM,WAAW,GAAG,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,WAAW,CAAC;IACxF,2CAA2C;IAC3C,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAEjC,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAE9F,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AACxE,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,SAAS,CAAC,IAAY;IACnC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,UAAU,IAAI,CAAC,GAAW;QAC7B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC"} \ No newline at end of file diff --git a/test/commands.pull.test.d.ts b/test/commands.pull.test.d.ts new file mode 100644 index 0000000..568a5c4 --- /dev/null +++ b/test/commands.pull.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.pull.test.d.ts.map \ No newline at end of file diff --git a/test/commands.pull.test.d.ts.map b/test/commands.pull.test.d.ts.map new file mode 100644 index 0000000..fe9d66e --- /dev/null +++ b/test/commands.pull.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.pull.test.d.ts","sourceRoot":"","sources":["commands.pull.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.pull.test.js b/test/commands.pull.test.js new file mode 100644 index 0000000..69301ee --- /dev/null +++ b/test/commands.pull.test.js @@ -0,0 +1,241 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { pullCommand } from "../src/commands/pull.js"; +import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + // stale files that should be removed on pull + await fs.writeFile(path.join(envDir, "type-schemas", "stale.schema.json"), "{}\n", "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "stale.js"), "export default null;\n", "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "stale.gql"), "query Stale { __typename }\n", "utf8"); + await writeComposeState(root, createInitialState("test-project", ["dev"])); + return { root, envDir }; +} +test("pull writes local files from API and updates state remoteAlias", async () => { + const { root, envDir } = await createFixture(); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content", description: "Main content" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { + edges: [{ node: { ingestionFunctionAlias: "map-content", description: "Maps content" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/map-content") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { + edges: [{ node: { persistedDocumentAlias: "software-query", description: "Software query" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/software-query") { + return jsonResponse(200, { + persistedDocumentAlias: "software-query", + description: "Software query", + document: "query Software { content { items { __typename } } }" + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/send-on-create") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + description: "Webhook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { "x-api-key": "abc123" } + }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + } + finally { + globalThis.fetch = oldFetch; + } + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); + assert.equal(collections.collections[0]?.alias, "content"); + const schema = JSON.parse(await fs.readFile(path.join(envDir, "type-schemas", "article.schema.json"), "utf8")); + assert.equal(schema.alias, "article"); + assert.equal(await exists(path.join(envDir, "type-schemas", "stale.schema.json")), false); + const ingestionRegistry = JSON.parse(await fs.readFile(path.join(envDir, "functions", "ingestion.json"), "utf8")); + assert.equal(ingestionRegistry.functions[0]?.alias, "map-content"); + assert.equal(await exists(path.join(envDir, "functions", "ingestion", "stale.js")), false); + const persistedRegistry = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")); + assert.equal(persistedRegistry.documents[0]?.alias, "software-query"); + assert.equal(await exists(path.join(envDir, "graphql", "persisted", "stale.gql")), false); + const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); + assert.equal(webhooks.webhooks[0]?.alias, "send-on-create"); + const state = await readComposeState(root); + assert.ok(state); + assert.equal(state.environments.find((e) => e.alias === "dev")?.remoteAlias, "dev"); +}); +test("pull fails when target environment is not present remotely", async () => { + const { root } = await createFixture(); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "other" } }] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await assert.rejects(() => pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }), /Environment "dev" was not found remotely/); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("pull uses state remoteAlias when local alias differs during pending rename", async () => { + const { root, envDir } = await createFixture(); + const composePath = path.join(root, "umbraco-compose.yaml"); + const cfgText = await fs.readFile(composePath, "utf8"); + const cfg = YAML.parse(cfgText); + cfg.environments = { + "dev-renamed": { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + }; + await fs.writeFile(composePath, YAML.stringify(cfg), "utf8"); + const state = await readComposeState(root); + assert.ok(state); + state.environments[0].alias = "dev-renamed"; + state.environments[0].remoteAlias = "dev"; + await writeComposeState(root, state); + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content", description: "Main content" } }] + }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await pullCommand.handler({ + dir: root, + env: "dev-renamed", + clientId: "client", + clientSecret: "secret" + }); + } + finally { + globalThis.fetch = oldFetch; + } + const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); + assert.equal(collections.collections[0]?.alias, "content"); + const after = await readComposeState(root); + assert.ok(after); + const envState = after.environments.find((e) => e.alias === "dev-renamed"); + assert.ok(envState); + assert.equal(envState.remoteAlias, "dev"); +}); +async function exists(p) { + try { + await fs.stat(p); + return true; + } + catch { + return false; + } +} +//# sourceMappingURL=commands.pull.test.js.map \ No newline at end of file diff --git a/test/commands.pull.test.js.map b/test/commands.pull.test.js.map new file mode 100644 index 0000000..a186d5e --- /dev/null +++ b/test/commands.pull.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.pull.test.js","sourceRoot":"","sources":["commands.pull.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.status-multi-env.test.d.ts b/test/commands.status-multi-env.test.d.ts new file mode 100644 index 0000000..ab7f899 --- /dev/null +++ b/test/commands.status-multi-env.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.status-multi-env.test.d.ts.map \ No newline at end of file diff --git a/test/commands.status-multi-env.test.d.ts.map b/test/commands.status-multi-env.test.d.ts.map new file mode 100644 index 0000000..67561a0 --- /dev/null +++ b/test/commands.status-multi-env.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.status-multi-env.test.d.ts","sourceRoot":"","sources":["commands.status-multi-env.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.status-multi-env.test.js b/test/commands.status-multi-env.test.js new file mode 100644 index 0000000..a9de2ac --- /dev/null +++ b/test/commands.status-multi-env.test.js @@ -0,0 +1,140 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { statusCommand } from "../src/commands/status.js"; +import { computeDesiredHashes } from "../src/compose/lock.js"; +import { ensureStateForAliases, markEnvironmentRemoteAlias, writeComposeState } from "../src/compose/state.js"; +async function createEnvScaffold(envDir) { + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-status-multi-env-")); + const devDir = path.join(root, "env", "dev"); + const prodDir = path.join(root, "env", "prod"); + await createEnvScaffold(devDir); + await createEnvScaffold(prodDir); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + }, + prod: { + dir: "./env/prod", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + return { root, devDir, prodDir }; +} +async function writeMatchingLock(envDir) { + const desired = await computeDesiredHashes(envDir); + const lock = { + version: 1, + project: "test-project", + environment: path.basename(envDir), + resources: desired + }; + await fs.writeFile(path.join(envDir, "compose.lock.json"), JSON.stringify(lock, null, 2) + "\n", "utf8"); +} +async function runStatusJson(args) { + const originalLog = console.log; + const out = []; + console.log = (...values) => { + out.push(values.map((v) => String(v)).join(" ")); + }; + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: args.root, + json: true, + failOnChanges: args.failOnChanges + }); + const jsonText = out.find((line) => line.trim().startsWith("{")); + assert.ok(jsonText, "status command did not emit JSON output"); + return { + data: JSON.parse(jsonText), + exitCode: process.exitCode + }; + } + finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} +async function runStatusHuman(args) { + const originalLog = console.log; + const out = []; + console.log = (...values) => { + out.push(values.map((v) => String(v)).join(" ")); + }; + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: args.root, + env: args.env, + json: false, + failOnChanges: args.failOnChanges + }); + return { output: out, exitCode: process.exitCode }; + } + finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} +test("status without --env returns results for all configured environments", async () => { + const { root, devDir, prodDir } = await createFixture(); + await writeMatchingLock(devDir); + await writeMatchingLock(prodDir); + const result = await runStatusJson({ root, failOnChanges: true }); + const envs = result.data.results.map((r) => r.env).sort(); + assert.deepEqual(envs, ["dev", "prod"]); + assert.equal(result.exitCode, 0); +}); +test("status across multiple environments sets exitCode=1 when any environment has changes/no lock", async () => { + const { root, devDir } = await createFixture(); + await writeMatchingLock(devDir); + // prod intentionally has no lock + const result = await runStatusJson({ root, failOnChanges: true }); + const dev = result.data.results.find((r) => r.env === "dev"); + const prod = result.data.results.find((r) => r.env === "prod"); + assert.ok(dev); + assert.ok(prod); + assert.equal(dev.groups.collections?.status, "UNCHANGED"); + assert.equal(prod.groups.collections?.status, "NO LOCK"); + assert.equal(result.exitCode, 1); +}); +test("status includes state identity context for pending rename in json and human output", async () => { + const { root, devDir } = await createFixture(); + await writeMatchingLock(devDir); + const aliases = ["dev", "prod"]; + const state = ensureStateForAliases(null, "test-project", aliases).state; + const devState = state.environments.find((e) => e.alias === "dev"); + assert.ok(devState); + const marked = markEnvironmentRemoteAlias(state, devState.id, "old-dev"); + assert.equal(marked, true); + await writeComposeState(root, state); + const jsonResult = await runStatusJson({ root, failOnChanges: false }); + const devJson = jsonResult.data.results.find((r) => r.env === "dev"); + assert.ok(devJson); + assert.equal(devJson.remoteAlias, "old-dev"); + assert.equal(devJson.renamePending, true); + const humanResult = await runStatusHuman({ root, env: "dev", failOnChanges: false }); + assert.equal(humanResult.output.some((line) => line.includes("Identity: pending rename (old-dev -> dev)")), true); +}); +//# sourceMappingURL=commands.status-multi-env.test.js.map \ No newline at end of file diff --git a/test/commands.status-multi-env.test.js.map b/test/commands.status-multi-env.test.js.map new file mode 100644 index 0000000..357c2a6 --- /dev/null +++ b/test/commands.status-multi-env.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.status-multi-env.test.js","sourceRoot":"","sources":["commands.status-multi-env.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.d.ts b/test/commands.status-validate-exit.test.d.ts new file mode 100644 index 0000000..0e047f3 --- /dev/null +++ b/test/commands.status-validate-exit.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.status-validate-exit.test.d.ts.map \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.d.ts.map b/test/commands.status-validate-exit.test.d.ts.map new file mode 100644 index 0000000..56a0f75 --- /dev/null +++ b/test/commands.status-validate-exit.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.status-validate-exit.test.d.ts","sourceRoot":"","sources":["commands.status-validate-exit.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.js b/test/commands.status-validate-exit.test.js new file mode 100644 index 0000000..a776f39 --- /dev/null +++ b/test/commands.status-validate-exit.test.js @@ -0,0 +1,87 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { statusCommand } from "../src/commands/status.js"; +import { validateCommand } from "../src/commands/validate.js"; +async function createStatusFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-status-exit-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return root; +} +async function createValidateFixtureWithWarnings() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-validate-exit-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "Not-Kebab" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + return root; +} +test("status --failOnChanges sets process.exitCode=1 when lock is missing", async () => { + const root = await createStatusFixture(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await statusCommand.handler({ + dir: root, + env: "dev", + json: true, + failOnChanges: true + }); + assert.equal(process.exitCode, 1); + } + finally { + process.exitCode = oldExitCode; + } +}); +test("validate --strict sets process.exitCode=1 when warnings exist", async () => { + const root = await createValidateFixtureWithWarnings(); + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await validateCommand.handler({ + dir: root, + strict: true + }); + assert.equal(process.exitCode, 1); + } + finally { + process.exitCode = oldExitCode; + } +}); +//# sourceMappingURL=commands.status-validate-exit.test.js.map \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.js.map b/test/commands.status-validate-exit.test.js.map new file mode 100644 index 0000000..891336d --- /dev/null +++ b/test/commands.status-validate-exit.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.status-validate-exit.test.js","sourceRoot":"","sources":["commands.status-validate-exit.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.d.ts b/test/commands.validate-rename-risk.test.d.ts new file mode 100644 index 0000000..903a1df --- /dev/null +++ b/test/commands.validate-rename-risk.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=commands.validate-rename-risk.test.d.ts.map \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.d.ts.map b/test/commands.validate-rename-risk.test.d.ts.map new file mode 100644 index 0000000..4690ca1 --- /dev/null +++ b/test/commands.validate-rename-risk.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.validate-rename-risk.test.d.ts","sourceRoot":"","sources":["commands.validate-rename-risk.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.js b/test/commands.validate-rename-risk.test.js new file mode 100644 index 0000000..fcc9073 --- /dev/null +++ b/test/commands.validate-rename-risk.test.js @@ -0,0 +1,79 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { validateCommand } from "../src/commands/validate.js"; +async function createFixtureWithCollectionRenameRisk() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-validate-rename-risk-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ + collections: [ + { alias: "content-a", description: "Same description" }, + { alias: "content-b", description: "Same description" } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + // Keep schema checks green so test isolates rename-risk warning behavior. + await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ + alias: "article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: {} + } + }, null, 2), "utf8"); + return root; +} +async function runValidateCapture(args) { + const originalLog = console.log; + const output = []; + console.log = (...values) => { + output.push(values.map((v) => String(v)).join(" ")); + }; + const oldExitCode = process.exitCode; + process.exitCode = 0; + try { + await validateCommand.handler({ + dir: args.root, + strict: args.strict + }); + return { output, exitCode: process.exitCode }; + } + finally { + console.log = originalLog; + process.exitCode = oldExitCode; + } +} +test("validate warns on duplicate rename-signatures in non-strict mode without failing", async () => { + const root = await createFixtureWithCollectionRenameRisk(); + const result = await runValidateCapture({ root, strict: false }); + assert.equal(result.output.some((line) => line.includes("identical rename-signatures")), true); + assert.equal(result.output.some((line) => line.includes("Resolve by: ensure collection descriptions are distinct")), true); + assert.equal(result.exitCode, 0); +}); +test("validate --strict fails when duplicate rename-signature warnings are present", async () => { + const root = await createFixtureWithCollectionRenameRisk(); + const result = await runValidateCapture({ root, strict: true }); + assert.equal(result.output.some((line) => line.includes("identical rename-signatures")), true); + assert.equal(result.output.some((line) => line.includes("apply the intended create/delete explicitly")), true); + assert.equal(result.exitCode, 1); +}); +//# sourceMappingURL=commands.validate-rename-risk.test.js.map \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.js.map b/test/commands.validate-rename-risk.test.js.map new file mode 100644 index 0000000..1fb7e3f --- /dev/null +++ b/test/commands.validate-rename-risk.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.validate-rename-risk.test.js","sourceRoot":"","sources":["commands.validate-rename-risk.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.d.ts b/test/compose.apply.environment-alias-routing.test.d.ts new file mode 100644 index 0000000..f697a49 --- /dev/null +++ b/test/compose.apply.environment-alias-routing.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=compose.apply.environment-alias-routing.test.d.ts.map \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.d.ts.map b/test/compose.apply.environment-alias-routing.test.d.ts.map new file mode 100644 index 0000000..af3f0f7 --- /dev/null +++ b/test/compose.apply.environment-alias-routing.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.apply.environment-alias-routing.test.d.ts","sourceRoot":"","sources":["compose.apply.environment-alias-routing.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.js b/test/compose.apply.environment-alias-routing.test.js new file mode 100644 index 0000000..4c7f516 --- /dev/null +++ b/test/compose.apply.environment-alias-routing.test.js @@ -0,0 +1,129 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function makeEnvDirWithCollections() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-env-")); + await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + return dir; +} +test("plan with pending rename reads nested resources from remote alias path", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls = []; + const baseUrl = "https://management.example"; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") { + bodyText = init.body; + } + else if (init?.body instanceof URLSearchParams) { + bodyText = init.body.toString(); + } + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { + edges: [{ node: { environmentAlias: "iac-dev" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/iac-dev/collections") { + return jsonResponse(200, { edges: [] }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "iac-development", + remoteEnvAlias: "iac-dev", + envDir, + envDescription: "Development", + baseUrl, + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: true + }); + const hasRenameOp = ops.some((o) => o.kind === "update" && o.resource === "environment" && typeof o.details === "string" && o.details.includes("iac-dev -> iac-development")); + assert.equal(hasRenameOp, true); + assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/collections")), true); + assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-development/collections")), false); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("apply with pending rename calls rename endpoint and then uses new alias path for nested resources", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls = []; + const baseUrl = "https://management.example"; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") { + bodyText = init.body; + } + else if (init?.body instanceof URLSearchParams) { + bodyText = init.body.toString(); + } + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { + edges: [{ node: { environmentAlias: "iac-dev" } }] + }); + } + if (method === "POST" && + u.pathname === "/v1/projects/proj/environments/iac-dev/commands/rename") { + return jsonResponse(200, { environmentAlias: "iac-development" }); + } + if (u.pathname === "/v1/projects/proj/environments/iac-development/collections") { + return jsonResponse(200, { edges: [] }); + } + if (method === "POST" && + u.pathname === "/v1/projects/proj/environments/iac-development/collections") { + return jsonResponse(201, { collectionAlias: "content" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "iac-development", + remoteEnvAlias: "iac-dev", + envDir, + envDescription: "Development", + baseUrl, + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + const renameCall = calls.find((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/commands/rename")); + assert.ok(renameCall); + assert.equal(renameCall.method, "POST"); + assert.equal(renameCall.bodyText, JSON.stringify({ newEnvironmentAlias: "iac-development" })); + assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-development/collections")), true); + assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/collections")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "environment"), true); + } + finally { + globalThis.fetch = oldFetch; + } +}); +//# sourceMappingURL=compose.apply.environment-alias-routing.test.js.map \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.js.map b/test/compose.apply.environment-alias-routing.test.js.map new file mode 100644 index 0000000..4f1a35c --- /dev/null +++ b/test/compose.apply.environment-alias-routing.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.apply.environment-alias-routing.test.js","sourceRoot":"","sources":["compose.apply.environment-alias-routing.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.apply.failures.test.d.ts b/test/compose.apply.failures.test.d.ts new file mode 100644 index 0000000..e545ab2 --- /dev/null +++ b/test/compose.apply.failures.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=compose.apply.failures.test.d.ts.map \ No newline at end of file diff --git a/test/compose.apply.failures.test.d.ts.map b/test/compose.apply.failures.test.d.ts.map new file mode 100644 index 0000000..f92ab5c --- /dev/null +++ b/test/compose.apply.failures.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.apply.failures.test.d.ts","sourceRoot":"","sources":["compose.apply.failures.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.apply.failures.test.js b/test/compose.apply.failures.test.js new file mode 100644 index 0000000..530f1d6 --- /dev/null +++ b/test/compose.apply.failures.test.js @@ -0,0 +1,96 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function makeEnvDirWithCollections() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-fail-")); + await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); + return dir; +} +test("environment list failure returns environment warning and short-circuits nested operations", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + calls.push({ method, url }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(500, { error: "boom" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: true + }); + assert.equal(ops.some((o) => o.kind === "warn" && o.resource === "environment"), true); + assert.equal(calls.some((c) => c.url.includes("/environments/dev/collections")), false); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("rename failure returns environment warning with status and short-circuits nested operations", async () => { + const envDir = await makeEnvDirWithCollections(); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); + } + if (method === "POST" && + u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename") { + return jsonResponse(409, { error: "conflict" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + remoteEnvAlias: "old-dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + assert.equal(ops.some((o) => o.kind === "warn" && o.resource === "environment" && (o.details ?? "").includes("status 409")), true); + assert.equal(calls.some((c) => c.url.includes("/environments/dev/collections")), false); + } + finally { + globalThis.fetch = oldFetch; + } +}); +//# sourceMappingURL=compose.apply.failures.test.js.map \ No newline at end of file diff --git a/test/compose.apply.failures.test.js.map b/test/compose.apply.failures.test.js.map new file mode 100644 index 0000000..bf74e43 --- /dev/null +++ b/test/compose.apply.failures.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.apply.failures.test.js","sourceRoot":"","sources":["compose.apply.failures.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.d.ts b/test/compose.apply.rename-inference.test.d.ts new file mode 100644 index 0000000..6397acd --- /dev/null +++ b/test/compose.apply.rename-inference.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=compose.apply.rename-inference.test.d.ts.map \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.d.ts.map b/test/compose.apply.rename-inference.test.d.ts.map new file mode 100644 index 0000000..e675548 --- /dev/null +++ b/test/compose.apply.rename-inference.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.apply.rename-inference.test.d.ts","sourceRoot":"","sources":["compose.apply.rename-inference.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.js b/test/compose.apply.rename-inference.test.js new file mode 100644 index 0000000..1f98cc1 --- /dev/null +++ b/test/compose.apply.rename-inference.test.js @@ -0,0 +1,370 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function makeCollectionsEnvDir(collections) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-")); + await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections }, null, 2), "utf8"); + return dir; +} +async function makePersistedEnvDir(docs) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-persisted-")); + await fs.mkdir(path.join(dir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(dir, "graphql", "persisted.json"), JSON.stringify({ + documents: docs.map((d) => ({ + alias: d.alias, + description: d.description ?? "", + queryFile: `graphql/persisted/${d.alias}.gql` + })) + }, null, 2), "utf8"); + for (const d of docs) { + await fs.writeFile(path.join(dir, "graphql", "persisted", `${d.alias}.gql`), d.query, "utf8"); + } + return dir; +} +test("collection rename inference skips ambiguous matches and falls back to create/delete", async () => { + const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "Same description" }]); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { collectionAlias: "content-old-a" } }, { node: { collectionAlias: "content-old-b" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-a" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old-a", description: "Same description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-b" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old-b", description: "Same description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { + return jsonResponse(201, { collectionAlias: "content-new" }); + } + if ((u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-a" || + u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-b") && + method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); + const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "collection"); + assert.equal(deleteOps.length, 2); + assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("collection rename inference skips when signatures do not match and falls back to create/delete", async () => { + const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "New description" }]); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Old description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { + return jsonResponse(201, { collectionAlias: "content-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); + assert.equal(ops.some((o) => o.kind === "delete" && o.resource === "collection" && o.alias === "content-old"), true); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("collection rename inference skips when desired signatures are ambiguous and falls back to create/delete", async () => { + const envDir = await makeCollectionsEnvDir([ + { alias: "content-new-a", description: "Same description" }, + { alias: "content-new-b", description: "Same description" } + ]); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Same description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { + return jsonResponse(201, { collectionAlias: "created" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new-a"), true); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new-b"), true); + assert.equal(ops.some((o) => o.kind === "delete" && o.resource === "collection" && o.alias === "content-old"), true); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("persisted-doc rename inference skips ambiguous remote matches and falls back to create/delete", async () => { + const envDir = await makePersistedEnvDir([ + { alias: "doc-new", description: "Shared description", query: "query { ping }" } + ]); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { persistedDocumentAlias: "doc-old-a" } }, { node: { persistedDocumentAlias: "doc-old-b" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-a" && + method === "GET") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old-a", + description: "Shared description", + document: "query { ping }" + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-b" && + method === "GET") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old-b", + description: "Shared description", + document: "query { ping }" + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "POST") { + return jsonResponse(201, { persistedDocumentAlias: "doc-new" }); + } + if ((u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-a" || + u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-b") && + method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + assert.equal(calls.some((c) => c.url.includes("/graphql/persisted-documents/") && c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "persisted-doc"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "persisted-doc" && o.alias === "doc-new"), true); + const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "persisted-doc"); + assert.equal(deleteOps.length, 2); + assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("webhook rename inference skips ambiguous remote matches and falls back to create/delete", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-webhook-")); + await fs.writeFile(path.join(dir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "hook-new", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, null, 2), "utf8"); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { + edges: [{ node: { webhookAlias: "hook-old-a" } }, { node: { webhookAlias: "hook-old-b" } }] + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-a" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old-a", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-b" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old-b", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "POST") { + return jsonResponse(201, { webhookAlias: "hook-new" }); + } + if ((u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-a" || + u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-b") && + method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + const ops = await applyEnvironment({ + project: "proj", + env: "dev", + envDir: dir, + envDescription: "Dev", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + assert.equal(calls.some((c) => c.url.includes("/webhooks/") && c.url.includes("/commands/rename")), false); + assert.equal(ops.some((o) => o.kind === "update" && o.resource === "webhook"), false); + assert.equal(ops.some((o) => o.kind === "create" && o.resource === "webhook" && o.alias === "hook-new"), true); + const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "webhook"); + assert.equal(deleteOps.length, 2); + assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); + } + finally { + globalThis.fetch = oldFetch; + } +}); +//# sourceMappingURL=compose.apply.rename-inference.test.js.map \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.js.map b/test/compose.apply.rename-inference.test.js.map new file mode 100644 index 0000000..a0a1027 --- /dev/null +++ b/test/compose.apply.rename-inference.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.apply.rename-inference.test.js","sourceRoot":"","sources":["compose.apply.rename-inference.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.management-client.test.d.ts b/test/compose.management-client.test.d.ts new file mode 100644 index 0000000..fe2ee23 --- /dev/null +++ b/test/compose.management-client.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=compose.management-client.test.d.ts.map \ No newline at end of file diff --git a/test/compose.management-client.test.d.ts.map b/test/compose.management-client.test.d.ts.map new file mode 100644 index 0000000..b521250 --- /dev/null +++ b/test/compose.management-client.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.management-client.test.d.ts","sourceRoot":"","sources":["compose.management-client.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.management-client.test.js b/test/compose.management-client.test.js new file mode 100644 index 0000000..ab5b59f --- /dev/null +++ b/test/compose.management-client.test.js @@ -0,0 +1,74 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { ManagementClient } from "../src/compose/management-client.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +test("ManagementClient retries once on 401 and invalidates auth cache", async () => { + const oldFetch = globalThis.fetch; + let fetchCount = 0; + let invalidateCount = 0; + let getHeadersCount = 0; + globalThis.fetch = (async () => { + fetchCount += 1; + if (fetchCount === 1) { + return jsonResponse(401, { error: "expired-token" }); + } + return jsonResponse(200, { ok: true }); + }); + const client = new ManagementClient({ + baseUrl: "https://management.example", + auth: { + getHeaders: async () => { + getHeadersCount += 1; + return { Authorization: `Bearer token-${getHeadersCount}` }; + }, + invalidate: () => { + invalidateCount += 1; + } + } + }); + try { + const res = await client.get("/v1/projects/p/environments"); + assert.equal(res.status, 200); + assert.equal(res.data?.ok, true); + assert.equal(fetchCount, 2); + assert.equal(getHeadersCount, 2); + assert.equal(invalidateCount, 1); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("ManagementClient does not loop infinitely when retry also returns 401", async () => { + const oldFetch = globalThis.fetch; + let fetchCount = 0; + let invalidateCount = 0; + globalThis.fetch = (async () => { + fetchCount += 1; + return jsonResponse(401, { error: "still-unauthorized" }); + }); + const client = new ManagementClient({ + baseUrl: "https://management.example", + auth: { + getHeaders: async () => ({ Authorization: "Bearer token" }), + invalidate: () => { + invalidateCount += 1; + } + } + }); + try { + const res = await client.get("/v1/projects/p/environments"); + assert.equal(res.status, 401); + assert.equal(res.data?.error, "still-unauthorized"); + assert.equal(fetchCount, 2); + assert.equal(invalidateCount, 1); + } + finally { + globalThis.fetch = oldFetch; + } +}); +//# sourceMappingURL=compose.management-client.test.js.map \ No newline at end of file diff --git a/test/compose.management-client.test.js.map b/test/compose.management-client.test.js.map new file mode 100644 index 0000000..97cc7a5 --- /dev/null +++ b/test/compose.management-client.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.management-client.test.js","sourceRoot":"","sources":["compose.management-client.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.oauth-base.test.d.ts b/test/compose.oauth-base.test.d.ts new file mode 100644 index 0000000..909cd42 --- /dev/null +++ b/test/compose.oauth-base.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=compose.oauth-base.test.d.ts.map \ No newline at end of file diff --git a/test/compose.oauth-base.test.d.ts.map b/test/compose.oauth-base.test.d.ts.map new file mode 100644 index 0000000..cf88029 --- /dev/null +++ b/test/compose.oauth-base.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.oauth-base.test.d.ts","sourceRoot":"","sources":["compose.oauth-base.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.oauth-base.test.js b/test/compose.oauth-base.test.js new file mode 100644 index 0000000..271718f --- /dev/null +++ b/test/compose.oauth-base.test.js @@ -0,0 +1,93 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { oauthClientCredentialsAuth } from "../src/compose/auth/providers/oauth-base.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +test("oauthClientCredentialsAuth caches token between calls until invalidated", async () => { + const oldFetch = globalThis.fetch; + let tokenCallCount = 0; + globalThis.fetch = (async () => { + tokenCallCount += 1; + return jsonResponse(200, { + access_token: `token-${tokenCallCount}`, + expires_in: 3600 + }); + }); + try { + const auth = oauthClientCredentialsAuth({ + tokenUrl: "https://auth.example/token", + clientId: "client", + clientSecret: "secret" + }); + const h1 = await auth.getHeaders(); + const h2 = await auth.getHeaders(); + assert.equal(h1.Authorization, "Bearer token-1"); + assert.equal(h2.Authorization, "Bearer token-1"); + assert.equal(tokenCallCount, 1); + auth.invalidate?.(); + const h3 = await auth.getHeaders(); + assert.equal(h3.Authorization, "Bearer token-2"); + assert.equal(tokenCallCount, 2); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("oauthClientCredentialsAuth deduplicates concurrent token requests", async () => { + const oldFetch = globalThis.fetch; + let tokenCallCount = 0; + let release = null; + const gate = new Promise((resolve) => { + release = resolve; + }); + globalThis.fetch = (async () => { + tokenCallCount += 1; + await gate; + return jsonResponse(200, { + access_token: "shared-token", + expires_in: 3600 + }); + }); + try { + const auth = oauthClientCredentialsAuth({ + tokenUrl: "https://auth.example/token", + clientId: "client", + clientSecret: "secret" + }); + const p1 = auth.getHeaders(); + const p2 = auth.getHeaders(); + const p3 = auth.getHeaders(); + release?.(); + const [h1, h2, h3] = await Promise.all([p1, p2, p3]); + assert.equal(h1.Authorization, "Bearer shared-token"); + assert.equal(h2.Authorization, "Bearer shared-token"); + assert.equal(h3.Authorization, "Bearer shared-token"); + assert.equal(tokenCallCount, 1); + } + finally { + globalThis.fetch = oldFetch; + } +}); +test("oauthClientCredentialsAuth surfaces token endpoint errors", async () => { + const oldFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response("unauthorized", { + status: 401, + headers: { "content-type": "text/plain" } + })); + try { + const auth = oauthClientCredentialsAuth({ + tokenUrl: "https://auth.example/token", + clientId: "client", + clientSecret: "secret" + }); + await assert.rejects(() => auth.getHeaders(), /OAuth token request failed \(401\): unauthorized/); + } + finally { + globalThis.fetch = oldFetch; + } +}); +//# sourceMappingURL=compose.oauth-base.test.js.map \ No newline at end of file diff --git a/test/compose.oauth-base.test.js.map b/test/compose.oauth-base.test.js.map new file mode 100644 index 0000000..b0ce046 --- /dev/null +++ b/test/compose.oauth-base.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.oauth-base.test.js","sourceRoot":"","sources":["compose.oauth-base.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.state.test.d.ts b/test/compose.state.test.d.ts new file mode 100644 index 0000000..164067f --- /dev/null +++ b/test/compose.state.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=compose.state.test.d.ts.map \ No newline at end of file diff --git a/test/compose.state.test.d.ts.map b/test/compose.state.test.d.ts.map new file mode 100644 index 0000000..4100aa1 --- /dev/null +++ b/test/compose.state.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.state.test.d.ts","sourceRoot":"","sources":["compose.state.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.state.test.js b/test/compose.state.test.js new file mode 100644 index 0000000..867543a --- /dev/null +++ b/test/compose.state.test.js @@ -0,0 +1,49 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { COMPOSE_STATE_FILE, ensureStateForAliases, findEnvironmentByAlias, markEnvironmentRemoteAlias, readComposeState, renameEnvironmentAlias, writeComposeState } from "../src/compose/state.js"; +test("ensureStateForAliases creates and extends state while preserving existing env ids", () => { + const first = ensureStateForAliases(null, "proj-a", ["dev"]).state; + assert.equal(first.project, "proj-a"); + assert.equal(first.environments.length, 1); + assert.equal(first.environments[0]?.alias, "dev"); + const originalId = first.environments[0]?.id; + assert.ok(originalId); + const second = ensureStateForAliases(first, "proj-a", ["dev", "stage"]).state; + assert.equal(second.environments.length, 2); + assert.equal(second.environments.find((e) => e.alias === "dev")?.id, originalId); + assert.ok(second.environments.find((e) => e.alias === "stage")?.id); +}); +test("renameEnvironmentAlias updates alias and keeps remoteAlias mapping stable by env id", () => { + const state = ensureStateForAliases(null, "proj-b", ["iac-dev"]).state; + const env = findEnvironmentByAlias(state, "iac-dev"); + assert.ok(env); + const marked = markEnvironmentRemoteAlias(state, env.id, "iac-dev"); + assert.equal(marked, true); + const renamed = renameEnvironmentAlias(state, "iac-dev", "iac-development"); + assert.equal(renamed, true); + const after = findEnvironmentByAlias(state, "iac-development"); + assert.ok(after); + assert.equal(after.id, env.id); + assert.equal(after.remoteAlias, "iac-dev"); +}); +test("readComposeState returns null when file is missing and throws on invalid JSON", async () => { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-state-test-")); + const missing = await readComposeState(tmpRoot); + assert.equal(missing, null); + const statePath = path.join(tmpRoot, COMPOSE_STATE_FILE); + await fs.writeFile(statePath, "{invalid json", "utf8"); + await assert.rejects(() => readComposeState(tmpRoot)); +}); +test("writeComposeState and readComposeState roundtrip", async () => { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-state-roundtrip-")); + const initial = ensureStateForAliases(null, "proj-c", ["dev", "prod"]).state; + await writeComposeState(tmpRoot, initial); + const read = await readComposeState(tmpRoot); + assert.ok(read); + assert.equal(read.project, "proj-c"); + assert.deepEqual(read.environments.map((e) => e.alias).sort(), ["dev", "prod"]); +}); +//# sourceMappingURL=compose.state.test.js.map \ No newline at end of file diff --git a/test/compose.state.test.js.map b/test/compose.state.test.js.map new file mode 100644 index 0000000..dfbe504 --- /dev/null +++ b/test/compose.state.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"compose.state.test.js","sourceRoot":"","sources":["compose.state.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.d.ts b/test/contracts.apply-contract-map.test.d.ts new file mode 100644 index 0000000..f9ee503 --- /dev/null +++ b/test/contracts.apply-contract-map.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=contracts.apply-contract-map.test.d.ts.map \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.d.ts.map b/test/contracts.apply-contract-map.test.d.ts.map new file mode 100644 index 0000000..549137e --- /dev/null +++ b/test/contracts.apply-contract-map.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"contracts.apply-contract-map.test.d.ts","sourceRoot":"","sources":["contracts.apply-contract-map.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.js b/test/contracts.apply-contract-map.test.js new file mode 100644 index 0000000..b5ca0f1 --- /dev/null +++ b/test/contracts.apply-contract-map.test.js @@ -0,0 +1,1089 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import { applyEnvironment } from "../src/compose/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function readContractMap() { + const filePath = path.resolve(process.cwd(), "docs/contracts/apply-contract.json"); + const text = await fs.readFile(filePath, "utf8"); + return JSON.parse(text); +} +function resolvePath(template, params) { + let out = template; + for (const [k, v] of Object.entries(params)) { + out = out.replaceAll(`{${k}}`, v); + } + return out; +} +function assertContractCall(contracts, operation, calls, params) { + const contract = contracts.operations[operation]; + assert.ok(contract, `Missing contract entry for ${operation}`); + const path = resolvePath(contract.pathTemplate, params); + const call = calls.find((c) => c.method === contract.method && c.pathname === path); + assert.ok(call, `Did not find contract call for ${operation}: ${contract.method} ${path}`); + if (contract.requiredBodyKeys.length > 0) { + assert.ok(call.body && typeof call.body === "object", `${operation} body was not an object`); + const body = call.body; + for (const key of contract.requiredBodyKeys) { + assert.ok(key in body, `${operation} body missing required key: ${key}`); + } + } +} +async function makeFullEnvDir() { + const envDir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-contract-map-full-")); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "Main content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "type-schemas", "software.schema.json"), JSON.stringify({ + alias: "software", + description: "Software", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + } + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ + alias: "article", + description: "Article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ + functions: [ + { + alias: "map-content", + description: "Maps content", + scriptFile: "functions/ingestion/map-content.js" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "export default (x) => x;", "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ + documents: [ + { alias: "doc-1", description: "Query", queryFile: "graphql/persisted/doc-1.gql" } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-1.gql"), "query { ping }", "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "send-on-create", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["software"], + customHeaders: { "x-api-key": "abc123" } + } + ] + }, null, 2), "utf8"); + return envDir; +} +async function mkEnvDir(prefix) { + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); +} +test("apply emitted calls match contract map for currently implemented write operations", async () => { + const contracts = await readContractMap(); + const envDir = await makeFullEnvDir(); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/software") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { oldField: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "software" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { persistedDocumentAlias: "doc-1" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + const params = { + projectAlias: "proj", + environmentAlias: "dev", + typeSchemaAlias: "article" + }; + assertContractCall(contracts, "collectionCreate", calls, params); + assertContractCall(contracts, "typeSchemaCreate", calls, params); + assertContractCall(contracts, "typeSchemaUpdateSchema", calls, params); + assertContractCall(contracts, "ingestionFunctionCreate", calls, params); + assertContractCall(contracts, "persistedDocumentCreate", calls, params); + assertContractCall(contracts, "webhookCreate", calls, params); +}); +test("environment create and rename calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-env-"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { environmentAlias: "dev" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "environmentCreate", calls, { + projectAlias: "proj" + }); + const renameCalls = []; + const oldFetch2 = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + renameCalls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" && method === "POST") { + return jsonResponse(200, { environmentAlias: "dev" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + remoteEnvAlias: "old-dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch2; + } + assertContractCall(contracts, "environmentRename", renameCalls, { + projectAlias: "proj", + environmentAlias: "old-dev" + }); +}); +test("ingestion update calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-ingestion-update-"); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ + functions: [ + { + alias: "fn-a", + description: "New desc", + scriptFile: "functions/ingestion/fn-a.js" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "fn-a.js"), "export default (x) => x;", "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a") { + return jsonResponse(200, { ingestionFunctionAlias: "fn-a", description: "Old desc", script: "export default () => null;" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a/commands/update-script" && method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "ingestionFunctionUpdateScript", calls, { + projectAlias: "proj", + environmentAlias: "dev", + ingestionFunctionAlias: "fn-a" + }); + assertContractCall(contracts, "ingestionFunctionUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + ingestionFunctionAlias: "fn-a" + }); +}); +test("collection update-description calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-collection-update-desc-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "New description" }] }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") + body = JSON.parse(init.body); + if (init?.body instanceof URLSearchParams) + body = init.body.toString(); + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content", description: "Old description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "collectionUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + collectionAlias: "content" + }); +}); +test("type-schema update-description calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-type-schema-update-desc-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ + alias: "article", + description: "New type-schema description", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") + body = JSON.parse(init.body); + if (init?.body instanceof URLSearchParams) + body = init.body.toString(); + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Old type-schema description", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "typeSchemaUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + typeSchemaAlias: "article" + }); +}); +test("ingestion delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-ingestion-delete-"); + await fs.mkdir(path.join(envDir, "functions"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "legacy-ingest" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "ingestionFunctionDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + ingestionFunctionAlias: "legacy-ingest" + }); +}); +test("persisted-document update calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-persisted-update-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ + documents: [ + { + alias: "doc-a", + description: "New persisted doc description", + queryFile: "graphql/persisted/doc-a.gql" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-a.gql"), "query { updated }", "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-a", + description: "Old persisted doc description", + document: "query { old }" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a/commands/update-document" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "persistedDocumentUpdateDocument", calls, { + projectAlias: "proj", + environmentAlias: "dev", + persistedDocumentAlias: "doc-a" + }); + assertContractCall(contracts, "persistedDocumentUpdateDescription", calls, { + projectAlias: "proj", + environmentAlias: "dev", + persistedDocumentAlias: "doc-a" + }); +}); +test("webhook update calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-webhook-update-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "send-on-create", + description: "New webhook description", + url: "https://example.com/new", + eventTypes: ["content.deleted"], + collectionAliases: ["articles"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + description: "Old webhook description", + url: "https://example.com/old", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["software"], + customHeaders: { authorization: "Bearer old" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + const params = { + projectAlias: "proj", + environmentAlias: "dev", + webhookAlias: "send-on-create" + }; + assertContractCall(contracts, "webhookUpdateDescription", calls, params); + assertContractCall(contracts, "webhookUpdateUrl", calls, params); + assertContractCall(contracts, "webhookUpdateEventTypes", calls, params); + assertContractCall(contracts, "webhookUpdateCollections", calls, params); + assertContractCall(contracts, "webhookUpdateTypeSchemas", calls, params); + assertContractCall(contracts, "webhookUpdateHeaders", calls, params); +}); +test("persisted-document delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-persisted-delete-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "persistedDocumentDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + persistedDocumentAlias: "doc-a" + }); +}); +test("webhook delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-webhook-delete-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "webhookDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + webhookAlias: "send-on-create" + }); +}); +test("collection delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-collection-delete-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "collectionDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + collectionAlias: "content" + }); +}); +test("type-schema delete calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-type-schema-delete-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "legacy-schema" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/legacy-schema" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + assertContractCall(contracts, "typeSchemaDelete", calls, { + projectAlias: "proj", + environmentAlias: "dev", + typeSchemaAlias: "legacy-schema" + }); +}); +test("non-environment rename calls match contract map", async () => { + const contracts = await readContractMap(); + const envDir = await mkEnvDir("compose-contract-map-rename-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content-new", description: "Main content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "type-schemas", "article-new.schema.json"), JSON.stringify({ + alias: "article-new", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ + functions: [ + { + alias: "map-content-new", + description: "Maps content", + scriptFile: "functions/ingestion/map-content-new.js" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content-new.js"), "export default (x) => x;", "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ + documents: [ + { + alias: "doc-new", + description: "Main query", + queryFile: "graphql/persisted/doc-new.gql" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-new.gql"), "query { ping }", "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "hook-new", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, null, 2), "utf8"); + const oldFetch = globalThis.fetch; + const calls = []; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + let body; + if (typeof init?.body === "string") { + body = JSON.parse(init.body); + } + else if (init?.body instanceof URLSearchParams) { + body = init.body.toString(); + } + calls.push({ method, pathname: u.pathname, body }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { collectionAlias: "content-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article-old", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { typeSchemaAlias: "article-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old" && + method === "GET") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content-old", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { ingestionFunctionAlias: "map-content-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old" && + method === "GET") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old", + description: "Main query", + document: "query { ping }" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { persistedDocumentAlias: "doc-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename" && method === "POST") { + return jsonResponse(200, { webhookAlias: "hook-new" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + globalThis.fetch = oldFetch; + } + const params = { projectAlias: "proj", environmentAlias: "dev" }; + assertContractCall(contracts, "collectionRename", calls, { + ...params, + collectionAlias: "content-old" + }); + assertContractCall(contracts, "typeSchemaRename", calls, { + ...params, + typeSchemaAlias: "article-old" + }); + assertContractCall(contracts, "ingestionFunctionRename", calls, { + ...params, + ingestionFunctionAlias: "map-content-old" + }); + assertContractCall(contracts, "persistedDocumentRename", calls, { + ...params, + persistedDocumentAlias: "doc-old" + }); + assertContractCall(contracts, "webhookRename", calls, { + ...params, + webhookAlias: "hook-old" + }); +}); +//# sourceMappingURL=contracts.apply-contract-map.test.js.map \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.js.map b/test/contracts.apply-contract-map.test.js.map new file mode 100644 index 0000000..82f2ae6 --- /dev/null +++ b/test/contracts.apply-contract-map.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"contracts.apply-contract-map.test.js","sourceRoot":"","sources":["contracts.apply-contract-map.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.d.ts b/test/contracts.apply-openapi-shape.test.d.ts new file mode 100644 index 0000000..5bd4899 --- /dev/null +++ b/test/contracts.apply-openapi-shape.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=contracts.apply-openapi-shape.test.d.ts.map \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.d.ts.map b/test/contracts.apply-openapi-shape.test.d.ts.map new file mode 100644 index 0000000..5bfcbe6 --- /dev/null +++ b/test/contracts.apply-openapi-shape.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"contracts.apply-openapi-shape.test.d.ts","sourceRoot":"","sources":["contracts.apply-openapi-shape.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.js b/test/contracts.apply-openapi-shape.test.js new file mode 100644 index 0000000..0c9ed0b --- /dev/null +++ b/test/contracts.apply-openapi-shape.test.js @@ -0,0 +1,1071 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { applyEnvironment } from "../src/compose/apply.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function mkEnvDir(prefix) { + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); +} +function installFetchMock(handler) { + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + let bodyText; + if (typeof init?.body === "string") + bodyText = init.body; + if (init?.body instanceof URLSearchParams) + bodyText = init.body.toString(); + calls.push({ method, url, bodyText }); + const u = new URL(url); + if (u.pathname === "/v1/auth/token") { + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + } + return handler(u, method, bodyText); + }); + return { + calls, + restore: () => { + globalThis.fetch = oldFetch; + } + }; +} +test("environment create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-create-env-"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { environmentAlias: "dev" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.environmentAlias, "dev"); + assert.equal(body.description, "Development"); +}); +test("collection create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-collection-create-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "Main content" }] }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { collectionAlias: "content" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/collections")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.collectionAlias, "content"); + assert.equal(body.description, "Main content"); +}); +test("collection update uses expected update-description command payload", async () => { + const envDir = await mkEnvDir("compose-contract-collection-update-desc-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "New description" }] }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content", description: "Old description" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const updateCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/collections/content/commands/update-description")); + assert.ok(updateCall); + const body = JSON.parse(updateCall.bodyText ?? "{}"); + assert.equal(body.newDescription, "New description"); +}); +test("collection delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-collection-delete-"); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections") { + if (method === "GET") + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const deleteCall = calls.find((c) => c.method === "DELETE" && c.url.includes("/v1/projects/proj/environments/dev/collections/content")); + assert.ok(deleteCall); +}); +test("environment rename uses expected endpoint and payload key", async () => { + const envDir = await mkEnvDir("compose-contract-rename-env-"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" && method === "POST") { + return jsonResponse(200, { environmentAlias: "dev" }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + remoteEnvAlias: "old-dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const renameCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/old-dev/commands/rename")); + assert.ok(renameCall); + const body = JSON.parse(renameCall.bodyText ?? "{}"); + assert.deepEqual(body, { newEnvironmentAlias: "dev" }); +}); +test("persisted-document create uses expected payload field 'document'", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [{ alias: "doc-1", description: "desc", queryFile: "graphql/persisted/doc-1.gql" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-1.gql"), "query { ping }", "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { persistedDocumentAlias: "doc-1" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.persistedDocumentAlias, "doc-1"); + assert.equal(body.description, "desc"); + assert.equal(body.document, "query { ping }"); + assert.equal("query" in body, false); +}); +test("persisted-document create sends string description when local description is omitted", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-missing-desc-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [{ alias: "doc-2", queryFile: "graphql/persisted/doc-2.gql" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-2.gql"), "query { pong }", "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { persistedDocumentAlias: "doc-2" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.persistedDocumentAlias, "doc-2"); + assert.equal(body.description, ""); + assert.equal(body.document, "query { pong }"); +}); +test("persisted-document update uses expected update-document and update-description commands", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-update-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ + documents: [ + { + alias: "doc-3", + description: "New description", + queryFile: "graphql/persisted/doc-3.gql" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-3.gql"), "query { newDoc }", "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-3" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-3", + description: "Old description", + document: "query { oldDoc }" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-document" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const updateDocumentCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-document")); + assert.ok(updateDocumentCall); + const updateDocumentBody = JSON.parse(updateDocumentCall.bodyText ?? "{}"); + assert.equal(updateDocumentBody.newDocument, "query { newDoc }"); + const updateDescriptionCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-description")); + assert.ok(updateDescriptionCall); + const updateDescriptionBody = JSON.parse(updateDescriptionCall.bodyText ?? "{}"); + assert.equal(updateDescriptionBody.newDescription, "New description"); +}); +test("persisted-document delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-persisted-doc-delete-"); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-4" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-4" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const deleteCall = calls.find((c) => c.method === "DELETE" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-4")); + assert.ok(deleteCall); +}); +test("ingestion-function create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-ingestion-create-"); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ + functions: [ + { + alias: "map-content", + description: "Maps content", + scriptFile: "functions/ingestion/map-content.js" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "export default function(x){ return x; }", "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.ingestionFunctionAlias, "map-content"); + assert.equal(body.description, "Maps content"); + assert.equal(typeof body.script, "string"); +}); +test("ingestion-function update uses expected update-script and update-description commands", async () => { + const envDir = await mkEnvDir("compose-contract-ingestion-update-"); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ + functions: [ + { + alias: "map-content", + description: "New description", + scriptFile: "functions/ingestion/map-content.js" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "export default function(input){ return input; }", "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content", + description: "Old description", + script: "export default () => null;" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-script" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const updateScriptCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-script")); + assert.ok(updateScriptCall); + const updateScriptBody = JSON.parse(updateScriptCall.bodyText ?? "{}"); + assert.equal(typeof updateScriptBody.script, "string"); + const updateDescriptionCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-description")); + assert.ok(updateDescriptionCall); + const updateDescriptionBody = JSON.parse(updateDescriptionCall.bodyText ?? "{}"); + assert.equal(updateDescriptionBody.newDescription, "New description"); +}); +test("ingestion-function delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-ingestion-delete-"); + await fs.mkdir(path.join(envDir, "functions"), { recursive: true }); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "legacy-ingest" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest" && + method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const deleteCall = calls.find((c) => c.method === "DELETE" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest")); + assert.ok(deleteCall); +}); +test("webhook create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-webhook-create-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "send-on-create", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["article"], + customHeaders: { "x-api-key": "abc123" } + } + ] + }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks") { + if (method === "GET") + return jsonResponse(200, { edges: [] }); + if (method === "POST") + return jsonResponse(201, { webhookAlias: "send-on-create" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/webhooks")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.webhookAlias, "send-on-create"); + assert.equal(body.url, "https://example.com/hook"); + assert.deepEqual(body.eventTypes, ["content.ingested"]); + assert.deepEqual(body.collectionAliases, ["content"]); + assert.deepEqual(body.typeSchemaAliases, ["article"]); + assert.deepEqual(body.customHeaders, { "x-api-key": "abc123" }); +}); +test("webhook update uses expected command endpoints and payload shapes", async () => { + const envDir = await mkEnvDir("compose-contract-webhook-update-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "send-on-create", + description: "New webhook description", + url: "https://example.com/new", + eventTypes: ["content.deleted"], + collectionAliases: ["articles"], + typeSchemaAliases: ["article"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "send-on-create", + description: "Old webhook description", + url: "https://example.com/old", + eventTypes: ["content.ingested"], + collectionAliases: ["content"], + typeSchemaAliases: ["software"], + customHeaders: { authorization: "Bearer old" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const updateDescriptionCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description")); + assert.ok(updateDescriptionCall); + assert.deepEqual(JSON.parse(updateDescriptionCall.bodyText ?? "{}"), { newDescription: "New webhook description" }); + const updateUrlCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url")); + assert.ok(updateUrlCall); + assert.deepEqual(JSON.parse(updateUrlCall.bodyText ?? "{}"), { url: "https://example.com/new" }); + const updateEventTypesCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types")); + assert.ok(updateEventTypesCall); + assert.deepEqual(JSON.parse(updateEventTypesCall.bodyText ?? "{}"), { eventTypes: ["content.deleted"] }); + const updateCollectionsCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections")); + assert.ok(updateCollectionsCall); + assert.deepEqual(JSON.parse(updateCollectionsCall.bodyText ?? "{}"), { collectionAliases: ["articles"] }); + const updateTypeSchemasCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas")); + assert.ok(updateTypeSchemasCall); + assert.deepEqual(JSON.parse(updateTypeSchemasCall.bodyText ?? "{}"), { typeSchemaAliases: ["article"] }); + const updateHeadersCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers")); + assert.ok(updateHeadersCall); + assert.deepEqual(JSON.parse(updateHeadersCall.bodyText ?? "{}"), { + newHeaders: { authorization: "Bearer abc" } + }); +}); +test("webhook delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-webhook-delete-"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const deleteCall = calls.find((c) => c.method === "DELETE" && c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create")); + assert.ok(deleteCall); +}); +test("type-schema update uses raw schema body at update-schema command endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + const desiredSchema = { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + }; + await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ + alias: "article", + description: "Article", + schema: desiredSchema + }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method, bodyText) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Article", + schema: { ...desiredSchema, properties: { title: { type: "string" } } } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema" && + method === "PUT") { + const parsed = JSON.parse(bodyText ?? "{}"); + return jsonResponse(200, parsed); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const updateCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema")); + assert.ok(updateCall); + const body = JSON.parse(updateCall.bodyText ?? "{}"); + assert.deepEqual(body, desiredSchema); + assert.equal("schema" in body, false); +}); +test("type-schema update uses expected update-description command payload", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-update-desc-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + const desiredSchema = { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + }; + await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ + alias: "article", + description: "New description", + schema: desiredSchema + }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article", + description: "Old description", + schema: desiredSchema + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description" && + method === "PUT") { + return jsonResponse(200, { ok: true }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const updateCall = calls.find((c) => c.method === "PUT" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description")); + assert.ok(updateCall); + const body = JSON.parse(updateCall.bodyText ?? "{}"); + assert.equal(body.newDescription, "New description"); +}); +test("type-schema create uses expected endpoint and payload shape", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-create-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + const desiredSchema = { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { name: { type: "string" } } + }; + await fs.writeFile(path.join(envDir, "type-schemas", "software.schema.json"), JSON.stringify({ + alias: "software", + description: "Software schema", + schema: desiredSchema + }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/software" && method === "GET") { + return jsonResponse(404, { error: "not found" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "POST") { + return jsonResponse(201, { typeSchemaAlias: "software" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/type-schemas")); + assert.ok(createCall); + const body = JSON.parse(createCall.bodyText ?? "{}"); + assert.equal(body.typeSchemaAlias, "software"); + assert.equal(body.description, "Software schema"); + assert.deepEqual(body.schema, desiredSchema); +}); +test("type-schema delete uses expected endpoint", async () => { + const envDir = await mkEnvDir("compose-contract-type-schema-delete-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "legacy-schema" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/legacy-schema" && method === "DELETE") { + return new Response(null, { status: 204 }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const deleteCall = calls.find((c) => c.method === "DELETE" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/legacy-schema")); + assert.ok(deleteCall); +}); +test("non-environment rename commands use expected endpoints and payload keys", async () => { + const envDir = await mkEnvDir("compose-contract-entity-rename-"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content-new", description: "Main content" }] }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "type-schemas", "article-new.schema.json"), JSON.stringify({ + alias: "article-new", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ + functions: [ + { + alias: "map-content-new", + description: "Maps content", + scriptFile: "functions/ingestion/map-content-new.js" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content-new.js"), "export default (x) => x;", "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ + documents: [ + { + alias: "doc-new", + description: "Main query", + queryFile: "graphql/persisted/doc-new.gql" + } + ] + }, null, 2), "utf8"); + await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-new.gql"), "query { ping }", "utf8"); + await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ + webhooks: [ + { + alias: "hook-new", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + } + ] + }, null, 2), "utf8"); + const { calls, restore } = installFetchMock((u, method) => { + if (u.pathname === "/v1/projects/proj/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { + return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { collectionAlias: "content-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old" && method === "GET") { + return jsonResponse(200, { + typeSchemaAlias: "article-old", + description: "Article schema", + schema: { + $schema: "https://umbracocompose.com/v1/schema", + allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], + properties: { title: { type: "string" } } + } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { typeSchemaAlias: "article-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old" && + method === "GET") { + return jsonResponse(200, { + ingestionFunctionAlias: "map-content-old", + description: "Maps content", + script: "export default (x) => x;" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { ingestionFunctionAlias: "map-content-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old" && + method === "GET") { + return jsonResponse(200, { + persistedDocumentAlias: "doc-old", + description: "Main query", + document: "query { ping }" + }); + } + if (u.pathname === + "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" && + method === "POST") { + return jsonResponse(200, { persistedDocumentAlias: "doc-new" }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-old" } }] }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old" && method === "GET") { + return jsonResponse(200, { + webhookAlias: "hook-old", + description: "Notify hook", + url: "https://example.com/hook", + eventTypes: ["content.ingested"], + collectionAliases: ["content-new"], + typeSchemaAliases: ["article-new"], + customHeaders: { authorization: "Bearer abc" } + }); + } + if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename" && method === "POST") { + return jsonResponse(200, { webhookAlias: "hook-new" }); + } + return jsonResponse(200, { edges: [] }); + }); + try { + await applyEnvironment({ + project: "proj", + env: "dev", + envDir, + envDescription: "Development", + baseUrl: "https://management.example", + oauth: { clientId: "client", clientSecret: "secret" }, + planOnly: false + }); + } + finally { + restore(); + } + const collectionRenameCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/collections/content-old/commands/rename")); + assert.ok(collectionRenameCall); + assert.deepEqual(JSON.parse(collectionRenameCall.bodyText ?? "{}"), { newCollectionAlias: "content-new" }); + const typeSchemaRenameCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename")); + assert.ok(typeSchemaRenameCall); + assert.deepEqual(JSON.parse(typeSchemaRenameCall.bodyText ?? "{}"), { newTypeSchemaAlias: "article-new" }); + const ingestionRenameCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename")); + assert.ok(ingestionRenameCall); + assert.deepEqual(JSON.parse(ingestionRenameCall.bodyText ?? "{}"), { + newIngestionFunctionAlias: "map-content-new" + }); + const persistedRenameCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename")); + assert.ok(persistedRenameCall); + assert.deepEqual(JSON.parse(persistedRenameCall.bodyText ?? "{}"), { + newPersistedDocumentAlias: "doc-new" + }); + const webhookRenameCall = calls.find((c) => c.method === "POST" && + c.url.includes("/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename")); + assert.ok(webhookRenameCall); + assert.deepEqual(JSON.parse(webhookRenameCall.bodyText ?? "{}"), { newWebhookAlias: "hook-new" }); +}); +//# sourceMappingURL=contracts.apply-openapi-shape.test.js.map \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.js.map b/test/contracts.apply-openapi-shape.test.js.map new file mode 100644 index 0000000..c9d4ab6 --- /dev/null +++ b/test/contracts.apply-openapi-shape.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"contracts.apply-openapi-shape.test.js","sourceRoot":"","sources":["contracts.apply-openapi-shape.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.d.ts b/test/contracts.pull-contract-map.test.d.ts new file mode 100644 index 0000000..25f9644 --- /dev/null +++ b/test/contracts.pull-contract-map.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=contracts.pull-contract-map.test.d.ts.map \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.d.ts.map b/test/contracts.pull-contract-map.test.d.ts.map new file mode 100644 index 0000000..81ba212 --- /dev/null +++ b/test/contracts.pull-contract-map.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"contracts.pull-contract-map.test.d.ts","sourceRoot":"","sources":["contracts.pull-contract-map.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.js b/test/contracts.pull-contract-map.test.js new file mode 100644 index 0000000..ee5c7ff --- /dev/null +++ b/test/contracts.pull-contract-map.test.js @@ -0,0 +1,216 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import YAML from "yaml"; +import { pullCommand } from "../src/commands/pull.js"; +import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" } + }); +} +async function readContractMap() { + const filePath = path.resolve(process.cwd(), "docs/contracts/pull-contract.json"); + const text = await fs.readFile(filePath, "utf8"); + return JSON.parse(text); +} +function resolvePath(template, params) { + let out = template; + for (const [k, v] of Object.entries(params)) { + out = out.replaceAll(`{${k}}`, v); + } + return out; +} +function assertContractCall(contracts, operation, calls, params) { + const contract = contracts.operations[operation]; + assert.ok(contract, `Missing contract entry for ${operation}`); + const expectedPath = resolvePath(contract.pathTemplate, params); + const call = calls.find((c) => c.method === contract.method && c.pathname === expectedPath); + assert.ok(call, `Missing call for ${operation}: ${contract.method} ${expectedPath}`); +} +async function createFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-contract-")); + const envDir = path.join(root, "env", "dev"); + await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); + await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); + await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); + const cfg = { + version: 1, + project: "test-project", + environments: { + dev: { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + } + }; + await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); + await writeComposeState(root, createInitialState("test-project", ["dev"])); + return { root }; +} +test("pull emitted read calls match pull contract map", async () => { + const contracts = await readContractMap(); + const { root } = await createFixture(); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + calls.push({ method, pathname: u.pathname }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") + return jsonResponse(200, { edges: [] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") + return jsonResponse(200, { typeSchemaAlias: "article", schema: {}, description: null }); + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/fn-a") + return jsonResponse(200, { ingestionFunctionAlias: "fn-a", script: "export default () => null;" }); + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/doc-a") + return jsonResponse(200, { persistedDocumentAlias: "doc-a", document: "query { __typename }" }); + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-a" } }] }); + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/hook-a") + return jsonResponse(200, { webhookAlias: "hook-a", url: "https://example.com/hook", eventTypes: [], collectionAliases: [], typeSchemaAliases: [], customHeaders: {} }); + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await pullCommand.handler({ + dir: root, + env: "dev", + clientId: "client", + clientSecret: "secret" + }); + } + finally { + globalThis.fetch = oldFetch; + } + const baseParams = { + projectAlias: "test-project", + environmentAlias: "dev", + typeSchemaAlias: "article", + ingestionFunctionAlias: "fn-a", + persistedDocumentAlias: "doc-a", + webhookAlias: "hook-a" + }; + assertContractCall(contracts, "environmentList", calls, baseParams); + assertContractCall(contracts, "collectionList", calls, baseParams); + assertContractCall(contracts, "typeSchemaList", calls, baseParams); + assertContractCall(contracts, "typeSchemaGet", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionList", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionGet", calls, baseParams); + assertContractCall(contracts, "persistedDocumentList", calls, baseParams); + assertContractCall(contracts, "persistedDocumentGet", calls, baseParams); + assertContractCall(contracts, "webhookList", calls, baseParams); + assertContractCall(contracts, "webhookGet", calls, baseParams); +}); +test("pull contract-map calls use state remoteAlias path when local alias differs", async () => { + const contracts = await readContractMap(); + const { root } = await createFixture(); + const composePath = path.join(root, "umbraco-compose.yaml"); + const cfgText = await fs.readFile(composePath, "utf8"); + const cfg = YAML.parse(cfgText); + cfg.environments = { + "dev-renamed": { + dir: "./env/dev", + description: "Dev", + managementBaseUrl: "https://management.example" + } + }; + await fs.writeFile(composePath, YAML.stringify(cfg), "utf8"); + const state = await readComposeState(root); + assert.ok(state); + state.environments[0].alias = "dev-renamed"; + state.environments[0].remoteAlias = "dev"; + await writeComposeState(root, state); + const calls = []; + const oldFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const u = new URL(url); + calls.push({ method, pathname: u.pathname }); + if (u.pathname === "/v1/auth/token") + return jsonResponse(200, { access_token: "token", expires_in: 3600 }); + if (u.pathname === "/v1/projects/test-project/environments") { + return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { + return jsonResponse(200, { edges: [] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { + return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { + return jsonResponse(200, { typeSchemaAlias: "article", schema: {}, description: null }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { + return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/fn-a") { + return jsonResponse(200, { ingestionFunctionAlias: "fn-a", script: "export default () => null;" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { + return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/doc-a") { + return jsonResponse(200, { persistedDocumentAlias: "doc-a", document: "query { __typename }" }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { + return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-a" } }] }); + } + if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/hook-a") { + return jsonResponse(200, { + webhookAlias: "hook-a", + url: "https://example.com/hook", + eventTypes: [], + collectionAliases: [], + typeSchemaAliases: [], + customHeaders: {} + }); + } + return jsonResponse(404, { error: "unexpected path" }); + }); + try { + await pullCommand.handler({ + dir: root, + env: "dev-renamed", + clientId: "client", + clientSecret: "secret" + }); + } + finally { + globalThis.fetch = oldFetch; + } + const baseParams = { + projectAlias: "test-project", + environmentAlias: "dev", + typeSchemaAlias: "article", + ingestionFunctionAlias: "fn-a", + persistedDocumentAlias: "doc-a", + webhookAlias: "hook-a" + }; + assertContractCall(contracts, "environmentList", calls, baseParams); + assertContractCall(contracts, "collectionList", calls, baseParams); + assertContractCall(contracts, "typeSchemaList", calls, baseParams); + assertContractCall(contracts, "typeSchemaGet", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionList", calls, baseParams); + assertContractCall(contracts, "ingestionFunctionGet", calls, baseParams); + assertContractCall(contracts, "persistedDocumentList", calls, baseParams); + assertContractCall(contracts, "persistedDocumentGet", calls, baseParams); + assertContractCall(contracts, "webhookList", calls, baseParams); + assertContractCall(contracts, "webhookGet", calls, baseParams); +}); +//# sourceMappingURL=contracts.pull-contract-map.test.js.map \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.js.map b/test/contracts.pull-contract-map.test.js.map new file mode 100644 index 0000000..2d91e9d --- /dev/null +++ b/test/contracts.pull-contract-map.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"contracts.pull-contract-map.test.js","sourceRoot":"","sources":["contracts.pull-contract-map.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a666cd2..eb40821 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // See also https://aka.ms/tsconfig/module "module": "nodenext", "target": "esnext", - "types": [], + // "types": [], // For nodejs: // "lib": ["esnext"], "types": ["node"], @@ -39,6 +39,8 @@ "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force", - "skipLibCheck": true, - } + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "test", "docs", ".github", "tmp-compose", "node_modules"] } From af29694c0e99b4c04cf0979e6c1406e3053885b5 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 10:18:42 -0500 Subject: [PATCH 44/47] Clean up testing artifacts. --- .gitignore | 8 +- test/commands.apply-multi-env.test.d.ts | 2 - test/commands.apply-multi-env.test.d.ts.map | 1 - test/commands.apply-multi-env.test.js | 163 --- test/commands.apply-multi-env.test.js.map | 1 - test/commands.apply.auth-failures.test.d.ts | 2 - ...commands.apply.auth-failures.test.d.ts.map | 1 - test/commands.apply.auth-failures.test.js | 84 -- test/commands.apply.auth-failures.test.js.map | 1 - ...ommands.apply.create-env-failure.test.d.ts | 2 - ...nds.apply.create-env-failure.test.d.ts.map | 1 - .../commands.apply.create-env-failure.test.js | 85 -- ...mands.apply.create-env-failure.test.js.map | 1 - .../commands.apply.output-snapshots.test.d.ts | 2 - ...mands.apply.output-snapshots.test.d.ts.map | 1 - test/commands.apply.output-snapshots.test.js | 277 ----- ...ommands.apply.output-snapshots.test.js.map | 1 - ...nds.apply.partial-failure-writes.test.d.ts | 2 - ...apply.partial-failure-writes.test.d.ts.map | 1 - ...mands.apply.partial-failure-writes.test.js | 90 -- ...s.apply.partial-failure-writes.test.js.map | 1 - test/commands.apply.test.d.ts | 2 - test/commands.apply.test.d.ts.map | 1 - test/commands.apply.test.js | 135 -- test/commands.apply.test.js.map | 1 - test/commands.clone.test.d.ts | 2 - test/commands.clone.test.d.ts.map | 1 - test/commands.clone.test.js | 82 -- test/commands.clone.test.js.map | 1 - test/commands.env-rename.test.d.ts | 2 - test/commands.env-rename.test.d.ts.map | 1 - test/commands.env-rename.test.js | 148 --- test/commands.env-rename.test.js.map | 1 - test/commands.generate-apply-flow.test.d.ts | 2 - ...commands.generate-apply-flow.test.d.ts.map | 1 - test/commands.generate-apply-flow.test.js | 170 --- test/commands.generate-apply-flow.test.js.map | 1 - ...ommands.generate-apply-warn-flow.test.d.ts | 2 - ...nds.generate-apply-warn-flow.test.d.ts.map | 1 - .../commands.generate-apply-warn-flow.test.js | 163 --- ...mands.generate-apply-warn-flow.test.js.map | 1 - test/commands.generate-flow.test.d.ts | 2 - test/commands.generate-flow.test.d.ts.map | 1 - test/commands.generate-flow.test.js | 150 --- test/commands.generate-flow.test.js.map | 1 - test/commands.generate-status-flow.test.d.ts | 2 - ...ommands.generate-status-flow.test.d.ts.map | 1 - test/commands.generate-status-flow.test.js | 147 --- .../commands.generate-status-flow.test.js.map | 1 - test/commands.generate.test.d.ts | 2 - test/commands.generate.test.d.ts.map | 1 - test/commands.generate.test.js | 275 ----- test/commands.generate.test.js.map | 1 - test/commands.plan-exit.test.d.ts | 2 - test/commands.plan-exit.test.d.ts.map | 1 - test/commands.plan-exit.test.js | 72 -- test/commands.plan-exit.test.js.map | 1 - test/commands.pull-hardening.test.d.ts | 2 - test/commands.pull-hardening.test.d.ts.map | 1 - test/commands.pull-hardening.test.js | 216 ---- test/commands.pull-hardening.test.js.map | 1 - test/commands.pull.test.d.ts | 2 - test/commands.pull.test.d.ts.map | 1 - test/commands.pull.test.js | 241 ---- test/commands.pull.test.js.map | 1 - test/commands.status-multi-env.test.d.ts | 2 - test/commands.status-multi-env.test.d.ts.map | 1 - test/commands.status-multi-env.test.js | 140 --- test/commands.status-multi-env.test.js.map | 1 - test/commands.status-validate-exit.test.d.ts | 2 - ...ommands.status-validate-exit.test.d.ts.map | 1 - test/commands.status-validate-exit.test.js | 87 -- .../commands.status-validate-exit.test.js.map | 1 - test/commands.validate-rename-risk.test.d.ts | 2 - ...ommands.validate-rename-risk.test.d.ts.map | 1 - test/commands.validate-rename-risk.test.js | 79 -- .../commands.validate-rename-risk.test.js.map | 1 - ....apply.environment-alias-routing.test.d.ts | 2 - ...ly.environment-alias-routing.test.d.ts.map | 1 - ...se.apply.environment-alias-routing.test.js | 129 -- ...pply.environment-alias-routing.test.js.map | 1 - test/compose.apply.failures.test.d.ts | 2 - test/compose.apply.failures.test.d.ts.map | 1 - test/compose.apply.failures.test.js | 96 -- test/compose.apply.failures.test.js.map | 1 - test/compose.apply.rename-inference.test.d.ts | 2 - ...mpose.apply.rename-inference.test.d.ts.map | 1 - test/compose.apply.rename-inference.test.js | 370 ------ ...compose.apply.rename-inference.test.js.map | 1 - test/compose.management-client.test.d.ts | 2 - test/compose.management-client.test.d.ts.map | 1 - test/compose.management-client.test.js | 74 -- test/compose.management-client.test.js.map | 1 - test/compose.oauth-base.test.d.ts | 2 - test/compose.oauth-base.test.d.ts.map | 1 - test/compose.oauth-base.test.js | 93 -- test/compose.oauth-base.test.js.map | 1 - test/compose.state.test.d.ts | 2 - test/compose.state.test.d.ts.map | 1 - test/compose.state.test.js | 49 - test/compose.state.test.js.map | 1 - test/contracts.apply-contract-map.test.d.ts | 2 - ...contracts.apply-contract-map.test.d.ts.map | 1 - test/contracts.apply-contract-map.test.js | 1089 ----------------- test/contracts.apply-contract-map.test.js.map | 1 - test/contracts.apply-openapi-shape.test.d.ts | 2 - ...ontracts.apply-openapi-shape.test.d.ts.map | 1 - test/contracts.apply-openapi-shape.test.js | 1071 ---------------- .../contracts.apply-openapi-shape.test.js.map | 1 - test/contracts.pull-contract-map.test.d.ts | 2 - .../contracts.pull-contract-map.test.d.ts.map | 1 - test/contracts.pull-contract-map.test.js | 216 ---- test/contracts.pull-contract-map.test.js.map | 1 - 113 files changed, 7 insertions(+), 6104 deletions(-) delete mode 100644 test/commands.apply-multi-env.test.d.ts delete mode 100644 test/commands.apply-multi-env.test.d.ts.map delete mode 100644 test/commands.apply-multi-env.test.js delete mode 100644 test/commands.apply-multi-env.test.js.map delete mode 100644 test/commands.apply.auth-failures.test.d.ts delete mode 100644 test/commands.apply.auth-failures.test.d.ts.map delete mode 100644 test/commands.apply.auth-failures.test.js delete mode 100644 test/commands.apply.auth-failures.test.js.map delete mode 100644 test/commands.apply.create-env-failure.test.d.ts delete mode 100644 test/commands.apply.create-env-failure.test.d.ts.map delete mode 100644 test/commands.apply.create-env-failure.test.js delete mode 100644 test/commands.apply.create-env-failure.test.js.map delete mode 100644 test/commands.apply.output-snapshots.test.d.ts delete mode 100644 test/commands.apply.output-snapshots.test.d.ts.map delete mode 100644 test/commands.apply.output-snapshots.test.js delete mode 100644 test/commands.apply.output-snapshots.test.js.map delete mode 100644 test/commands.apply.partial-failure-writes.test.d.ts delete mode 100644 test/commands.apply.partial-failure-writes.test.d.ts.map delete mode 100644 test/commands.apply.partial-failure-writes.test.js delete mode 100644 test/commands.apply.partial-failure-writes.test.js.map delete mode 100644 test/commands.apply.test.d.ts delete mode 100644 test/commands.apply.test.d.ts.map delete mode 100644 test/commands.apply.test.js delete mode 100644 test/commands.apply.test.js.map delete mode 100644 test/commands.clone.test.d.ts delete mode 100644 test/commands.clone.test.d.ts.map delete mode 100644 test/commands.clone.test.js delete mode 100644 test/commands.clone.test.js.map delete mode 100644 test/commands.env-rename.test.d.ts delete mode 100644 test/commands.env-rename.test.d.ts.map delete mode 100644 test/commands.env-rename.test.js delete mode 100644 test/commands.env-rename.test.js.map delete mode 100644 test/commands.generate-apply-flow.test.d.ts delete mode 100644 test/commands.generate-apply-flow.test.d.ts.map delete mode 100644 test/commands.generate-apply-flow.test.js delete mode 100644 test/commands.generate-apply-flow.test.js.map delete mode 100644 test/commands.generate-apply-warn-flow.test.d.ts delete mode 100644 test/commands.generate-apply-warn-flow.test.d.ts.map delete mode 100644 test/commands.generate-apply-warn-flow.test.js delete mode 100644 test/commands.generate-apply-warn-flow.test.js.map delete mode 100644 test/commands.generate-flow.test.d.ts delete mode 100644 test/commands.generate-flow.test.d.ts.map delete mode 100644 test/commands.generate-flow.test.js delete mode 100644 test/commands.generate-flow.test.js.map delete mode 100644 test/commands.generate-status-flow.test.d.ts delete mode 100644 test/commands.generate-status-flow.test.d.ts.map delete mode 100644 test/commands.generate-status-flow.test.js delete mode 100644 test/commands.generate-status-flow.test.js.map delete mode 100644 test/commands.generate.test.d.ts delete mode 100644 test/commands.generate.test.d.ts.map delete mode 100644 test/commands.generate.test.js delete mode 100644 test/commands.generate.test.js.map delete mode 100644 test/commands.plan-exit.test.d.ts delete mode 100644 test/commands.plan-exit.test.d.ts.map delete mode 100644 test/commands.plan-exit.test.js delete mode 100644 test/commands.plan-exit.test.js.map delete mode 100644 test/commands.pull-hardening.test.d.ts delete mode 100644 test/commands.pull-hardening.test.d.ts.map delete mode 100644 test/commands.pull-hardening.test.js delete mode 100644 test/commands.pull-hardening.test.js.map delete mode 100644 test/commands.pull.test.d.ts delete mode 100644 test/commands.pull.test.d.ts.map delete mode 100644 test/commands.pull.test.js delete mode 100644 test/commands.pull.test.js.map delete mode 100644 test/commands.status-multi-env.test.d.ts delete mode 100644 test/commands.status-multi-env.test.d.ts.map delete mode 100644 test/commands.status-multi-env.test.js delete mode 100644 test/commands.status-multi-env.test.js.map delete mode 100644 test/commands.status-validate-exit.test.d.ts delete mode 100644 test/commands.status-validate-exit.test.d.ts.map delete mode 100644 test/commands.status-validate-exit.test.js delete mode 100644 test/commands.status-validate-exit.test.js.map delete mode 100644 test/commands.validate-rename-risk.test.d.ts delete mode 100644 test/commands.validate-rename-risk.test.d.ts.map delete mode 100644 test/commands.validate-rename-risk.test.js delete mode 100644 test/commands.validate-rename-risk.test.js.map delete mode 100644 test/compose.apply.environment-alias-routing.test.d.ts delete mode 100644 test/compose.apply.environment-alias-routing.test.d.ts.map delete mode 100644 test/compose.apply.environment-alias-routing.test.js delete mode 100644 test/compose.apply.environment-alias-routing.test.js.map delete mode 100644 test/compose.apply.failures.test.d.ts delete mode 100644 test/compose.apply.failures.test.d.ts.map delete mode 100644 test/compose.apply.failures.test.js delete mode 100644 test/compose.apply.failures.test.js.map delete mode 100644 test/compose.apply.rename-inference.test.d.ts delete mode 100644 test/compose.apply.rename-inference.test.d.ts.map delete mode 100644 test/compose.apply.rename-inference.test.js delete mode 100644 test/compose.apply.rename-inference.test.js.map delete mode 100644 test/compose.management-client.test.d.ts delete mode 100644 test/compose.management-client.test.d.ts.map delete mode 100644 test/compose.management-client.test.js delete mode 100644 test/compose.management-client.test.js.map delete mode 100644 test/compose.oauth-base.test.d.ts delete mode 100644 test/compose.oauth-base.test.d.ts.map delete mode 100644 test/compose.oauth-base.test.js delete mode 100644 test/compose.oauth-base.test.js.map delete mode 100644 test/compose.state.test.d.ts delete mode 100644 test/compose.state.test.d.ts.map delete mode 100644 test/compose.state.test.js delete mode 100644 test/compose.state.test.js.map delete mode 100644 test/contracts.apply-contract-map.test.d.ts delete mode 100644 test/contracts.apply-contract-map.test.d.ts.map delete mode 100644 test/contracts.apply-contract-map.test.js delete mode 100644 test/contracts.apply-contract-map.test.js.map delete mode 100644 test/contracts.apply-openapi-shape.test.d.ts delete mode 100644 test/contracts.apply-openapi-shape.test.d.ts.map delete mode 100644 test/contracts.apply-openapi-shape.test.js delete mode 100644 test/contracts.apply-openapi-shape.test.js.map delete mode 100644 test/contracts.pull-contract-map.test.d.ts delete mode 100644 test/contracts.pull-contract-map.test.d.ts.map delete mode 100644 test/contracts.pull-contract-map.test.js delete mode 100644 test/contracts.pull-contract-map.test.js.map diff --git a/.gitignore b/.gitignore index f1a90a0..c632255 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,10 @@ dist # Vite files vite.config.js.timestamp-* vite.config.ts.timestamp-* -.vite/ \ No newline at end of file +.vite/ + +# Test build artifacts (keep only .ts test sources) +test/**/*.js +test/**/*.d.ts +test/**/*.js.map +test/**/*.d.ts.map diff --git a/test/commands.apply-multi-env.test.d.ts b/test/commands.apply-multi-env.test.d.ts deleted file mode 100644 index 7ebc986..0000000 --- a/test/commands.apply-multi-env.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.apply-multi-env.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply-multi-env.test.d.ts.map b/test/commands.apply-multi-env.test.d.ts.map deleted file mode 100644 index c0bfbc6..0000000 --- a/test/commands.apply-multi-env.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply-multi-env.test.d.ts","sourceRoot":"","sources":["commands.apply-multi-env.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply-multi-env.test.js b/test/commands.apply-multi-env.test.js deleted file mode 100644 index f806b3b..0000000 --- a/test/commands.apply-multi-env.test.js +++ /dev/null @@ -1,163 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -import { readComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-multi-env-")); - const devDir = path.join(root, "env", "dev"); - const prodDir = path.join(root, "env", "prod"); - await fs.mkdir(devDir, { recursive: true }); - await fs.mkdir(prodDir, { recursive: true }); - await fs.writeFile(path.join(devDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(devDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(prodDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(prodDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - }, - prod: { - dir: "./env/prod", - description: "Prod", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - return { root, devDir, prodDir }; -} -function installMockFetch() { - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { - edges: [{ node: { environmentAlias: "dev" } }, { node: { environmentAlias: "prod" } }] - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/prod/collections") { - return jsonResponse(500, { error: "prod unavailable" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/prod/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(200, { edges: [] }); - }); - return () => { - globalThis.fetch = oldFetch; - }; -} -test("plan without --env evaluates all configured environments and warns if any environment warns", async () => { - const { root } = await createFixture(); - const restoreFetch = installMockFetch(); - const oldExitCode = process.exitCode; - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((a) => String(a)).join(" ")); - }; - let observedExitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - observedExitCode = process.exitCode; - } - finally { - restoreFetch(); - console.log = originalLog; - process.exitCode = oldExitCode; - } - assert.equal(output.some((line) => line.includes("[env=dev] collection:content")), true); - assert.equal(output.some((line) => line.includes("[env=prod] collection:*")), true); - assert.equal(output.some((line) => line.includes("Plan env dev: create=1, delete=0, update=0, noop=1, warn=0")), true); - assert.equal(output.some((line) => line.includes("Plan env prod: create=0, delete=0, update=0, noop=1, warn=1")), true); - assert.equal(output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), true); - assert.equal(output.some((line) => line.includes("Plan complete.")), true); - assert.equal(observedExitCode, 1); -}); -test("apply without --env writes lock/state for successful environments and skips failed environments", async () => { - const { root, devDir, prodDir } = await createFixture(); - const restoreFetch = installMockFetch(); - const oldExitCode = process.exitCode; - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((a) => String(a)).join(" ")); - }; - let observedExitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - observedExitCode = process.exitCode; - } - finally { - restoreFetch(); - console.log = originalLog; - process.exitCode = oldExitCode; - } - assert.equal(await exists(path.join(devDir, "compose.lock.json")), true); - assert.equal(await exists(path.join(prodDir, "compose.lock.json")), false); - const state = await readComposeState(root); - assert.ok(state); - const dev = state.environments.find((e) => e.alias === "dev"); - const prod = state.environments.find((e) => e.alias === "prod"); - assert.ok(dev); - assert.ok(prod); - assert.equal(dev.remoteAlias, "dev"); - assert.equal(prod.remoteAlias, undefined); - assert.equal(output.some((line) => line.includes("Apply env dev: create=1, delete=0, update=0, noop=1, warn=0")), true); - assert.equal(output.some((line) => line.includes("Apply env prod: create=0, delete=0, update=0, noop=1, warn=1")), true); - assert.equal(output.some((line) => line.includes("Environments processed: total=2, succeeded=1, warned=1")), true); - assert.equal(observedExitCode, 1); -}); -//# sourceMappingURL=commands.apply-multi-env.test.js.map \ No newline at end of file diff --git a/test/commands.apply-multi-env.test.js.map b/test/commands.apply-multi-env.test.js.map deleted file mode 100644 index 46d872c..0000000 --- a/test/commands.apply-multi-env.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply-multi-env.test.js","sourceRoot":"","sources":["commands.apply-multi-env.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.d.ts b/test/commands.apply.auth-failures.test.d.ts deleted file mode 100644 index 361f5f6..0000000 --- a/test/commands.apply.auth-failures.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.apply.auth-failures.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.d.ts.map b/test/commands.apply.auth-failures.test.d.ts.map deleted file mode 100644 index 1f8fa50..0000000 --- a/test/commands.apply.auth-failures.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.auth-failures.test.d.ts","sourceRoot":"","sources":["commands.apply.auth-failures.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.js b/test/commands.apply.auth-failures.test.js deleted file mode 100644 index 8d92922..0000000 --- a/test/commands.apply.auth-failures.test.js +++ /dev/null @@ -1,84 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createComposeRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-auth-fail-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(envDir, { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - return root; -} -test("runApply fails clearly when token endpoint returns non-200", async () => { - const root = await createComposeRoot(); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return new Response("bad client", { status: 401, headers: { "content-type": "text/plain" } }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await assert.rejects(() => runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }), /OAuth token request failed \(401\): bad client/); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("runApply fails clearly when token endpoint response lacks access_token", async () => { - const root = await createComposeRoot(); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { token_type: "Bearer", expires_in: 3600 }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await assert.rejects(() => runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }), /OAuth token response missing access_token/); - } - finally { - globalThis.fetch = oldFetch; - } -}); -//# sourceMappingURL=commands.apply.auth-failures.test.js.map \ No newline at end of file diff --git a/test/commands.apply.auth-failures.test.js.map b/test/commands.apply.auth-failures.test.js.map deleted file mode 100644 index 0f60d74..0000000 --- a/test/commands.apply.auth-failures.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.auth-failures.test.js","sourceRoot":"","sources":["commands.apply.auth-failures.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.d.ts b/test/commands.apply.create-env-failure.test.d.ts deleted file mode 100644 index 090984b..0000000 --- a/test/commands.apply.create-env-failure.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.apply.create-env-failure.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.d.ts.map b/test/commands.apply.create-env-failure.test.d.ts.map deleted file mode 100644 index df7ff2a..0000000 --- a/test/commands.apply.create-env-failure.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.create-env-failure.test.d.ts","sourceRoot":"","sources":["commands.apply.create-env-failure.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.js b/test/commands.apply.create-env-failure.test.js deleted file mode 100644 index 41ec976..0000000 --- a/test/commands.apply.create-env-failure.test.js +++ /dev/null @@ -1,85 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -import { readComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createComposeRootForMissingEnv() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-create-env-fail-")); - const envDir = path.join(root, "env", "new-env"); - await fs.mkdir(envDir, { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const cfg = { - version: 1, - project: "test-project", - environments: { - "new-env": { - dir: "./env/new-env", - description: "New Env", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - return { root, envDir }; -} -test("apply create-environment failure sets exitCode=1 and skips lock/state writes", async () => { - const { root, envDir } = await createComposeRootForMissingEnv(); - const oldFetch = globalThis.fetch; - const oldExitCode = process.exitCode; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(500, { error: "server-error" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "new-env", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assert.equal(process.exitCode, 1); - process.exitCode = oldExitCode; - const lockPath = path.join(envDir, "compose.lock.json"); - const lockExists = await exists(lockPath); - assert.equal(lockExists, false); - const state = await readComposeState(root); - assert.equal(state, null); -}); -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -//# sourceMappingURL=commands.apply.create-env-failure.test.js.map \ No newline at end of file diff --git a/test/commands.apply.create-env-failure.test.js.map b/test/commands.apply.create-env-failure.test.js.map deleted file mode 100644 index 785af3a..0000000 --- a/test/commands.apply.create-env-failure.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.create-env-failure.test.js","sourceRoot":"","sources":["commands.apply.create-env-failure.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.d.ts b/test/commands.apply.output-snapshots.test.d.ts deleted file mode 100644 index e54e9fa..0000000 --- a/test/commands.apply.output-snapshots.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.apply.output-snapshots.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.d.ts.map b/test/commands.apply.output-snapshots.test.d.ts.map deleted file mode 100644 index f78ddbb..0000000 --- a/test/commands.apply.output-snapshots.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.output-snapshots.test.d.ts","sourceRoot":"","sources":["commands.apply.output-snapshots.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.js b/test/commands.apply.output-snapshots.test.js deleted file mode 100644 index 0d541a4..0000000 --- a/test/commands.apply.output-snapshots.test.js +++ /dev/null @@ -1,277 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createComposeRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-output-snapshot-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(envDir, { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - return root; -} -async function createComposeRootWithCollections(collections) { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-output-snapshot-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(envDir, { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - return root; -} -function installMockFetch() { - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - return () => { - globalThis.fetch = oldFetch; - }; -} -function installMockFetchWithHandler(handler) { - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - return handler(u, method); - }); - return () => { - globalThis.fetch = oldFetch; - }; -} -function captureLogs() { - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((x) => String(x)).join(" ")); - }; - return { - output, - restore: () => { - console.log = originalLog; - } - }; -} -function operationLines(output) { - return output.filter((line) => /^(CREATE|DELETE|UPDATE|NOOP|WARN)\s+\[env=/.test(line)); -} -function finalSummaryLine(output) { - const line = output.find((l) => /(Plan|Apply) complete\./.test(l)); - assert.ok(line); - return line; -} -function environmentSummaryLine(output) { - const line = output.find((l) => /^Environments processed: /.test(l)); - assert.ok(line); - return line; -} -function perEnvironmentRunSummaryLine(output) { - const line = output.find((l) => /^(Plan|Apply) env /.test(l)); - assert.ok(line); - return line; -} -test("plan output operation lines and summary match expected snapshot", async () => { - const root = await createComposeRoot(); - const restoreFetch = installMockFetch(); - const { output, restore } = captureLogs(); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - } - finally { - restoreFetch(); - restore(); - process.exitCode = oldExitCode; - } - assert.deepEqual(operationLines(output), [ - "NOOP [env=dev] environment:dev", - "CREATE [env=dev] collection:content" - ]); - assert.equal(perEnvironmentRunSummaryLine(output), "Plan env dev: create=1, delete=0, update=0, noop=1, warn=0"); - assert.equal(environmentSummaryLine(output), "Environments processed: total=1, succeeded=1, warned=0"); - assert.equal(finalSummaryLine(output), "Plan complete. create=1, delete=0, update=0, noop=1, warn=0"); -}); -test("apply output operation lines and summary match expected snapshot", async () => { - const root = await createComposeRoot(); - const restoreFetch = installMockFetch(); - const { output, restore } = captureLogs(); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - } - finally { - restoreFetch(); - restore(); - process.exitCode = oldExitCode; - } - assert.deepEqual(operationLines(output), [ - "NOOP [env=dev] environment:dev", - "CREATE [env=dev] collection:content" - ]); - assert.equal(perEnvironmentRunSummaryLine(output), "Apply env dev: create=1, delete=0, update=0, noop=1, warn=0"); - assert.equal(environmentSummaryLine(output), "Environments processed: total=1, succeeded=1, warned=0"); - assert.equal(finalSummaryLine(output), "Apply complete. create=1, delete=0, update=0, noop=1, warn=0"); -}); -test("plan output shows inferred collection rename as update with explicit rename details", async () => { - const root = await createComposeRootWithCollections([{ alias: "content-new", description: "Main content" }]); - const restoreFetch = installMockFetchWithHandler((u, method) => { - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - const { output, restore } = captureLogs(); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - } - finally { - restoreFetch(); - restore(); - process.exitCode = oldExitCode; - } - assert.deepEqual(operationLines(output), [ - "NOOP [env=dev] environment:dev", - "UPDATE [env=dev] collection:content-new — rename content-old -> content-new" - ]); - assert.equal(perEnvironmentRunSummaryLine(output), "Plan env dev: create=0, delete=0, update=1, noop=1, warn=0"); - assert.equal(finalSummaryLine(output), "Plan complete. create=0, delete=0, update=1, noop=1, warn=0"); -}); -test("plan output for ambiguous collection rename fallback shows create/delete and no rename update line", async () => { - const root = await createComposeRootWithCollections([{ alias: "content-new", description: "Main content" }]); - const restoreFetch = installMockFetchWithHandler((u, method) => { - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { - edges: [{ node: { collectionAlias: "content-old-a" } }, { node: { collectionAlias: "content-old-b" } }] - }); - } - if ((u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old-a" || - u.pathname === "/v1/projects/test-project/environments/dev/collections/content-old-b") && - method === "GET") { - return jsonResponse(200, { description: "Main content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - const { output, restore } = captureLogs(); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - } - finally { - restoreFetch(); - restore(); - process.exitCode = oldExitCode; - } - const opLines = operationLines(output); - assert.equal(opLines.some((line) => line.includes("UPDATE [env=dev] collection:content-new — rename")), false); - assert.deepEqual(opLines, [ - "NOOP [env=dev] environment:dev", - "CREATE [env=dev] collection:content-new", - "DELETE [env=dev] collection:content-old-a — no unique rename match", - "DELETE [env=dev] collection:content-old-b — no unique rename match" - ]); - assert.equal(perEnvironmentRunSummaryLine(output), "Plan env dev: create=1, delete=2, update=0, noop=1, warn=0"); - assert.equal(finalSummaryLine(output), "Plan complete. create=1, delete=2, update=0, noop=1, warn=0"); -}); -//# sourceMappingURL=commands.apply.output-snapshots.test.js.map \ No newline at end of file diff --git a/test/commands.apply.output-snapshots.test.js.map b/test/commands.apply.output-snapshots.test.js.map deleted file mode 100644 index 6207c08..0000000 --- a/test/commands.apply.output-snapshots.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.output-snapshots.test.js","sourceRoot":"","sources":["commands.apply.output-snapshots.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.d.ts b/test/commands.apply.partial-failure-writes.test.d.ts deleted file mode 100644 index e274027..0000000 --- a/test/commands.apply.partial-failure-writes.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.apply.partial-failure-writes.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.d.ts.map b/test/commands.apply.partial-failure-writes.test.d.ts.map deleted file mode 100644 index f14d7bb..0000000 --- a/test/commands.apply.partial-failure-writes.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.partial-failure-writes.test.d.ts","sourceRoot":"","sources":["commands.apply.partial-failure-writes.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.js b/test/commands.apply.partial-failure-writes.test.js deleted file mode 100644 index c2eb945..0000000 --- a/test/commands.apply.partial-failure-writes.test.js +++ /dev/null @@ -1,90 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -import { readComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createComposeRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-partial-warn-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(envDir, { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - return { root, envDir }; -} -test("apply skips lock/state writes when nested resource create emits warning", async () => { - const { root, envDir } = await createComposeRoot(); - const oldFetch = globalThis.fetch; - const oldExitCode = process.exitCode; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(500, { error: "collection-create-fail" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assert.equal(process.exitCode, 1); - process.exitCode = oldExitCode; - const lockExists = await exists(path.join(envDir, "compose.lock.json")); - assert.equal(lockExists, false); - const state = await readComposeState(root); - assert.equal(state, null); -}); -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -//# sourceMappingURL=commands.apply.partial-failure-writes.test.js.map \ No newline at end of file diff --git a/test/commands.apply.partial-failure-writes.test.js.map b/test/commands.apply.partial-failure-writes.test.js.map deleted file mode 100644 index 0d9e007..0000000 --- a/test/commands.apply.partial-failure-writes.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.partial-failure-writes.test.js","sourceRoot":"","sources":["commands.apply.partial-failure-writes.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.apply.test.d.ts b/test/commands.apply.test.d.ts deleted file mode 100644 index 51c3e0f..0000000 --- a/test/commands.apply.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.apply.test.d.ts.map \ No newline at end of file diff --git a/test/commands.apply.test.d.ts.map b/test/commands.apply.test.d.ts.map deleted file mode 100644 index c0cf00d..0000000 --- a/test/commands.apply.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.test.d.ts","sourceRoot":"","sources":["commands.apply.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.apply.test.js b/test/commands.apply.test.js deleted file mode 100644 index 97e1614..0000000 --- a/test/commands.apply.test.js +++ /dev/null @@ -1,135 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -import { readComposeState } from "../src/compose/state.js"; -async function createComposeRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-cmd-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(envDir, { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Development", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - return { root, envDir }; -} -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -function installMockFetch(calls) { - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") { - bodyText = init.body; - } - else if (init?.body instanceof URLSearchParams) { - bodyText = init.body.toString(); - } - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - return () => { - globalThis.fetch = oldFetch; - }; -} -test("runApply prints operation lines with explicit environment context", async () => { - const { root } = await createComposeRoot(); - const calls = []; - const restoreFetch = installMockFetch(calls); - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((x) => String(x)).join(" ")); - }; - const originalExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - } - finally { - restoreFetch(); - console.log = originalLog; - process.exitCode = originalExitCode; - } - assert.equal(output.some((line) => line.includes("[env=dev] environment:dev")), true); - assert.equal(output.some((line) => line.includes("[env=dev] collection:content")), true); - assert.equal(calls.length > 0, true); -}); -test("runApply writes compose.lock.json and updates compose.state.json after successful apply", async () => { - const { root, envDir } = await createComposeRoot(); - const calls = []; - const restoreFetch = installMockFetch(calls); - const originalExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - } - finally { - restoreFetch(); - process.exitCode = originalExitCode; - } - const lockPath = path.join(envDir, "compose.lock.json"); - const lockText = await fs.readFile(lockPath, "utf8"); - const lock = JSON.parse(lockText); - assert.equal(lock.version, 1); - assert.equal(lock.project, "test-project"); - assert.equal(lock.environment, "dev"); - assert.equal(typeof lock.lastApplied?.at, "string"); - assert.equal(typeof lock.resources.collections?.hash, "string"); - assert.equal(typeof lock.resources.webhooks?.hash, "string"); - const state = await readComposeState(root); - assert.ok(state); - const envState = state.environments.find((e) => e.alias === "dev"); - assert.ok(envState); - assert.equal(envState.remoteAlias, "dev"); -}); -//# sourceMappingURL=commands.apply.test.js.map \ No newline at end of file diff --git a/test/commands.apply.test.js.map b/test/commands.apply.test.js.map deleted file mode 100644 index 84b4f5a..0000000 --- a/test/commands.apply.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.apply.test.js","sourceRoot":"","sources":["commands.apply.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.clone.test.d.ts b/test/commands.clone.test.d.ts deleted file mode 100644 index 12c6731..0000000 --- a/test/commands.clone.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.clone.test.d.ts.map \ No newline at end of file diff --git a/test/commands.clone.test.d.ts.map b/test/commands.clone.test.d.ts.map deleted file mode 100644 index d45f1ad..0000000 --- a/test/commands.clone.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.clone.test.d.ts","sourceRoot":"","sources":["commands.clone.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.clone.test.js b/test/commands.clone.test.js deleted file mode 100644 index 5114198..0000000 --- a/test/commands.clone.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { cloneCommand } from "../src/commands/clone.js"; -import { readComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -test("clone scaffolds project and pulls remote env state", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-clone-")); - const targetDir = path.join(root, "cloned-compose"); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/my-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/my-project/environments/dev/collections") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Main content" } }] }); - } - if (u.pathname === "/v1/projects/my-project/environments/dev/type-schemas") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/my-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/my-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/my-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await cloneCommand.handler({ - dir: targetDir, - project: "my-project", - env: "dev", - baseUrl: "https://management.example", - clientId: "client", - clientSecret: "secret", - force: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - const composeYamlPath = path.join(targetDir, "umbraco-compose.yaml"); - const cfg = YAML.parse(await fs.readFile(composeYamlPath, "utf8")); - assert.equal(cfg.project, "my-project"); - assert.equal(cfg.environments.dev?.dir, "./env/dev"); - const collectionsPath = path.join(targetDir, "env", "dev", "collections.json"); - const collections = JSON.parse(await fs.readFile(collectionsPath, "utf8")); - assert.equal(collections.collections[0]?.alias, "content"); - const state = await readComposeState(targetDir); - assert.ok(state); - assert.equal(state.environments.find((e) => e.alias === "dev")?.remoteAlias, "dev"); -}); -test("clone fails when target directory is non-empty without --force", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-clone-nonempty-")); - await fs.writeFile(path.join(root, "keep.txt"), "keep\n", "utf8"); - await assert.rejects(() => cloneCommand.handler({ - dir: root, - project: "my-project", - env: "dev", - baseUrl: "https://management.example", - clientId: "client", - clientSecret: "secret", - force: false - }), /target directory is not empty/); -}); -//# sourceMappingURL=commands.clone.test.js.map \ No newline at end of file diff --git a/test/commands.clone.test.js.map b/test/commands.clone.test.js.map deleted file mode 100644 index 4b7cec7..0000000 --- a/test/commands.clone.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.clone.test.js","sourceRoot":"","sources":["commands.clone.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,SAAS,YAAY,CAAC,MAAc,EAAE,IAAa;IACjD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED,IAAI,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;IACpE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACxE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAElC,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,KAAwB,EAAE,EAAE;QACrD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QACpG,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvB,IAAI,CAAC,CAAC,QAAQ,KAAK,gBAAgB;YAAE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3G,IAAI,CAAC,CAAC,QAAQ,KAAK,sCAAsC,EAAE,CAAC;YAC1D,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,sDAAsD,EAAE,CAAC;YAC1E,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/G,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,uDAAuD,EAAE,CAAC;YAC3E,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,8DAA8D,EAAE,CAAC;YAClF,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,sEAAsE,EAAE,CAAC;YAC1F,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,KAAK,mDAAmD,EAAE,CAAC;YACvE,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACzD,CAAC,CAAiB,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,YAAY,CAAC,OAAO,CAAC;YACzB,GAAG,EAAE,SAAS;YACd,OAAO,EAAE,YAAY;YACrB,GAAG,EAAE,KAAK;YACV,OAAO,EAAE,4BAA4B;YACrC,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;YACtB,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC;IAED,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;IACrE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,CAGhE,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACxC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;IAErD,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC;IAC/E,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,CAExE,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IAE3D,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACjB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;AACtF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;IAChF,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAAC,CAAC;IACjF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAElE,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CACH,YAAY,CAAC,OAAO,CAAC;QACnB,GAAG,EAAE,IAAI;QACT,OAAO,EAAE,YAAY;QACrB,GAAG,EAAE,KAAK;QACV,OAAO,EAAE,4BAA4B;QACrC,QAAQ,EAAE,QAAQ;QAClB,YAAY,EAAE,QAAQ;QACtB,KAAK,EAAE,KAAK;KACb,CAAC,EACJ,+BAA+B,CAChC,CAAC;AACJ,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/test/commands.env-rename.test.d.ts b/test/commands.env-rename.test.d.ts deleted file mode 100644 index 9318cf2..0000000 --- a/test/commands.env-rename.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.env-rename.test.d.ts.map \ No newline at end of file diff --git a/test/commands.env-rename.test.d.ts.map b/test/commands.env-rename.test.d.ts.map deleted file mode 100644 index 5e10345..0000000 --- a/test/commands.env-rename.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.env-rename.test.d.ts","sourceRoot":"","sources":["commands.env-rename.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.env-rename.test.js b/test/commands.env-rename.test.js deleted file mode 100644 index eb41b7c..0000000 --- a/test/commands.env-rename.test.js +++ /dev/null @@ -1,148 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { envRenameCommand } from "../src/commands/env-rename.js"; -import { createInitialState, markEnvironmentRemoteAlias, readComposeState, writeComposeState } from "../src/compose/state.js"; -async function writeComposeConfig(rootDir, cfg) { - await fs.writeFile(path.join(rootDir, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); -} -async function readComposeConfig(rootDir) { - const text = await fs.readFile(path.join(rootDir, "umbraco-compose.yaml"), "utf8"); - return YAML.parse(text); -} -function baseConfig() { - return { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - }, - prod: { - dir: "./env/prod", - description: "Prod", - managementBaseUrl: "https://management.example" - } - } - }; -} -test("env rename updates umbraco-compose.yaml and compose.state.json while preserving env identity", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-")); - await writeComposeConfig(root, baseConfig()); - const state = createInitialState("test-project", ["dev", "prod"]); - const dev = state.environments.find((e) => e.alias === "dev"); - assert.ok(dev); - markEnvironmentRemoteAlias(state, dev.id, "dev"); - await writeComposeState(root, state); - await envRenameCommand.handler({ - dir: root, - from: "dev", - to: "development", - moveDir: false - }); - const cfgAfter = await readComposeConfig(root); - assert.ok(cfgAfter.environments.development); - assert.equal(cfgAfter.environments.dev, undefined); - assert.equal(cfgAfter.environments.development?.dir, "./env/dev"); - const stateAfter = await readComposeState(root); - assert.ok(stateAfter); - const renamed = stateAfter.environments.find((e) => e.alias === "development"); - assert.ok(renamed); - assert.equal(renamed.id, dev.id); - assert.equal(renamed.remoteAlias, "dev"); -}); -test("env rename with --moveDir renames default env directory path and folder", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-move-")); - await writeComposeConfig(root, baseConfig()); - await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); - await fs.mkdir(path.join(root, "env", "dev"), { recursive: true }); - await fs.writeFile(path.join(root, "env", "dev", "collections.json"), "{\"collections\":[]}\n", "utf8"); - await envRenameCommand.handler({ - dir: root, - from: "dev", - to: "development", - moveDir: true - }); - const cfgAfter = await readComposeConfig(root); - assert.equal(cfgAfter.environments.development?.dir, "./env/development"); - const oldExists = await exists(path.join(root, "env", "dev")); - const newExists = await exists(path.join(root, "env", "development")); - assert.equal(oldExists, false); - assert.equal(newExists, true); -}); -test("env rename fails when target alias already exists and leaves files unchanged", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-fail-")); - const cfg = baseConfig(); - await writeComposeConfig(root, cfg); - const state = createInitialState("test-project", ["dev", "prod"]); - await writeComposeState(root, state); - const beforeYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); - const beforeState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); - await assert.rejects(() => envRenameCommand.handler({ - dir: root, - from: "dev", - to: "prod", - moveDir: false - })); - const afterYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); - const afterState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); - assert.equal(afterYaml, beforeYaml); - assert.equal(afterState, beforeState); -}); -test("env rename --moveDir fails on destination collision and leaves config/state unchanged", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-collision-")); - await writeComposeConfig(root, baseConfig()); - await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); - await fs.mkdir(path.join(root, "env", "dev"), { recursive: true }); - await fs.mkdir(path.join(root, "env", "development"), { recursive: true }); - const beforeYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); - const beforeState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); - await assert.rejects(() => envRenameCommand.handler({ - dir: root, - from: "dev", - to: "development", - moveDir: true - }), /destination already exists/); - const afterYaml = await fs.readFile(path.join(root, "umbraco-compose.yaml"), "utf8"); - const afterState = await fs.readFile(path.join(root, "compose.state.json"), "utf8"); - assert.equal(afterYaml, beforeYaml); - assert.equal(afterState, beforeState); -}); -test("env rename --moveDir succeeds when source directory is missing", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-env-rename-missing-src-")); - await writeComposeConfig(root, baseConfig()); - await writeComposeState(root, createInitialState("test-project", ["dev", "prod"])); - const oldExistsBefore = await exists(path.join(root, "env", "dev")); - assert.equal(oldExistsBefore, false); - await envRenameCommand.handler({ - dir: root, - from: "dev", - to: "development", - moveDir: true - }); - const cfgAfter = await readComposeConfig(root); - assert.equal(cfgAfter.environments.development?.dir, "./env/development"); - assert.equal(cfgAfter.environments.dev, undefined); - const stateAfter = await readComposeState(root); - assert.ok(stateAfter); - assert.ok(stateAfter.environments.find((e) => e.alias === "development")); - const oldExists = await exists(path.join(root, "env", "dev")); - const newExists = await exists(path.join(root, "env", "development")); - assert.equal(oldExists, false); - assert.equal(newExists, false); -}); -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -//# sourceMappingURL=commands.env-rename.test.js.map \ No newline at end of file diff --git a/test/commands.env-rename.test.js.map b/test/commands.env-rename.test.js.map deleted file mode 100644 index 395439a..0000000 --- a/test/commands.env-rename.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.env-rename.test.js","sourceRoot":"","sources":["commands.env-rename.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.d.ts b/test/commands.generate-apply-flow.test.d.ts deleted file mode 100644 index 309fb42..0000000 --- a/test/commands.generate-apply-flow.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.generate-apply-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.d.ts.map b/test/commands.generate-apply-flow.test.d.ts.map deleted file mode 100644 index 7ffd0f3..0000000 --- a/test/commands.generate-apply-flow.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-apply-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-apply-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.js b/test/commands.generate-apply-flow.test.js deleted file mode 100644 index 2088203..0000000 --- a/test/commands.generate-apply-flow.test.js +++ /dev/null @@ -1,170 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { generateCommand } from "../src/commands/generate.js"; -import { runApply } from "../src/commands/apply.js"; -import { readComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-apply-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return { root, envDir }; -} -test("generate -> apply writes lock/state and reports expected apply operations", async () => { - const { root, envDir } = await createFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Content" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "type-schema", - alias: "article", - description: "Article schema" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "ingestion-function", - alias: "map-content", - description: "Map content" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "persisted-doc", - alias: "software-query", - description: "Software query" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "webhook", - alias: "send-on-create", - description: "Webhook" - }); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { - return jsonResponse(404, { error: "not found" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { - return jsonResponse(201, { typeSchemaAlias: "article" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { persistedDocumentAlias: "software-query" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { webhookAlias: "send-on-create" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((x) => String(x)).join(" ")); - }; - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - assert.equal(process.exitCode, 0); - } - finally { - globalThis.fetch = oldFetch; - console.log = originalLog; - process.exitCode = oldExitCode; - } - const lockPath = path.join(envDir, "compose.lock.json"); - const lockExists = await exists(lockPath); - assert.equal(lockExists, true); - const state = await readComposeState(root); - assert.ok(state); - const envState = state.environments.find((e) => e.alias === "dev"); - assert.ok(envState); - assert.equal(envState.remoteAlias, "dev"); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] collection:content")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] type-schema:article")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] ingestion-function:map-content")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); - assert.equal(output.some((line) => line.includes("Apply complete. create=5, delete=0, update=0, noop=1, warn=0")), true); -}); -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -//# sourceMappingURL=commands.generate-apply-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-apply-flow.test.js.map b/test/commands.generate-apply-flow.test.js.map deleted file mode 100644 index 091caf9..0000000 --- a/test/commands.generate-apply-flow.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-apply-flow.test.js","sourceRoot":"","sources":["commands.generate-apply-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.d.ts b/test/commands.generate-apply-warn-flow.test.d.ts deleted file mode 100644 index 7eb6304..0000000 --- a/test/commands.generate-apply-warn-flow.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.generate-apply-warn-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.d.ts.map b/test/commands.generate-apply-warn-flow.test.d.ts.map deleted file mode 100644 index 71d7ca6..0000000 --- a/test/commands.generate-apply-warn-flow.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-apply-warn-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-apply-warn-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.js b/test/commands.generate-apply-warn-flow.test.js deleted file mode 100644 index a8b56e6..0000000 --- a/test/commands.generate-apply-warn-flow.test.js +++ /dev/null @@ -1,163 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { generateCommand } from "../src/commands/generate.js"; -import { runApply } from "../src/commands/apply.js"; -import { readComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-apply-warn-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return { root, envDir }; -} -test("generate -> apply warning path skips lock/state writes", async () => { - const { root, envDir } = await createFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Content" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "type-schema", - alias: "article", - description: "Article schema" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "ingestion-function", - alias: "map-content", - description: "Map content" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "persisted-doc", - alias: "software-query", - description: "Software query" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "webhook", - alias: "send-on-create", - description: "Webhook" - }); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { - return jsonResponse(404, { error: "not found" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas" && method === "POST") { - return jsonResponse(201, { typeSchemaAlias: "article" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(500, { error: "persisted-create-fail" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { webhookAlias: "send-on-create" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((x) => String(x)).join(" ")); - }; - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - assert.equal(process.exitCode, 1); - } - finally { - globalThis.fetch = oldFetch; - console.log = originalLog; - process.exitCode = oldExitCode; - } - const lockExists = await exists(path.join(envDir, "compose.lock.json")); - assert.equal(lockExists, false); - const state = await readComposeState(root); - assert.equal(state, null); - assert.equal(output.some((line) => line.includes("WARN") && line.includes("[env=dev] persisted-doc:software-query")), true); - assert.equal(output.some((line) => line.includes("Skipped lock/state writes because warnings were emitted.")), true); - assert.equal(output.some((line) => line.includes("Apply complete. create=5, delete=0, update=0, noop=1, warn=1")), true); -}); -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -//# sourceMappingURL=commands.generate-apply-warn-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-apply-warn-flow.test.js.map b/test/commands.generate-apply-warn-flow.test.js.map deleted file mode 100644 index c00adac..0000000 --- a/test/commands.generate-apply-warn-flow.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-apply-warn-flow.test.js","sourceRoot":"","sources":["commands.generate-apply-warn-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-flow.test.d.ts b/test/commands.generate-flow.test.d.ts deleted file mode 100644 index 8c7996d..0000000 --- a/test/commands.generate-flow.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.generate-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-flow.test.d.ts.map b/test/commands.generate-flow.test.d.ts.map deleted file mode 100644 index 1dad42b..0000000 --- a/test/commands.generate-flow.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-flow.test.js b/test/commands.generate-flow.test.js deleted file mode 100644 index 821086d..0000000 --- a/test/commands.generate-flow.test.js +++ /dev/null @@ -1,150 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { generateCommand } from "../src/commands/generate.js"; -import { validateCommand } from "../src/commands/validate.js"; -import { runApply } from "../src/commands/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-flow-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return { root, envDir }; -} -test("generate -> validate --strict -> plan baseline succeeds with expected operations", async () => { - const { root } = await createFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Content" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "type-schema", - alias: "article", - description: "Article schema" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "ingestion-function", - alias: "map-content", - description: "Map content" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "persisted-doc", - alias: "software-query", - description: "Software query" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "webhook", - alias: "send-on-create", - description: "Webhook" - }); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await validateCommand.handler({ - dir: root, - strict: true - }); - assert.equal(process.exitCode, 0); - } - finally { - process.exitCode = oldExitCode; - } - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { - return jsonResponse(404, { error: "not found" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - const originalLog = console.log; - const output = []; - console.log = (...args) => { - output.push(args.map((x) => String(x)).join(" ")); - }; - const oldExitCode2 = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - assert.equal(process.exitCode, 0); - } - finally { - globalThis.fetch = oldFetch; - console.log = originalLog; - process.exitCode = oldExitCode2; - } - assert.equal(output.some((line) => line.includes("NOOP [env=dev] environment:dev")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] collection:content")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] type-schema:article")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] ingestion-function:map-content")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] persisted-doc:software-query")), true); - assert.equal(output.some((line) => line.includes("CREATE [env=dev] webhook:send-on-create")), true); - assert.equal(output.some((line) => line.includes("Plan complete. create=5, delete=0, update=0, noop=1, warn=0")), true); -}); -//# sourceMappingURL=commands.generate-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-flow.test.js.map b/test/commands.generate-flow.test.js.map deleted file mode 100644 index 3993b46..0000000 --- a/test/commands.generate-flow.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-flow.test.js","sourceRoot":"","sources":["commands.generate-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.d.ts b/test/commands.generate-status-flow.test.d.ts deleted file mode 100644 index d055fb4..0000000 --- a/test/commands.generate-status-flow.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.generate-status-flow.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.d.ts.map b/test/commands.generate-status-flow.test.d.ts.map deleted file mode 100644 index b33ce3f..0000000 --- a/test/commands.generate-status-flow.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-status-flow.test.d.ts","sourceRoot":"","sources":["commands.generate-status-flow.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.js b/test/commands.generate-status-flow.test.js deleted file mode 100644 index 7e6f926..0000000 --- a/test/commands.generate-status-flow.test.js +++ /dev/null @@ -1,147 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { generateCommand } from "../src/commands/generate.js"; -import { statusCommand } from "../src/commands/status.js"; -import { runApply } from "../src/commands/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-status-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return { root, envDir }; -} -async function runStatusJson(args) { - const originalLog = console.log; - const out = []; - console.log = (...values) => { - out.push(values.map((v) => String(v)).join(" ")); - }; - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await statusCommand.handler({ - dir: args.root, - env: "dev", - json: true, - failOnChanges: args.failOnChanges - }); - const jsonText = out.find((line) => line.trim().startsWith("{")); - assert.ok(jsonText, "status command did not emit JSON output"); - return { - data: JSON.parse(jsonText), - exitCode: process.exitCode - }; - } - finally { - console.log = originalLog; - process.exitCode = oldExitCode; - } -} -test("generate -> status reports NO LOCK and sets non-zero exit with --failOnChanges", async () => { - const { root } = await createFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Content" - }); - const result = await runStatusJson({ root, failOnChanges: true }); - const groups = result.data.results[0]?.groups; - assert.ok(groups); - assert.equal(groups.collections?.status, "NO LOCK"); - assert.equal(groups.webhooks?.status, "NO LOCK"); - assert.equal(result.exitCode, 1); -}); -test("generate -> apply -> status transitions from UNCHANGED to CHANGED after local edit", async () => { - const { root, envDir } = await createFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Content" - }); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(200, { edges: [] }); - }); - const oldExit = process.exitCode; - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: false, - requireClean: false - }); - assert.equal(process.exitCode, 0); - } - finally { - globalThis.fetch = oldFetch; - process.exitCode = oldExit; - } - const unchanged = await runStatusJson({ root, failOnChanges: true }); - const unchangedGroups = unchanged.data.results[0]?.groups; - assert.ok(unchangedGroups); - assert.equal(unchangedGroups.collections?.status, "UNCHANGED"); - assert.equal(unchanged.exitCode, 0); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "Content updated" }] }, null, 2) + "\n", "utf8"); - const changed = await runStatusJson({ root, failOnChanges: true }); - const changedGroups = changed.data.results[0]?.groups; - assert.ok(changedGroups); - assert.equal(changedGroups.collections?.status, "CHANGED"); - assert.equal(changed.exitCode, 1); -}); -//# sourceMappingURL=commands.generate-status-flow.test.js.map \ No newline at end of file diff --git a/test/commands.generate-status-flow.test.js.map b/test/commands.generate-status-flow.test.js.map deleted file mode 100644 index 6bbabc0..0000000 --- a/test/commands.generate-status-flow.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate-status-flow.test.js","sourceRoot":"","sources":["commands.generate-status-flow.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.generate.test.d.ts b/test/commands.generate.test.d.ts deleted file mode 100644 index dac4efe..0000000 --- a/test/commands.generate.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.generate.test.d.ts.map \ No newline at end of file diff --git a/test/commands.generate.test.d.ts.map b/test/commands.generate.test.d.ts.map deleted file mode 100644 index b054a0d..0000000 --- a/test/commands.generate.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate.test.d.ts","sourceRoot":"","sources":["commands.generate.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.generate.test.js b/test/commands.generate.test.js deleted file mode 100644 index dd023dc..0000000 --- a/test/commands.generate.test.js +++ /dev/null @@ -1,275 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { generateCommand } from "../src/commands/generate.js"; -async function createGenerateFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-generate-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return { root, envDir }; -} -test("generate collection appends a collection entry", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Main content" - }); - const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); - assert.equal(collections.collections.length, 1); - assert.equal(collections.collections[0]?.alias, "content"); - assert.equal(collections.collections[0]?.description, "Main content"); -}); -test("generate type-schema creates schema file", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "type-schema", - alias: "article", - description: "Article schema" - }); - const schemaPath = path.join(envDir, "type-schemas", "article.schema.json"); - const schema = JSON.parse(await fs.readFile(schemaPath, "utf8")); - assert.equal(schema.alias, "article"); - assert.equal(schema.description, "Article schema"); - assert.equal(schema.schema.$schema, "https://umbracocompose.com/v1/schema"); - assert.ok(Array.isArray(schema.schema.allOf)); -}); -test("generate ingestion-function creates script and updates registry", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "ingestion-function", - alias: "map-content", - description: "Maps content" - }); - const registry = JSON.parse(await fs.readFile(path.join(envDir, "functions", "ingestion.json"), "utf8")); - assert.equal(registry.functions.length, 1); - assert.equal(registry.functions[0]?.alias, "map-content"); - assert.equal(registry.functions[0]?.scriptFile, "functions/ingestion/map-content.js"); - const script = await fs.readFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "utf8"); - assert.equal(script.includes("export default function"), true); -}); -test("generate persisted-doc creates query file and updates registry", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "persisted-doc", - alias: "software-query", - description: "Software query" - }); - const registry = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")); - assert.equal(registry.documents.length, 1); - assert.equal(registry.documents[0]?.alias, "software-query"); - assert.equal(registry.documents[0]?.description, "Software query"); - assert.equal(registry.documents[0]?.queryFile, "graphql/persisted/software-query.gql"); - const query = await fs.readFile(path.join(envDir, "graphql", "persisted", "software-query.gql"), "utf8"); - assert.equal(query.includes("query Example"), true); -}); -test("generate webhook creates webhook entry with default URL", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "webhook", - alias: "send-on-create", - description: "My webhook" - }); - const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); - assert.equal(webhooks.webhooks.length, 1); - assert.equal(webhooks.webhooks[0]?.alias, "send-on-create"); - assert.equal(webhooks.webhooks[0]?.description, "My webhook"); - assert.equal(webhooks.webhooks[0]?.url, "https://example.com/webhook"); - assert.deepEqual(webhooks.webhooks[0]?.eventTypes, ["content.ingested"]); - assert.deepEqual(webhooks.webhooks[0]?.collectionAliases, []); - assert.deepEqual(webhooks.webhooks[0]?.typeSchemaAliases, []); - assert.deepEqual(webhooks.webhooks[0]?.customHeaders, {}); -}); -test("generate webhook accepts explicit URL", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "webhook", - alias: "send-on-update", - description: "URL override", - url: "https://hooks.example.com/compose" - }); - const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); - assert.equal(webhooks.webhooks[0]?.alias, "send-on-update"); - assert.equal(webhooks.webhooks[0]?.url, "https://hooks.example.com/compose"); -}); -test("generate rejects duplicate aliases", async () => { - const { root } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Main content" - }); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - description: "Duplicate" - })); -}); -test("generate rejects --url for non-webhook entities", async () => { - const { root, envDir } = await createGenerateFixture(); - const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "content", - url: "https://hooks.example.com/compose" - }), /--url is only supported for entity=webhook/); - const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); - assert.equal(after, before); -}); -test("generate rejects invalid alias format", async () => { - const { root, envDir } = await createGenerateFixture(); - const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "Not_Kebab" - }), /must be kebab-case/); - const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); - assert.equal(after, before); -}); -test("generate rejects reserved alias names", async () => { - const { root, envDir } = await createGenerateFixture(); - const before = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "con" - }), /Reserved filename/); - const after = await fs.readFile(path.join(envDir, "collections.json"), "utf8"); - assert.equal(after, before); -}); -test("generate type-schema rejects when schema file already exists", async () => { - const { root, envDir } = await createGenerateFixture(); - const schemaPath = path.join(envDir, "type-schemas", "article.schema.json"); - await fs.writeFile(schemaPath, JSON.stringify({ - alias: "article", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [], - properties: {} - } - }, null, 2) + "\n", "utf8"); - const before = await fs.readFile(schemaPath, "utf8"); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "type-schema", - alias: "article" - }), /already exists/); - const after = await fs.readFile(schemaPath, "utf8"); - assert.equal(after, before); -}); -test("generate ingestion-function rejects when script file already exists", async () => { - const { root, envDir } = await createGenerateFixture(); - const scriptPath = path.join(envDir, "functions", "ingestion", "map-content.js"); - await fs.writeFile(scriptPath, "export default () => null;\n", "utf8"); - const registryPath = path.join(envDir, "functions", "ingestion.json"); - const beforeRegistry = await fs.readFile(registryPath, "utf8"); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "ingestion-function", - alias: "map-content" - }), /script already exists/); - const afterRegistry = await fs.readFile(registryPath, "utf8"); - assert.equal(afterRegistry, beforeRegistry); -}); -test("generate persisted-doc rejects when query file already exists", async () => { - const { root, envDir } = await createGenerateFixture(); - const queryPath = path.join(envDir, "graphql", "persisted", "software-query.gql"); - await fs.writeFile(queryPath, "query Existing { __typename }\n", "utf8"); - const registryPath = path.join(envDir, "graphql", "persisted.json"); - const beforeRegistry = await fs.readFile(registryPath, "utf8"); - await assert.rejects(() => generateCommand.handler({ - dir: root, - env: "dev", - entity: "persisted-doc", - alias: "software-query" - }), /query already exists/); - const afterRegistry = await fs.readFile(registryPath, "utf8"); - assert.equal(afterRegistry, beforeRegistry); -}); -test("generate allows same alias across different entity types", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "collection", - alias: "shared-alias", - description: "Shared collection alias" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "type-schema", - alias: "shared-alias", - description: "Shared schema alias" - }); - const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); - const schema = JSON.parse(await fs.readFile(path.join(envDir, "type-schemas", "shared-alias.schema.json"), "utf8")); - assert.equal(collections.collections.some((c) => c.alias === "shared-alias"), true); - assert.equal(schema.alias, "shared-alias"); -}); -test("generate allows same alias for webhook and persisted-doc", async () => { - const { root, envDir } = await createGenerateFixture(); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "webhook", - alias: "shared", - description: "Webhook alias" - }); - await generateCommand.handler({ - dir: root, - env: "dev", - entity: "persisted-doc", - alias: "shared", - description: "Persisted alias" - }); - const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); - const persisted = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")); - assert.equal(webhooks.webhooks.some((w) => w.alias === "shared"), true); - assert.equal(persisted.documents.some((d) => d.alias === "shared"), true); -}); -//# sourceMappingURL=commands.generate.test.js.map \ No newline at end of file diff --git a/test/commands.generate.test.js.map b/test/commands.generate.test.js.map deleted file mode 100644 index 4799550..0000000 --- a/test/commands.generate.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.generate.test.js","sourceRoot":"","sources":["commands.generate.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.plan-exit.test.d.ts b/test/commands.plan-exit.test.d.ts deleted file mode 100644 index e127170..0000000 --- a/test/commands.plan-exit.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.plan-exit.test.d.ts.map \ No newline at end of file diff --git a/test/commands.plan-exit.test.d.ts.map b/test/commands.plan-exit.test.d.ts.map deleted file mode 100644 index 559d42e..0000000 --- a/test/commands.plan-exit.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.plan-exit.test.d.ts","sourceRoot":"","sources":["commands.plan-exit.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.plan-exit.test.js b/test/commands.plan-exit.test.js deleted file mode 100644 index a77deb8..0000000 --- a/test/commands.plan-exit.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { runApply } from "../src/commands/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createComposeRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-plan-exit-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(envDir, { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - return root; -} -test("plan mode sets process.exitCode=1 when warnings are produced", async () => { - const root = await createComposeRoot(); - const oldFetch = globalThis.fetch; - const oldExitCode = process.exitCode; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(500, { error: "unavailable" }); - } - if (method === "GET") - return jsonResponse(200, { edges: [] }); - return jsonResponse(404, { error: "unexpected path" }); - }); - process.exitCode = 0; - try { - await runApply({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret", - plan: true, - requireClean: false - }); - assert.equal(process.exitCode, 1); - } - finally { - globalThis.fetch = oldFetch; - process.exitCode = oldExitCode; - } -}); -//# sourceMappingURL=commands.plan-exit.test.js.map \ No newline at end of file diff --git a/test/commands.plan-exit.test.js.map b/test/commands.plan-exit.test.js.map deleted file mode 100644 index 2dd3cf1..0000000 --- a/test/commands.plan-exit.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.plan-exit.test.js","sourceRoot":"","sources":["commands.plan-exit.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.pull-hardening.test.d.ts b/test/commands.pull-hardening.test.d.ts deleted file mode 100644 index 4c8ae5b..0000000 --- a/test/commands.pull-hardening.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.pull-hardening.test.d.ts.map \ No newline at end of file diff --git a/test/commands.pull-hardening.test.d.ts.map b/test/commands.pull-hardening.test.d.ts.map deleted file mode 100644 index 38826e5..0000000 --- a/test/commands.pull-hardening.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.pull-hardening.test.d.ts","sourceRoot":"","sources":["commands.pull-hardening.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.pull-hardening.test.js b/test/commands.pull-hardening.test.js deleted file mode 100644 index 4d8a06c..0000000 --- a/test/commands.pull-hardening.test.js +++ /dev/null @@ -1,216 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import crypto from "node:crypto"; -import YAML from "yaml"; -import { pullCommand } from "../src/commands/pull.js"; -import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-hardening-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await writeComposeState(root, createInitialState("test-project", ["dev"])); - return { root, envDir }; -} -function installSuccessfulPullFetch() { - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Main content" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { - return jsonResponse(200, { - typeSchemaAlias: "article", - description: "Article schema", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content", description: "Maps content" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/map-content") { - return jsonResponse(200, { - ingestionFunctionAlias: "map-content", - description: "Maps content", - script: "export default (x) => x;" - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "software-query", description: "Software query" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/software-query") { - return jsonResponse(200, { - persistedDocumentAlias: "software-query", - description: "Software query", - document: "query Software { content { items { __typename } } }" - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/send-on-create") { - return jsonResponse(200, { - webhookAlias: "send-on-create", - description: "Webhook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["article"], - customHeaders: { "x-api-key": "abc123" } - }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - return () => { - globalThis.fetch = oldFetch; - }; -} -async function snapshotDir(root) { - const files = await listFiles(root); - const parts = []; - for (const rel of files) { - const abs = path.join(root, rel); - const text = await fs.readFile(abs, "utf8"); - parts.push({ - file: rel, - hash: sha(text) - }); - } - parts.sort((a, b) => a.file.localeCompare(b.file)); - return sha(JSON.stringify(parts)); -} -test("pull is idempotent for unchanged remote responses", async () => { - const { root, envDir } = await createFixture(); - const restoreFetch = installSuccessfulPullFetch(); - try { - await pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }); - const first = await snapshotDir(envDir); - await pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }); - const second = await snapshotDir(envDir); - assert.equal(second, first); - } - finally { - restoreFetch(); - } -}); -test("pull partial failure keeps previous state remoteAlias unchanged and does not partially mutate local files", async () => { - const { root, envDir } = await createFixture(); - // First do a successful pull to set baseline and state - const restoreSuccess = installSuccessfulPullFetch(); - try { - await pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }); - } - finally { - restoreSuccess(); - } - const stateBefore = await readComposeState(root); - assert.ok(stateBefore); - const remoteBefore = stateBefore.environments.find((e) => e.alias === "dev")?.remoteAlias; - assert.equal(remoteBefore, "dev"); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content", description: "Changed before fail" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { - return jsonResponse(500, { error: "type-schema-list-fail" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await assert.rejects(() => pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }), /Failed to list type schemas \(500\)\./); - } - finally { - globalThis.fetch = oldFetch; - } - const stateAfter = await readComposeState(root); - assert.ok(stateAfter); - const remoteAfter = stateAfter.environments.find((e) => e.alias === "dev")?.remoteAlias; - // state is not advanced during failed pull - assert.equal(remoteAfter, "dev"); - const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); - assert.equal(collections.collections[0]?.description, "Main content"); -}); -async function listFiles(root) { - const out = []; - async function walk(dir) { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const abs = path.join(dir, entry.name); - if (entry.isDirectory()) { - await walk(abs); - } - else if (entry.isFile()) { - out.push(path.relative(root, abs)); - } - } - } - await walk(root); - return out; -} -function sha(s) { - return crypto.createHash("sha256").update(s).digest("hex"); -} -//# sourceMappingURL=commands.pull-hardening.test.js.map \ No newline at end of file diff --git a/test/commands.pull-hardening.test.js.map b/test/commands.pull-hardening.test.js.map deleted file mode 100644 index 2c62c81..0000000 --- a/test/commands.pull-hardening.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.pull-hardening.test.js","sourceRoot":"","sources":["commands.pull-hardening.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.pull.test.d.ts b/test/commands.pull.test.d.ts deleted file mode 100644 index 568a5c4..0000000 --- a/test/commands.pull.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.pull.test.d.ts.map \ No newline at end of file diff --git a/test/commands.pull.test.d.ts.map b/test/commands.pull.test.d.ts.map deleted file mode 100644 index fe9d66e..0000000 --- a/test/commands.pull.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.pull.test.d.ts","sourceRoot":"","sources":["commands.pull.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.pull.test.js b/test/commands.pull.test.js deleted file mode 100644 index 69301ee..0000000 --- a/test/commands.pull.test.js +++ /dev/null @@ -1,241 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { pullCommand } from "../src/commands/pull.js"; -import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - // stale files that should be removed on pull - await fs.writeFile(path.join(envDir, "type-schemas", "stale.schema.json"), "{}\n", "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "stale.js"), "export default null;\n", "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "stale.gql"), "query Stale { __typename }\n", "utf8"); - await writeComposeState(root, createInitialState("test-project", ["dev"])); - return { root, envDir }; -} -test("pull writes local files from API and updates state remoteAlias", async () => { - const { root, envDir } = await createFixture(); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(200, { - edges: [{ node: { collectionAlias: "content", description: "Main content" } }] - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { - return jsonResponse(200, { - typeSchemaAlias: "article", - description: "Article schema", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { - edges: [{ node: { ingestionFunctionAlias: "map-content", description: "Maps content" } }] - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/map-content") { - return jsonResponse(200, { - ingestionFunctionAlias: "map-content", - description: "Maps content", - script: "export default (x) => x;" - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { - edges: [{ node: { persistedDocumentAlias: "software-query", description: "Software query" } }] - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/software-query") { - return jsonResponse(200, { - persistedDocumentAlias: "software-query", - description: "Software query", - document: "query Software { content { items { __typename } } }" - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/send-on-create") { - return jsonResponse(200, { - webhookAlias: "send-on-create", - description: "Webhook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["article"], - customHeaders: { "x-api-key": "abc123" } - }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }); - } - finally { - globalThis.fetch = oldFetch; - } - const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); - assert.equal(collections.collections[0]?.alias, "content"); - const schema = JSON.parse(await fs.readFile(path.join(envDir, "type-schemas", "article.schema.json"), "utf8")); - assert.equal(schema.alias, "article"); - assert.equal(await exists(path.join(envDir, "type-schemas", "stale.schema.json")), false); - const ingestionRegistry = JSON.parse(await fs.readFile(path.join(envDir, "functions", "ingestion.json"), "utf8")); - assert.equal(ingestionRegistry.functions[0]?.alias, "map-content"); - assert.equal(await exists(path.join(envDir, "functions", "ingestion", "stale.js")), false); - const persistedRegistry = JSON.parse(await fs.readFile(path.join(envDir, "graphql", "persisted.json"), "utf8")); - assert.equal(persistedRegistry.documents[0]?.alias, "software-query"); - assert.equal(await exists(path.join(envDir, "graphql", "persisted", "stale.gql")), false); - const webhooks = JSON.parse(await fs.readFile(path.join(envDir, "webhooks.json"), "utf8")); - assert.equal(webhooks.webhooks[0]?.alias, "send-on-create"); - const state = await readComposeState(root); - assert.ok(state); - assert.equal(state.environments.find((e) => e.alias === "dev")?.remoteAlias, "dev"); -}); -test("pull fails when target environment is not present remotely", async () => { - const { root } = await createFixture(); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "other" } }] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await assert.rejects(() => pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }), /Environment "dev" was not found remotely/); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("pull uses state remoteAlias when local alias differs during pending rename", async () => { - const { root, envDir } = await createFixture(); - const composePath = path.join(root, "umbraco-compose.yaml"); - const cfgText = await fs.readFile(composePath, "utf8"); - const cfg = YAML.parse(cfgText); - cfg.environments = { - "dev-renamed": { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - }; - await fs.writeFile(composePath, YAML.stringify(cfg), "utf8"); - const state = await readComposeState(root); - assert.ok(state); - state.environments[0].alias = "dev-renamed"; - state.environments[0].remoteAlias = "dev"; - await writeComposeState(root, state); - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(200, { - edges: [{ node: { collectionAlias: "content", description: "Main content" } }] - }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await pullCommand.handler({ - dir: root, - env: "dev-renamed", - clientId: "client", - clientSecret: "secret" - }); - } - finally { - globalThis.fetch = oldFetch; - } - const collections = JSON.parse(await fs.readFile(path.join(envDir, "collections.json"), "utf8")); - assert.equal(collections.collections[0]?.alias, "content"); - const after = await readComposeState(root); - assert.ok(after); - const envState = after.environments.find((e) => e.alias === "dev-renamed"); - assert.ok(envState); - assert.equal(envState.remoteAlias, "dev"); -}); -async function exists(p) { - try { - await fs.stat(p); - return true; - } - catch { - return false; - } -} -//# sourceMappingURL=commands.pull.test.js.map \ No newline at end of file diff --git a/test/commands.pull.test.js.map b/test/commands.pull.test.js.map deleted file mode 100644 index a186d5e..0000000 --- a/test/commands.pull.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.pull.test.js","sourceRoot":"","sources":["commands.pull.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.status-multi-env.test.d.ts b/test/commands.status-multi-env.test.d.ts deleted file mode 100644 index ab7f899..0000000 --- a/test/commands.status-multi-env.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.status-multi-env.test.d.ts.map \ No newline at end of file diff --git a/test/commands.status-multi-env.test.d.ts.map b/test/commands.status-multi-env.test.d.ts.map deleted file mode 100644 index 67561a0..0000000 --- a/test/commands.status-multi-env.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.status-multi-env.test.d.ts","sourceRoot":"","sources":["commands.status-multi-env.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.status-multi-env.test.js b/test/commands.status-multi-env.test.js deleted file mode 100644 index a9de2ac..0000000 --- a/test/commands.status-multi-env.test.js +++ /dev/null @@ -1,140 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { statusCommand } from "../src/commands/status.js"; -import { computeDesiredHashes } from "../src/compose/lock.js"; -import { ensureStateForAliases, markEnvironmentRemoteAlias, writeComposeState } from "../src/compose/state.js"; -async function createEnvScaffold(envDir) { - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-status-multi-env-")); - const devDir = path.join(root, "env", "dev"); - const prodDir = path.join(root, "env", "prod"); - await createEnvScaffold(devDir); - await createEnvScaffold(prodDir); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - managementBaseUrl: "https://management.example" - }, - prod: { - dir: "./env/prod", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - return { root, devDir, prodDir }; -} -async function writeMatchingLock(envDir) { - const desired = await computeDesiredHashes(envDir); - const lock = { - version: 1, - project: "test-project", - environment: path.basename(envDir), - resources: desired - }; - await fs.writeFile(path.join(envDir, "compose.lock.json"), JSON.stringify(lock, null, 2) + "\n", "utf8"); -} -async function runStatusJson(args) { - const originalLog = console.log; - const out = []; - console.log = (...values) => { - out.push(values.map((v) => String(v)).join(" ")); - }; - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await statusCommand.handler({ - dir: args.root, - json: true, - failOnChanges: args.failOnChanges - }); - const jsonText = out.find((line) => line.trim().startsWith("{")); - assert.ok(jsonText, "status command did not emit JSON output"); - return { - data: JSON.parse(jsonText), - exitCode: process.exitCode - }; - } - finally { - console.log = originalLog; - process.exitCode = oldExitCode; - } -} -async function runStatusHuman(args) { - const originalLog = console.log; - const out = []; - console.log = (...values) => { - out.push(values.map((v) => String(v)).join(" ")); - }; - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await statusCommand.handler({ - dir: args.root, - env: args.env, - json: false, - failOnChanges: args.failOnChanges - }); - return { output: out, exitCode: process.exitCode }; - } - finally { - console.log = originalLog; - process.exitCode = oldExitCode; - } -} -test("status without --env returns results for all configured environments", async () => { - const { root, devDir, prodDir } = await createFixture(); - await writeMatchingLock(devDir); - await writeMatchingLock(prodDir); - const result = await runStatusJson({ root, failOnChanges: true }); - const envs = result.data.results.map((r) => r.env).sort(); - assert.deepEqual(envs, ["dev", "prod"]); - assert.equal(result.exitCode, 0); -}); -test("status across multiple environments sets exitCode=1 when any environment has changes/no lock", async () => { - const { root, devDir } = await createFixture(); - await writeMatchingLock(devDir); - // prod intentionally has no lock - const result = await runStatusJson({ root, failOnChanges: true }); - const dev = result.data.results.find((r) => r.env === "dev"); - const prod = result.data.results.find((r) => r.env === "prod"); - assert.ok(dev); - assert.ok(prod); - assert.equal(dev.groups.collections?.status, "UNCHANGED"); - assert.equal(prod.groups.collections?.status, "NO LOCK"); - assert.equal(result.exitCode, 1); -}); -test("status includes state identity context for pending rename in json and human output", async () => { - const { root, devDir } = await createFixture(); - await writeMatchingLock(devDir); - const aliases = ["dev", "prod"]; - const state = ensureStateForAliases(null, "test-project", aliases).state; - const devState = state.environments.find((e) => e.alias === "dev"); - assert.ok(devState); - const marked = markEnvironmentRemoteAlias(state, devState.id, "old-dev"); - assert.equal(marked, true); - await writeComposeState(root, state); - const jsonResult = await runStatusJson({ root, failOnChanges: false }); - const devJson = jsonResult.data.results.find((r) => r.env === "dev"); - assert.ok(devJson); - assert.equal(devJson.remoteAlias, "old-dev"); - assert.equal(devJson.renamePending, true); - const humanResult = await runStatusHuman({ root, env: "dev", failOnChanges: false }); - assert.equal(humanResult.output.some((line) => line.includes("Identity: pending rename (old-dev -> dev)")), true); -}); -//# sourceMappingURL=commands.status-multi-env.test.js.map \ No newline at end of file diff --git a/test/commands.status-multi-env.test.js.map b/test/commands.status-multi-env.test.js.map deleted file mode 100644 index 357c2a6..0000000 --- a/test/commands.status-multi-env.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.status-multi-env.test.js","sourceRoot":"","sources":["commands.status-multi-env.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAiB,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAmB/G,KAAK,UAAU,iBAAiB,CAAC,MAAc;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/E,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAChH,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1G,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,gBAAgB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACzH,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,gBAAgB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AACzH,CAAC;AAED,KAAK,UAAU,aAAa;IAC1B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,2BAA2B,CAAC,CAAC,CAAC;IACnF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAE/C,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAEjC,MAAM,GAAG,GAAkB;QACzB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,cAAc;QACvB,YAAY,EAAE;YACZ,GAAG,EAAE;gBACH,GAAG,EAAE,WAAW;gBAChB,iBAAiB,EAAE,4BAA4B;aAChD;YACD,IAAI,EAAE;gBACJ,GAAG,EAAE,YAAY;gBACjB,iBAAiB,EAAE,4BAA4B;aAChD;SACF;KACF,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IAEzF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,MAAc;IAC7C,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,IAAI,GAAa;QACrB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,cAAc;QACvB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAClC,SAAS,EAAE,OAAO;KACnB,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3G,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAG5B;IACC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,MAAiB,EAAE,EAAE;QACrC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,aAAa,CAAC,OAAO,CAAC;YAC1B,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,IAAI,EAAE,IAAI;YACV,aAAa,EAAE,IAAI,CAAC,aAAa;SAClC,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QACjE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,yCAAyC,CAAC,CAAC;QAC/D,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAe;YACxC,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,GAAG,GAAG,WAAW,CAAC;QAC1B,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IACjC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAI7B;IACC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,MAAiB,EAAE,EAAE;QACrC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,aAAa,CAAC,OAAO,CAAC;YAC1B,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,IAAI,EAAE,KAAK;YACX,aAAa,EAAE,IAAI,CAAC,aAAa;SAClC,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IACrD,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,GAAG,GAAG,WAAW,CAAC;QAC1B,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IACjC,CAAC;AACH,CAAC;AAED,IAAI,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;IACtF,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;IACxD,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAEjC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACxC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8FAA8F,EAAE,KAAK,IAAI,EAAE;IAC9G,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;IAC/C,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChC,iCAAiC;IAEjC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAClE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;IAE/D,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IAChB,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAC1D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IACzD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oFAAoF,EAAE,KAAK,IAAI,EAAE;IACpG,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;IAC/C,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAEhC,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC;IACzE,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IACnE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IACpB,MAAM,MAAM,GAAG,0BAA0B,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IACzE,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3B,MAAM,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAErC,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IACvE,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC;IACrE,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;IACnB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;IAE1C,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,MAAM,CAAC,KAAK,CACV,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,2CAA2C,CAAC,CAAC,EAC7F,IAAI,CACL,CAAC;AACJ,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.d.ts b/test/commands.status-validate-exit.test.d.ts deleted file mode 100644 index 0e047f3..0000000 --- a/test/commands.status-validate-exit.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.status-validate-exit.test.d.ts.map \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.d.ts.map b/test/commands.status-validate-exit.test.d.ts.map deleted file mode 100644 index 56a0f75..0000000 --- a/test/commands.status-validate-exit.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.status-validate-exit.test.d.ts","sourceRoot":"","sources":["commands.status-validate-exit.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.js b/test/commands.status-validate-exit.test.js deleted file mode 100644 index a776f39..0000000 --- a/test/commands.status-validate-exit.test.js +++ /dev/null @@ -1,87 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { statusCommand } from "../src/commands/status.js"; -import { validateCommand } from "../src/commands/validate.js"; -async function createStatusFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-status-exit-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return root; -} -async function createValidateFixtureWithWarnings() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-validate-exit-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "Not-Kebab" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - return root; -} -test("status --failOnChanges sets process.exitCode=1 when lock is missing", async () => { - const root = await createStatusFixture(); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await statusCommand.handler({ - dir: root, - env: "dev", - json: true, - failOnChanges: true - }); - assert.equal(process.exitCode, 1); - } - finally { - process.exitCode = oldExitCode; - } -}); -test("validate --strict sets process.exitCode=1 when warnings exist", async () => { - const root = await createValidateFixtureWithWarnings(); - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await validateCommand.handler({ - dir: root, - strict: true - }); - assert.equal(process.exitCode, 1); - } - finally { - process.exitCode = oldExitCode; - } -}); -//# sourceMappingURL=commands.status-validate-exit.test.js.map \ No newline at end of file diff --git a/test/commands.status-validate-exit.test.js.map b/test/commands.status-validate-exit.test.js.map deleted file mode 100644 index 891336d..0000000 --- a/test/commands.status-validate-exit.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.status-validate-exit.test.js","sourceRoot":"","sources":["commands.status-validate-exit.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.d.ts b/test/commands.validate-rename-risk.test.d.ts deleted file mode 100644 index 903a1df..0000000 --- a/test/commands.validate-rename-risk.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=commands.validate-rename-risk.test.d.ts.map \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.d.ts.map b/test/commands.validate-rename-risk.test.d.ts.map deleted file mode 100644 index 4690ca1..0000000 --- a/test/commands.validate-rename-risk.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.validate-rename-risk.test.d.ts","sourceRoot":"","sources":["commands.validate-rename-risk.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.js b/test/commands.validate-rename-risk.test.js deleted file mode 100644 index fcc9073..0000000 --- a/test/commands.validate-rename-risk.test.js +++ /dev/null @@ -1,79 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import YAML from "yaml"; -import { validateCommand } from "../src/commands/validate.js"; -async function createFixtureWithCollectionRenameRisk() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-validate-rename-risk-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ - collections: [ - { alias: "content-a", description: "Same description" }, - { alias: "content-b", description: "Same description" } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - // Keep schema checks green so test isolates rename-risk warning behavior. - await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ - alias: "article", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: {} - } - }, null, 2), "utf8"); - return root; -} -async function runValidateCapture(args) { - const originalLog = console.log; - const output = []; - console.log = (...values) => { - output.push(values.map((v) => String(v)).join(" ")); - }; - const oldExitCode = process.exitCode; - process.exitCode = 0; - try { - await validateCommand.handler({ - dir: args.root, - strict: args.strict - }); - return { output, exitCode: process.exitCode }; - } - finally { - console.log = originalLog; - process.exitCode = oldExitCode; - } -} -test("validate warns on duplicate rename-signatures in non-strict mode without failing", async () => { - const root = await createFixtureWithCollectionRenameRisk(); - const result = await runValidateCapture({ root, strict: false }); - assert.equal(result.output.some((line) => line.includes("identical rename-signatures")), true); - assert.equal(result.output.some((line) => line.includes("Resolve by: ensure collection descriptions are distinct")), true); - assert.equal(result.exitCode, 0); -}); -test("validate --strict fails when duplicate rename-signature warnings are present", async () => { - const root = await createFixtureWithCollectionRenameRisk(); - const result = await runValidateCapture({ root, strict: true }); - assert.equal(result.output.some((line) => line.includes("identical rename-signatures")), true); - assert.equal(result.output.some((line) => line.includes("apply the intended create/delete explicitly")), true); - assert.equal(result.exitCode, 1); -}); -//# sourceMappingURL=commands.validate-rename-risk.test.js.map \ No newline at end of file diff --git a/test/commands.validate-rename-risk.test.js.map b/test/commands.validate-rename-risk.test.js.map deleted file mode 100644 index 1fb7e3f..0000000 --- a/test/commands.validate-rename-risk.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"commands.validate-rename-risk.test.js","sourceRoot":"","sources":["commands.validate-rename-risk.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAQ9D,KAAK,UAAU,qCAAqC;IAClD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,+BAA+B,CAAC,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/E,MAAM,GAAG,GAAkB;QACzB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,cAAc;QACvB,YAAY,EAAE;YACZ,GAAG,EAAE;gBACH,GAAG,EAAE,WAAW;gBAChB,iBAAiB,EAAE,4BAA4B;aAChD;SACF;KACF,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IAEzF,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACrC,IAAI,CAAC,SAAS,CACZ;QACE,WAAW,EAAE;YACX,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE;YACvD,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE;SACxD;KACF,EACD,IAAI,EACJ,CAAC,CACF,EACD,MAAM,CACP,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1G,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,gBAAgB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACzH,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,gBAAgB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAEvH,0EAA0E;IAC1E,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,qBAAqB,CAAC,EACxD,IAAI,CAAC,SAAS,CACZ;QACE,KAAK,EAAE,SAAS;QAChB,MAAM,EAAE;YACN,OAAO,EAAE,sCAAsC;YAC/C,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,oCAAoC,EAAE,CAAC;YACvD,UAAU,EAAE,EAAE;SACf;KACF,EACD,IAAI,EACJ,CAAC,CACF,EACD,MAAM,CACP,CAAC;IAEF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,IAGjC;IACC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,MAAiB,EAAE,EAAE;QACrC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,eAAe,CAAC,OAAO,CAAC;YAC5B,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IAChD,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,GAAG,GAAG,WAAW,CAAC;QAC1B,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IACjC,CAAC;AACH,CAAC;AAED,IAAI,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;IAClG,MAAM,IAAI,GAAG,MAAM,qCAAqC,EAAE,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAEjE,MAAM,CAAC,KAAK,CACV,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,EAC1E,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CACV,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,yDAAyD,CAAC,CAAC,EACtG,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;IAC9F,MAAM,IAAI,GAAG,MAAM,qCAAqC,EAAE,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAEhE,MAAM,CAAC,KAAK,CACV,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,EAC1E,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CACV,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,6CAA6C,CAAC,CAAC,EAC1F,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.d.ts b/test/compose.apply.environment-alias-routing.test.d.ts deleted file mode 100644 index f697a49..0000000 --- a/test/compose.apply.environment-alias-routing.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=compose.apply.environment-alias-routing.test.d.ts.map \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.d.ts.map b/test/compose.apply.environment-alias-routing.test.d.ts.map deleted file mode 100644 index af3f0f7..0000000 --- a/test/compose.apply.environment-alias-routing.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.apply.environment-alias-routing.test.d.ts","sourceRoot":"","sources":["compose.apply.environment-alias-routing.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.js b/test/compose.apply.environment-alias-routing.test.js deleted file mode 100644 index 4c7f516..0000000 --- a/test/compose.apply.environment-alias-routing.test.js +++ /dev/null @@ -1,129 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { applyEnvironment } from "../src/compose/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function makeEnvDirWithCollections() { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-env-")); - await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - return dir; -} -test("plan with pending rename reads nested resources from remote alias path", async () => { - const envDir = await makeEnvDirWithCollections(); - const calls = []; - const baseUrl = "https://management.example"; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") { - bodyText = init.body; - } - else if (init?.body instanceof URLSearchParams) { - bodyText = init.body.toString(); - } - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { - edges: [{ node: { environmentAlias: "iac-dev" } }] - }); - } - if (u.pathname === "/v1/projects/proj/environments/iac-dev/collections") { - return jsonResponse(200, { edges: [] }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "iac-development", - remoteEnvAlias: "iac-dev", - envDir, - envDescription: "Development", - baseUrl, - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: true - }); - const hasRenameOp = ops.some((o) => o.kind === "update" && o.resource === "environment" && typeof o.details === "string" && o.details.includes("iac-dev -> iac-development")); - assert.equal(hasRenameOp, true); - assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/collections")), true); - assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-development/collections")), false); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("apply with pending rename calls rename endpoint and then uses new alias path for nested resources", async () => { - const envDir = await makeEnvDirWithCollections(); - const calls = []; - const baseUrl = "https://management.example"; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") { - bodyText = init.body; - } - else if (init?.body instanceof URLSearchParams) { - bodyText = init.body.toString(); - } - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { - edges: [{ node: { environmentAlias: "iac-dev" } }] - }); - } - if (method === "POST" && - u.pathname === "/v1/projects/proj/environments/iac-dev/commands/rename") { - return jsonResponse(200, { environmentAlias: "iac-development" }); - } - if (u.pathname === "/v1/projects/proj/environments/iac-development/collections") { - return jsonResponse(200, { edges: [] }); - } - if (method === "POST" && - u.pathname === "/v1/projects/proj/environments/iac-development/collections") { - return jsonResponse(201, { collectionAlias: "content" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "iac-development", - remoteEnvAlias: "iac-dev", - envDir, - envDescription: "Development", - baseUrl, - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - const renameCall = calls.find((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/commands/rename")); - assert.ok(renameCall); - assert.equal(renameCall.method, "POST"); - assert.equal(renameCall.bodyText, JSON.stringify({ newEnvironmentAlias: "iac-development" })); - assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-development/collections")), true); - assert.equal(calls.some((c) => c.url.includes("/v1/projects/proj/environments/iac-dev/collections")), false); - assert.equal(ops.some((o) => o.kind === "update" && o.resource === "environment"), true); - } - finally { - globalThis.fetch = oldFetch; - } -}); -//# sourceMappingURL=compose.apply.environment-alias-routing.test.js.map \ No newline at end of file diff --git a/test/compose.apply.environment-alias-routing.test.js.map b/test/compose.apply.environment-alias-routing.test.js.map deleted file mode 100644 index 4f1a35c..0000000 --- a/test/compose.apply.environment-alias-routing.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.apply.environment-alias-routing.test.js","sourceRoot":"","sources":["compose.apply.environment-alias-routing.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.apply.failures.test.d.ts b/test/compose.apply.failures.test.d.ts deleted file mode 100644 index e545ab2..0000000 --- a/test/compose.apply.failures.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=compose.apply.failures.test.d.ts.map \ No newline at end of file diff --git a/test/compose.apply.failures.test.d.ts.map b/test/compose.apply.failures.test.d.ts.map deleted file mode 100644 index f92ab5c..0000000 --- a/test/compose.apply.failures.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.apply.failures.test.d.ts","sourceRoot":"","sources":["compose.apply.failures.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.apply.failures.test.js b/test/compose.apply.failures.test.js deleted file mode 100644 index 530f1d6..0000000 --- a/test/compose.apply.failures.test.js +++ /dev/null @@ -1,96 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { applyEnvironment } from "../src/compose/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function makeEnvDirWithCollections() { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-fail-")); - await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections: [{ alias: "content" }] }, null, 2), "utf8"); - return dir; -} -test("environment list failure returns environment warning and short-circuits nested operations", async () => { - const envDir = await makeEnvDirWithCollections(); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - calls.push({ method, url }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(500, { error: "boom" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: true - }); - assert.equal(ops.some((o) => o.kind === "warn" && o.resource === "environment"), true); - assert.equal(calls.some((c) => c.url.includes("/environments/dev/collections")), false); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("rename failure returns environment warning with status and short-circuits nested operations", async () => { - const envDir = await makeEnvDirWithCollections(); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); - } - if (method === "POST" && - u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename") { - return jsonResponse(409, { error: "conflict" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - remoteEnvAlias: "old-dev", - envDir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - assert.equal(ops.some((o) => o.kind === "warn" && o.resource === "environment" && (o.details ?? "").includes("status 409")), true); - assert.equal(calls.some((c) => c.url.includes("/environments/dev/collections")), false); - } - finally { - globalThis.fetch = oldFetch; - } -}); -//# sourceMappingURL=compose.apply.failures.test.js.map \ No newline at end of file diff --git a/test/compose.apply.failures.test.js.map b/test/compose.apply.failures.test.js.map deleted file mode 100644 index bf74e43..0000000 --- a/test/compose.apply.failures.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.apply.failures.test.js","sourceRoot":"","sources":["compose.apply.failures.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.d.ts b/test/compose.apply.rename-inference.test.d.ts deleted file mode 100644 index 6397acd..0000000 --- a/test/compose.apply.rename-inference.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=compose.apply.rename-inference.test.d.ts.map \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.d.ts.map b/test/compose.apply.rename-inference.test.d.ts.map deleted file mode 100644 index e675548..0000000 --- a/test/compose.apply.rename-inference.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.apply.rename-inference.test.d.ts","sourceRoot":"","sources":["compose.apply.rename-inference.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.js b/test/compose.apply.rename-inference.test.js deleted file mode 100644 index 1f98cc1..0000000 --- a/test/compose.apply.rename-inference.test.js +++ /dev/null @@ -1,370 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { applyEnvironment } from "../src/compose/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function makeCollectionsEnvDir(collections) { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-")); - await fs.writeFile(path.join(dir, "collections.json"), JSON.stringify({ collections }, null, 2), "utf8"); - return dir; -} -async function makePersistedEnvDir(docs) { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-persisted-")); - await fs.mkdir(path.join(dir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(dir, "graphql", "persisted.json"), JSON.stringify({ - documents: docs.map((d) => ({ - alias: d.alias, - description: d.description ?? "", - queryFile: `graphql/persisted/${d.alias}.gql` - })) - }, null, 2), "utf8"); - for (const d of docs) { - await fs.writeFile(path.join(dir, "graphql", "persisted", `${d.alias}.gql`), d.query, "utf8"); - } - return dir; -} -test("collection rename inference skips ambiguous matches and falls back to create/delete", async () => { - const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "Same description" }]); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { - edges: [{ node: { collectionAlias: "content-old-a" } }, { node: { collectionAlias: "content-old-b" } }] - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-a" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old-a", description: "Same description" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-b" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old-b", description: "Same description" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { - return jsonResponse(201, { collectionAlias: "content-new" }); - } - if ((u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-a" || - u.pathname === "/v1/projects/proj/environments/dev/collections/content-old-b") && - method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); - assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); - assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); - const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "collection"); - assert.equal(deleteOps.length, 2); - assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("collection rename inference skips when signatures do not match and falls back to create/delete", async () => { - const envDir = await makeCollectionsEnvDir([{ alias: "content-new", description: "New description" }]); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old", description: "Old description" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { - return jsonResponse(201, { collectionAlias: "content-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); - assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); - assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new"), true); - assert.equal(ops.some((o) => o.kind === "delete" && o.resource === "collection" && o.alias === "content-old"), true); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("collection rename inference skips when desired signatures are ambiguous and falls back to create/delete", async () => { - const envDir = await makeCollectionsEnvDir([ - { alias: "content-new-a", description: "Same description" }, - { alias: "content-new-b", description: "Same description" } - ]); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old", description: "Same description" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "POST") { - return jsonResponse(201, { collectionAlias: "created" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - assert.equal(calls.some((c) => c.url.includes("/commands/rename")), false); - assert.equal(ops.some((o) => o.kind === "update" && o.resource === "collection"), false); - assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new-a"), true); - assert.equal(ops.some((o) => o.kind === "create" && o.resource === "collection" && o.alias === "content-new-b"), true); - assert.equal(ops.some((o) => o.kind === "delete" && o.resource === "collection" && o.alias === "content-old"), true); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("persisted-doc rename inference skips ambiguous remote matches and falls back to create/delete", async () => { - const envDir = await makePersistedEnvDir([ - { alias: "doc-new", description: "Shared description", query: "query { ping }" } - ]); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { - return jsonResponse(200, { - edges: [{ node: { persistedDocumentAlias: "doc-old-a" } }, { node: { persistedDocumentAlias: "doc-old-b" } }] - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-a" && - method === "GET") { - return jsonResponse(200, { - persistedDocumentAlias: "doc-old-a", - description: "Shared description", - document: "query { ping }" - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-b" && - method === "GET") { - return jsonResponse(200, { - persistedDocumentAlias: "doc-old-b", - description: "Shared description", - document: "query { ping }" - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "POST") { - return jsonResponse(201, { persistedDocumentAlias: "doc-new" }); - } - if ((u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-a" || - u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old-b") && - method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - assert.equal(calls.some((c) => c.url.includes("/graphql/persisted-documents/") && c.url.includes("/commands/rename")), false); - assert.equal(ops.some((o) => o.kind === "update" && o.resource === "persisted-doc"), false); - assert.equal(ops.some((o) => o.kind === "create" && o.resource === "persisted-doc" && o.alias === "doc-new"), true); - const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "persisted-doc"); - assert.equal(deleteOps.length, 2); - assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("webhook rename inference skips ambiguous remote matches and falls back to create/delete", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-apply-rename-inf-webhook-")); - await fs.writeFile(path.join(dir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "hook-new", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["article"], - customHeaders: { authorization: "Bearer abc" } - } - ] - }, null, 2), "utf8"); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { - edges: [{ node: { webhookAlias: "hook-old-a" } }, { node: { webhookAlias: "hook-old-b" } }] - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-a" && method === "GET") { - return jsonResponse(200, { - webhookAlias: "hook-old-a", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["article"], - customHeaders: { authorization: "Bearer abc" } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-b" && method === "GET") { - return jsonResponse(200, { - webhookAlias: "hook-old-b", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["article"], - customHeaders: { authorization: "Bearer abc" } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "POST") { - return jsonResponse(201, { webhookAlias: "hook-new" }); - } - if ((u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-a" || - u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old-b") && - method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - const ops = await applyEnvironment({ - project: "proj", - env: "dev", - envDir: dir, - envDescription: "Dev", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - assert.equal(calls.some((c) => c.url.includes("/webhooks/") && c.url.includes("/commands/rename")), false); - assert.equal(ops.some((o) => o.kind === "update" && o.resource === "webhook"), false); - assert.equal(ops.some((o) => o.kind === "create" && o.resource === "webhook" && o.alias === "hook-new"), true); - const deleteOps = ops.filter((o) => o.kind === "delete" && o.resource === "webhook"); - assert.equal(deleteOps.length, 2); - assert.equal(deleteOps.every((o) => o.details === "no unique rename match"), true); - } - finally { - globalThis.fetch = oldFetch; - } -}); -//# sourceMappingURL=compose.apply.rename-inference.test.js.map \ No newline at end of file diff --git a/test/compose.apply.rename-inference.test.js.map b/test/compose.apply.rename-inference.test.js.map deleted file mode 100644 index a0a1027..0000000 --- a/test/compose.apply.rename-inference.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.apply.rename-inference.test.js","sourceRoot":"","sources":["compose.apply.rename-inference.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.management-client.test.d.ts b/test/compose.management-client.test.d.ts deleted file mode 100644 index fe2ee23..0000000 --- a/test/compose.management-client.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=compose.management-client.test.d.ts.map \ No newline at end of file diff --git a/test/compose.management-client.test.d.ts.map b/test/compose.management-client.test.d.ts.map deleted file mode 100644 index b521250..0000000 --- a/test/compose.management-client.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.management-client.test.d.ts","sourceRoot":"","sources":["compose.management-client.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.management-client.test.js b/test/compose.management-client.test.js deleted file mode 100644 index ab5b59f..0000000 --- a/test/compose.management-client.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { ManagementClient } from "../src/compose/management-client.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -test("ManagementClient retries once on 401 and invalidates auth cache", async () => { - const oldFetch = globalThis.fetch; - let fetchCount = 0; - let invalidateCount = 0; - let getHeadersCount = 0; - globalThis.fetch = (async () => { - fetchCount += 1; - if (fetchCount === 1) { - return jsonResponse(401, { error: "expired-token" }); - } - return jsonResponse(200, { ok: true }); - }); - const client = new ManagementClient({ - baseUrl: "https://management.example", - auth: { - getHeaders: async () => { - getHeadersCount += 1; - return { Authorization: `Bearer token-${getHeadersCount}` }; - }, - invalidate: () => { - invalidateCount += 1; - } - } - }); - try { - const res = await client.get("/v1/projects/p/environments"); - assert.equal(res.status, 200); - assert.equal(res.data?.ok, true); - assert.equal(fetchCount, 2); - assert.equal(getHeadersCount, 2); - assert.equal(invalidateCount, 1); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("ManagementClient does not loop infinitely when retry also returns 401", async () => { - const oldFetch = globalThis.fetch; - let fetchCount = 0; - let invalidateCount = 0; - globalThis.fetch = (async () => { - fetchCount += 1; - return jsonResponse(401, { error: "still-unauthorized" }); - }); - const client = new ManagementClient({ - baseUrl: "https://management.example", - auth: { - getHeaders: async () => ({ Authorization: "Bearer token" }), - invalidate: () => { - invalidateCount += 1; - } - } - }); - try { - const res = await client.get("/v1/projects/p/environments"); - assert.equal(res.status, 401); - assert.equal(res.data?.error, "still-unauthorized"); - assert.equal(fetchCount, 2); - assert.equal(invalidateCount, 1); - } - finally { - globalThis.fetch = oldFetch; - } -}); -//# sourceMappingURL=compose.management-client.test.js.map \ No newline at end of file diff --git a/test/compose.management-client.test.js.map b/test/compose.management-client.test.js.map deleted file mode 100644 index 97cc7a5..0000000 --- a/test/compose.management-client.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.management-client.test.js","sourceRoot":"","sources":["compose.management-client.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/compose.oauth-base.test.d.ts b/test/compose.oauth-base.test.d.ts deleted file mode 100644 index 909cd42..0000000 --- a/test/compose.oauth-base.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=compose.oauth-base.test.d.ts.map \ No newline at end of file diff --git a/test/compose.oauth-base.test.d.ts.map b/test/compose.oauth-base.test.d.ts.map deleted file mode 100644 index cf88029..0000000 --- a/test/compose.oauth-base.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.oauth-base.test.d.ts","sourceRoot":"","sources":["compose.oauth-base.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.oauth-base.test.js b/test/compose.oauth-base.test.js deleted file mode 100644 index 271718f..0000000 --- a/test/compose.oauth-base.test.js +++ /dev/null @@ -1,93 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { oauthClientCredentialsAuth } from "../src/compose/auth/providers/oauth-base.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -test("oauthClientCredentialsAuth caches token between calls until invalidated", async () => { - const oldFetch = globalThis.fetch; - let tokenCallCount = 0; - globalThis.fetch = (async () => { - tokenCallCount += 1; - return jsonResponse(200, { - access_token: `token-${tokenCallCount}`, - expires_in: 3600 - }); - }); - try { - const auth = oauthClientCredentialsAuth({ - tokenUrl: "https://auth.example/token", - clientId: "client", - clientSecret: "secret" - }); - const h1 = await auth.getHeaders(); - const h2 = await auth.getHeaders(); - assert.equal(h1.Authorization, "Bearer token-1"); - assert.equal(h2.Authorization, "Bearer token-1"); - assert.equal(tokenCallCount, 1); - auth.invalidate?.(); - const h3 = await auth.getHeaders(); - assert.equal(h3.Authorization, "Bearer token-2"); - assert.equal(tokenCallCount, 2); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("oauthClientCredentialsAuth deduplicates concurrent token requests", async () => { - const oldFetch = globalThis.fetch; - let tokenCallCount = 0; - let release = null; - const gate = new Promise((resolve) => { - release = resolve; - }); - globalThis.fetch = (async () => { - tokenCallCount += 1; - await gate; - return jsonResponse(200, { - access_token: "shared-token", - expires_in: 3600 - }); - }); - try { - const auth = oauthClientCredentialsAuth({ - tokenUrl: "https://auth.example/token", - clientId: "client", - clientSecret: "secret" - }); - const p1 = auth.getHeaders(); - const p2 = auth.getHeaders(); - const p3 = auth.getHeaders(); - release?.(); - const [h1, h2, h3] = await Promise.all([p1, p2, p3]); - assert.equal(h1.Authorization, "Bearer shared-token"); - assert.equal(h2.Authorization, "Bearer shared-token"); - assert.equal(h3.Authorization, "Bearer shared-token"); - assert.equal(tokenCallCount, 1); - } - finally { - globalThis.fetch = oldFetch; - } -}); -test("oauthClientCredentialsAuth surfaces token endpoint errors", async () => { - const oldFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("unauthorized", { - status: 401, - headers: { "content-type": "text/plain" } - })); - try { - const auth = oauthClientCredentialsAuth({ - tokenUrl: "https://auth.example/token", - clientId: "client", - clientSecret: "secret" - }); - await assert.rejects(() => auth.getHeaders(), /OAuth token request failed \(401\): unauthorized/); - } - finally { - globalThis.fetch = oldFetch; - } -}); -//# sourceMappingURL=compose.oauth-base.test.js.map \ No newline at end of file diff --git a/test/compose.oauth-base.test.js.map b/test/compose.oauth-base.test.js.map deleted file mode 100644 index b0ce046..0000000 --- a/test/compose.oauth-base.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.oauth-base.test.js","sourceRoot":"","sources":["compose.oauth-base.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,6CAA6C,CAAC;AAEzF,SAAS,YAAY,CAAC,MAAc,EAAE,IAAa;IACjD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED,IAAI,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;IACzF,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,IAAI,cAAc,GAAG,CAAC,CAAC;IAEvB,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE;QAC7B,cAAc,IAAI,CAAC,CAAC;QACpB,OAAO,YAAY,CAAC,GAAG,EAAE;YACvB,YAAY,EAAE,SAAS,cAAc,EAAE;YACvC,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC,CAAiB,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,0BAA0B,CAAC;YACtC,QAAQ,EAAE,4BAA4B;YACtC,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,CAAC;QAEH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QACnC,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAEnC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;QAEhC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;QACpB,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;IACnF,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,OAAO,GAAwB,IAAI,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACzC,OAAO,GAAG,OAAO,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE;QAC7B,cAAc,IAAI,CAAC,CAAC;QACpB,MAAM,IAAI,CAAC;QACX,OAAO,YAAY,CAAC,GAAG,EAAE;YACvB,YAAY,EAAE,cAAc;YAC5B,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC,CAAiB,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,0BAA0B,CAAC;YACtC,QAAQ,EAAE,4BAA4B;YACtC,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,CAAC;QAEH,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAE7B,OAAO,EAAE,EAAE,CAAC;QACZ,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAErD,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;IAC3E,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;IAClC,UAAU,CAAC,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE,CAC7B,IAAI,QAAQ,CAAC,cAAc,EAAE;QAC3B,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;KAC1C,CAAC,CAAiB,CAAC;IAEtB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,0BAA0B,CAAC;YACtC,QAAQ,EAAE,4BAA4B;YACtC,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,QAAQ;SACvB,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,EACvB,kDAAkD,CACnD,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC9B,CAAC;AACH,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/test/compose.state.test.d.ts b/test/compose.state.test.d.ts deleted file mode 100644 index 164067f..0000000 --- a/test/compose.state.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=compose.state.test.d.ts.map \ No newline at end of file diff --git a/test/compose.state.test.d.ts.map b/test/compose.state.test.d.ts.map deleted file mode 100644 index 4100aa1..0000000 --- a/test/compose.state.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.state.test.d.ts","sourceRoot":"","sources":["compose.state.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/compose.state.test.js b/test/compose.state.test.js deleted file mode 100644 index 867543a..0000000 --- a/test/compose.state.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { COMPOSE_STATE_FILE, ensureStateForAliases, findEnvironmentByAlias, markEnvironmentRemoteAlias, readComposeState, renameEnvironmentAlias, writeComposeState } from "../src/compose/state.js"; -test("ensureStateForAliases creates and extends state while preserving existing env ids", () => { - const first = ensureStateForAliases(null, "proj-a", ["dev"]).state; - assert.equal(first.project, "proj-a"); - assert.equal(first.environments.length, 1); - assert.equal(first.environments[0]?.alias, "dev"); - const originalId = first.environments[0]?.id; - assert.ok(originalId); - const second = ensureStateForAliases(first, "proj-a", ["dev", "stage"]).state; - assert.equal(second.environments.length, 2); - assert.equal(second.environments.find((e) => e.alias === "dev")?.id, originalId); - assert.ok(second.environments.find((e) => e.alias === "stage")?.id); -}); -test("renameEnvironmentAlias updates alias and keeps remoteAlias mapping stable by env id", () => { - const state = ensureStateForAliases(null, "proj-b", ["iac-dev"]).state; - const env = findEnvironmentByAlias(state, "iac-dev"); - assert.ok(env); - const marked = markEnvironmentRemoteAlias(state, env.id, "iac-dev"); - assert.equal(marked, true); - const renamed = renameEnvironmentAlias(state, "iac-dev", "iac-development"); - assert.equal(renamed, true); - const after = findEnvironmentByAlias(state, "iac-development"); - assert.ok(after); - assert.equal(after.id, env.id); - assert.equal(after.remoteAlias, "iac-dev"); -}); -test("readComposeState returns null when file is missing and throws on invalid JSON", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-state-test-")); - const missing = await readComposeState(tmpRoot); - assert.equal(missing, null); - const statePath = path.join(tmpRoot, COMPOSE_STATE_FILE); - await fs.writeFile(statePath, "{invalid json", "utf8"); - await assert.rejects(() => readComposeState(tmpRoot)); -}); -test("writeComposeState and readComposeState roundtrip", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compose-state-roundtrip-")); - const initial = ensureStateForAliases(null, "proj-c", ["dev", "prod"]).state; - await writeComposeState(tmpRoot, initial); - const read = await readComposeState(tmpRoot); - assert.ok(read); - assert.equal(read.project, "proj-c"); - assert.deepEqual(read.environments.map((e) => e.alias).sort(), ["dev", "prod"]); -}); -//# sourceMappingURL=compose.state.test.js.map \ No newline at end of file diff --git a/test/compose.state.test.js.map b/test/compose.state.test.js.map deleted file mode 100644 index dfbe504..0000000 --- a/test/compose.state.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compose.state.test.js","sourceRoot":"","sources":["compose.state.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.d.ts b/test/contracts.apply-contract-map.test.d.ts deleted file mode 100644 index f9ee503..0000000 --- a/test/contracts.apply-contract-map.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=contracts.apply-contract-map.test.d.ts.map \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.d.ts.map b/test/contracts.apply-contract-map.test.d.ts.map deleted file mode 100644 index 549137e..0000000 --- a/test/contracts.apply-contract-map.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"contracts.apply-contract-map.test.d.ts","sourceRoot":"","sources":["contracts.apply-contract-map.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.js b/test/contracts.apply-contract-map.test.js deleted file mode 100644 index b5ca0f1..0000000 --- a/test/contracts.apply-contract-map.test.js +++ /dev/null @@ -1,1089 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import path from "node:path"; -import os from "node:os"; -import { applyEnvironment } from "../src/compose/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function readContractMap() { - const filePath = path.resolve(process.cwd(), "docs/contracts/apply-contract.json"); - const text = await fs.readFile(filePath, "utf8"); - return JSON.parse(text); -} -function resolvePath(template, params) { - let out = template; - for (const [k, v] of Object.entries(params)) { - out = out.replaceAll(`{${k}}`, v); - } - return out; -} -function assertContractCall(contracts, operation, calls, params) { - const contract = contracts.operations[operation]; - assert.ok(contract, `Missing contract entry for ${operation}`); - const path = resolvePath(contract.pathTemplate, params); - const call = calls.find((c) => c.method === contract.method && c.pathname === path); - assert.ok(call, `Did not find contract call for ${operation}: ${contract.method} ${path}`); - if (contract.requiredBodyKeys.length > 0) { - assert.ok(call.body && typeof call.body === "object", `${operation} body was not an object`); - const body = call.body; - for (const key of contract.requiredBodyKeys) { - assert.ok(key in body, `${operation} body missing required key: ${key}`); - } - } -} -async function makeFullEnvDir() { - const envDir = await fs.mkdtemp(path.join(os.tmpdir(), "compose-contract-map-full-")); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "Main content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "type-schemas", "software.schema.json"), JSON.stringify({ - alias: "software", - description: "Software", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { name: { type: "string" } } - } - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ - alias: "article", - description: "Article", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ - functions: [ - { - alias: "map-content", - description: "Maps content", - scriptFile: "functions/ingestion/map-content.js" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "export default (x) => x;", "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ - documents: [ - { alias: "doc-1", description: "Query", queryFile: "graphql/persisted/doc-1.gql" } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-1.gql"), "query { ping }", "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "send-on-create", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["software"], - customHeaders: { "x-api-key": "abc123" } - } - ] - }, null, 2), "utf8"); - return envDir; -} -async function mkEnvDir(prefix) { - return fs.mkdtemp(path.join(os.tmpdir(), prefix)); -} -test("apply emitted calls match contract map for currently implemented write operations", async () => { - const contracts = await readContractMap(); - const envDir = await makeFullEnvDir(); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/software") { - return jsonResponse(404, { error: "not found" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article") { - return jsonResponse(200, { - typeSchemaAlias: "article", - description: "Article", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { oldField: { type: "string" } } - } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "POST") { - return jsonResponse(201, { typeSchemaAlias: "software" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { persistedDocumentAlias: "doc-1" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { webhookAlias: "send-on-create" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - const params = { - projectAlias: "proj", - environmentAlias: "dev", - typeSchemaAlias: "article" - }; - assertContractCall(contracts, "collectionCreate", calls, params); - assertContractCall(contracts, "typeSchemaCreate", calls, params); - assertContractCall(contracts, "typeSchemaUpdateSchema", calls, params); - assertContractCall(contracts, "ingestionFunctionCreate", calls, params); - assertContractCall(contracts, "persistedDocumentCreate", calls, params); - assertContractCall(contracts, "webhookCreate", calls, params); -}); -test("environment create and rename calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-env-"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { environmentAlias: "dev" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "environmentCreate", calls, { - projectAlias: "proj" - }); - const renameCalls = []; - const oldFetch2 = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - renameCalls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" && method === "POST") { - return jsonResponse(200, { environmentAlias: "dev" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - remoteEnvAlias: "old-dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch2; - } - assertContractCall(contracts, "environmentRename", renameCalls, { - projectAlias: "proj", - environmentAlias: "old-dev" - }); -}); -test("ingestion update calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-ingestion-update-"); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ - functions: [ - { - alias: "fn-a", - description: "New desc", - scriptFile: "functions/ingestion/fn-a.js" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "fn-a.js"), "export default (x) => x;", "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a") { - return jsonResponse(200, { ingestionFunctionAlias: "fn-a", description: "Old desc", script: "export default () => null;" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a/commands/update-script" && method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/fn-a/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "ingestionFunctionUpdateScript", calls, { - projectAlias: "proj", - environmentAlias: "dev", - ingestionFunctionAlias: "fn-a" - }); - assertContractCall(contracts, "ingestionFunctionUpdateDescription", calls, { - projectAlias: "proj", - environmentAlias: "dev", - ingestionFunctionAlias: "fn-a" - }); -}); -test("collection update-description calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-collection-update-desc-"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "New description" }] }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") - body = JSON.parse(init.body); - if (init?.body instanceof URLSearchParams) - body = init.body.toString(); - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content", description: "Old description" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "collectionUpdateDescription", calls, { - projectAlias: "proj", - environmentAlias: "dev", - collectionAlias: "content" - }); -}); -test("type-schema update-description calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-type-schema-update-desc-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ - alias: "article", - description: "New type-schema description", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") - body = JSON.parse(init.body); - if (init?.body instanceof URLSearchParams) - body = init.body.toString(); - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { - return jsonResponse(200, { - typeSchemaAlias: "article", - description: "Old type-schema description", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "typeSchemaUpdateDescription", calls, { - projectAlias: "proj", - environmentAlias: "dev", - typeSchemaAlias: "article" - }); -}); -test("ingestion delete calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-ingestion-delete-"); - await fs.mkdir(path.join(envDir, "functions"), { recursive: true }); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "legacy-ingest" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "ingestionFunctionDelete", calls, { - projectAlias: "proj", - environmentAlias: "dev", - ingestionFunctionAlias: "legacy-ingest" - }); -}); -test("persisted-document update calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-persisted-update-"); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ - documents: [ - { - alias: "doc-a", - description: "New persisted doc description", - queryFile: "graphql/persisted/doc-a.gql" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-a.gql"), "query { updated }", "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a") { - return jsonResponse(200, { - persistedDocumentAlias: "doc-a", - description: "Old persisted doc description", - document: "query { old }" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a/commands/update-document" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "persistedDocumentUpdateDocument", calls, { - projectAlias: "proj", - environmentAlias: "dev", - persistedDocumentAlias: "doc-a" - }); - assertContractCall(contracts, "persistedDocumentUpdateDescription", calls, { - projectAlias: "proj", - environmentAlias: "dev", - persistedDocumentAlias: "doc-a" - }); -}); -test("webhook update calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-webhook-update-"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "send-on-create", - description: "New webhook description", - url: "https://example.com/new", - eventTypes: ["content.deleted"], - collectionAliases: ["articles"], - typeSchemaAliases: ["article"], - customHeaders: { authorization: "Bearer abc" } - } - ] - }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { - return jsonResponse(200, { - webhookAlias: "send-on-create", - description: "Old webhook description", - url: "https://example.com/old", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["software"], - customHeaders: { authorization: "Bearer old" } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - const params = { - projectAlias: "proj", - environmentAlias: "dev", - webhookAlias: "send-on-create" - }; - assertContractCall(contracts, "webhookUpdateDescription", calls, params); - assertContractCall(contracts, "webhookUpdateUrl", calls, params); - assertContractCall(contracts, "webhookUpdateEventTypes", calls, params); - assertContractCall(contracts, "webhookUpdateCollections", calls, params); - assertContractCall(contracts, "webhookUpdateTypeSchemas", calls, params); - assertContractCall(contracts, "webhookUpdateHeaders", calls, params); -}); -test("persisted-document delete calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-persisted-delete-"); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-a" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "persistedDocumentDelete", calls, { - projectAlias: "proj", - environmentAlias: "dev", - persistedDocumentAlias: "doc-a" - }); -}); -test("webhook delete calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-webhook-delete-"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "webhookDelete", calls, { - projectAlias: "proj", - environmentAlias: "dev", - webhookAlias: "send-on-create" - }); -}); -test("collection delete calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-collection-delete-"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "collectionDelete", calls, { - projectAlias: "proj", - environmentAlias: "dev", - collectionAlias: "content" - }); -}); -test("type-schema delete calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-type-schema-delete-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "legacy-schema" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/legacy-schema" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - assertContractCall(contracts, "typeSchemaDelete", calls, { - projectAlias: "proj", - environmentAlias: "dev", - typeSchemaAlias: "legacy-schema" - }); -}); -test("non-environment rename calls match contract map", async () => { - const contracts = await readContractMap(); - const envDir = await mkEnvDir("compose-contract-map-rename-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content-new", description: "Main content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "type-schemas", "article-new.schema.json"), JSON.stringify({ - alias: "article-new", - description: "Article schema", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ - functions: [ - { - alias: "map-content-new", - description: "Maps content", - scriptFile: "functions/ingestion/map-content-new.js" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content-new.js"), "export default (x) => x;", "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ - documents: [ - { - alias: "doc-new", - description: "Main query", - queryFile: "graphql/persisted/doc-new.gql" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-new.gql"), "query { ping }", "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "hook-new", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content-new"], - typeSchemaAliases: ["article-new"], - customHeaders: { authorization: "Bearer abc" } - } - ] - }, null, 2), "utf8"); - const oldFetch = globalThis.fetch; - const calls = []; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - let body; - if (typeof init?.body === "string") { - body = JSON.parse(init.body); - } - else if (init?.body instanceof URLSearchParams) { - body = init.body.toString(); - } - calls.push({ method, pathname: u.pathname, body }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { collectionAlias: "content-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old" && method === "GET") { - return jsonResponse(200, { - typeSchemaAlias: "article-old", - description: "Article schema", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { typeSchemaAlias: "article-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old" && - method === "GET") { - return jsonResponse(200, { - ingestionFunctionAlias: "map-content-old", - description: "Maps content", - script: "export default (x) => x;" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { ingestionFunctionAlias: "map-content-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old" && - method === "GET") { - return jsonResponse(200, { - persistedDocumentAlias: "doc-old", - description: "Main query", - document: "query { ping }" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { persistedDocumentAlias: "doc-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old" && method === "GET") { - return jsonResponse(200, { - webhookAlias: "hook-old", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content-new"], - typeSchemaAliases: ["article-new"], - customHeaders: { authorization: "Bearer abc" } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename" && method === "POST") { - return jsonResponse(200, { webhookAlias: "hook-new" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - globalThis.fetch = oldFetch; - } - const params = { projectAlias: "proj", environmentAlias: "dev" }; - assertContractCall(contracts, "collectionRename", calls, { - ...params, - collectionAlias: "content-old" - }); - assertContractCall(contracts, "typeSchemaRename", calls, { - ...params, - typeSchemaAlias: "article-old" - }); - assertContractCall(contracts, "ingestionFunctionRename", calls, { - ...params, - ingestionFunctionAlias: "map-content-old" - }); - assertContractCall(contracts, "persistedDocumentRename", calls, { - ...params, - persistedDocumentAlias: "doc-old" - }); - assertContractCall(contracts, "webhookRename", calls, { - ...params, - webhookAlias: "hook-old" - }); -}); -//# sourceMappingURL=contracts.apply-contract-map.test.js.map \ No newline at end of file diff --git a/test/contracts.apply-contract-map.test.js.map b/test/contracts.apply-contract-map.test.js.map deleted file mode 100644 index 82f2ae6..0000000 --- a/test/contracts.apply-contract-map.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"contracts.apply-contract-map.test.js","sourceRoot":"","sources":["contracts.apply-contract-map.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.d.ts b/test/contracts.apply-openapi-shape.test.d.ts deleted file mode 100644 index 5bd4899..0000000 --- a/test/contracts.apply-openapi-shape.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=contracts.apply-openapi-shape.test.d.ts.map \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.d.ts.map b/test/contracts.apply-openapi-shape.test.d.ts.map deleted file mode 100644 index 5bfcbe6..0000000 --- a/test/contracts.apply-openapi-shape.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"contracts.apply-openapi-shape.test.d.ts","sourceRoot":"","sources":["contracts.apply-openapi-shape.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.js b/test/contracts.apply-openapi-shape.test.js deleted file mode 100644 index 0c9ed0b..0000000 --- a/test/contracts.apply-openapi-shape.test.js +++ /dev/null @@ -1,1071 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { applyEnvironment } from "../src/compose/apply.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function mkEnvDir(prefix) { - return fs.mkdtemp(path.join(os.tmpdir(), prefix)); -} -function installFetchMock(handler) { - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - let bodyText; - if (typeof init?.body === "string") - bodyText = init.body; - if (init?.body instanceof URLSearchParams) - bodyText = init.body.toString(); - calls.push({ method, url, bodyText }); - const u = new URL(url); - if (u.pathname === "/v1/auth/token") { - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - } - return handler(u, method, bodyText); - }); - return { - calls, - restore: () => { - globalThis.fetch = oldFetch; - } - }; -} -test("environment create uses expected endpoint and payload shape", async () => { - const envDir = await mkEnvDir("compose-contract-create-env-"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { environmentAlias: "dev" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.environmentAlias, "dev"); - assert.equal(body.description, "Development"); -}); -test("collection create uses expected endpoint and payload shape", async () => { - const envDir = await mkEnvDir("compose-contract-collection-create-"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "Main content" }] }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { collectionAlias: "content" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/collections")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.collectionAlias, "content"); - assert.equal(body.description, "Main content"); -}); -test("collection update uses expected update-description command payload", async () => { - const envDir = await mkEnvDir("compose-contract-collection-update-desc-"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content", description: "New description" }] }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content", description: "Old description" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const updateCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/collections/content/commands/update-description")); - assert.ok(updateCall); - const body = JSON.parse(updateCall.bodyText ?? "{}"); - assert.equal(body.newDescription, "New description"); -}); -test("collection delete uses expected endpoint", async () => { - const envDir = await mkEnvDir("compose-contract-collection-delete-"); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [] }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections") { - if (method === "GET") - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const deleteCall = calls.find((c) => c.method === "DELETE" && c.url.includes("/v1/projects/proj/environments/dev/collections/content")); - assert.ok(deleteCall); -}); -test("environment rename uses expected endpoint and payload key", async () => { - const envDir = await mkEnvDir("compose-contract-rename-env-"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "old-dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/old-dev/commands/rename" && method === "POST") { - return jsonResponse(200, { environmentAlias: "dev" }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - remoteEnvAlias: "old-dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const renameCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/old-dev/commands/rename")); - assert.ok(renameCall); - const body = JSON.parse(renameCall.bodyText ?? "{}"); - assert.deepEqual(body, { newEnvironmentAlias: "dev" }); -}); -test("persisted-document create uses expected payload field 'document'", async () => { - const envDir = await mkEnvDir("compose-contract-persisted-doc-"); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [{ alias: "doc-1", description: "desc", queryFile: "graphql/persisted/doc-1.gql" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-1.gql"), "query { ping }", "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { persistedDocumentAlias: "doc-1" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.persistedDocumentAlias, "doc-1"); - assert.equal(body.description, "desc"); - assert.equal(body.document, "query { ping }"); - assert.equal("query" in body, false); -}); -test("persisted-document create sends string description when local description is omitted", async () => { - const envDir = await mkEnvDir("compose-contract-persisted-doc-missing-desc-"); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [{ alias: "doc-2", queryFile: "graphql/persisted/doc-2.gql" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-2.gql"), "query { pong }", "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { persistedDocumentAlias: "doc-2" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.persistedDocumentAlias, "doc-2"); - assert.equal(body.description, ""); - assert.equal(body.document, "query { pong }"); -}); -test("persisted-document update uses expected update-document and update-description commands", async () => { - const envDir = await mkEnvDir("compose-contract-persisted-doc-update-"); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ - documents: [ - { - alias: "doc-3", - description: "New description", - queryFile: "graphql/persisted/doc-3.gql" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-3.gql"), "query { newDoc }", "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-3" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3") { - return jsonResponse(200, { - persistedDocumentAlias: "doc-3", - description: "Old description", - document: "query { oldDoc }" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-document" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const updateDocumentCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-document")); - assert.ok(updateDocumentCall); - const updateDocumentBody = JSON.parse(updateDocumentCall.bodyText ?? "{}"); - assert.equal(updateDocumentBody.newDocument, "query { newDoc }"); - const updateDescriptionCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-3/commands/update-description")); - assert.ok(updateDescriptionCall); - const updateDescriptionBody = JSON.parse(updateDescriptionCall.bodyText ?? "{}"); - assert.equal(updateDescriptionBody.newDescription, "New description"); -}); -test("persisted-document delete uses expected endpoint", async () => { - const envDir = await mkEnvDir("compose-contract-persisted-doc-delete-"); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ documents: [] }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-4" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-4" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const deleteCall = calls.find((c) => c.method === "DELETE" && - c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-4")); - assert.ok(deleteCall); -}); -test("ingestion-function create uses expected endpoint and payload shape", async () => { - const envDir = await mkEnvDir("compose-contract-ingestion-create-"); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ - functions: [ - { - alias: "map-content", - description: "Maps content", - scriptFile: "functions/ingestion/map-content.js" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "export default function(x){ return x; }", "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { ingestionFunctionAlias: "map-content" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.ingestionFunctionAlias, "map-content"); - assert.equal(body.description, "Maps content"); - assert.equal(typeof body.script, "string"); -}); -test("ingestion-function update uses expected update-script and update-description commands", async () => { - const envDir = await mkEnvDir("compose-contract-ingestion-update-"); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ - functions: [ - { - alias: "map-content", - description: "New description", - scriptFile: "functions/ingestion/map-content.js" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content.js"), "export default function(input){ return input; }", "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content") { - return jsonResponse(200, { - ingestionFunctionAlias: "map-content", - description: "Old description", - script: "export default () => null;" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-script" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const updateScriptCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-script")); - assert.ok(updateScriptCall); - const updateScriptBody = JSON.parse(updateScriptCall.bodyText ?? "{}"); - assert.equal(typeof updateScriptBody.script, "string"); - const updateDescriptionCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content/commands/update-description")); - assert.ok(updateDescriptionCall); - const updateDescriptionBody = JSON.parse(updateDescriptionCall.bodyText ?? "{}"); - assert.equal(updateDescriptionBody.newDescription, "New description"); -}); -test("ingestion-function delete uses expected endpoint", async () => { - const envDir = await mkEnvDir("compose-contract-ingestion-delete-"); - await fs.mkdir(path.join(envDir, "functions"), { recursive: true }); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ functions: [] }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "legacy-ingest" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest" && - method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const deleteCall = calls.find((c) => c.method === "DELETE" && - c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/legacy-ingest")); - assert.ok(deleteCall); -}); -test("webhook create uses expected endpoint and payload shape", async () => { - const envDir = await mkEnvDir("compose-contract-webhook-create-"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "send-on-create", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["article"], - customHeaders: { "x-api-key": "abc123" } - } - ] - }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks") { - if (method === "GET") - return jsonResponse(200, { edges: [] }); - if (method === "POST") - return jsonResponse(201, { webhookAlias: "send-on-create" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/webhooks")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.webhookAlias, "send-on-create"); - assert.equal(body.url, "https://example.com/hook"); - assert.deepEqual(body.eventTypes, ["content.ingested"]); - assert.deepEqual(body.collectionAliases, ["content"]); - assert.deepEqual(body.typeSchemaAliases, ["article"]); - assert.deepEqual(body.customHeaders, { "x-api-key": "abc123" }); -}); -test("webhook update uses expected command endpoints and payload shapes", async () => { - const envDir = await mkEnvDir("compose-contract-webhook-update-"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "send-on-create", - description: "New webhook description", - url: "https://example.com/new", - eventTypes: ["content.deleted"], - collectionAliases: ["articles"], - typeSchemaAliases: ["article"], - customHeaders: { authorization: "Bearer abc" } - } - ] - }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "GET") { - return jsonResponse(200, { - webhookAlias: "send-on-create", - description: "Old webhook description", - url: "https://example.com/old", - eventTypes: ["content.ingested"], - collectionAliases: ["content"], - typeSchemaAliases: ["software"], - customHeaders: { authorization: "Bearer old" } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const updateDescriptionCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-description")); - assert.ok(updateDescriptionCall); - assert.deepEqual(JSON.parse(updateDescriptionCall.bodyText ?? "{}"), { newDescription: "New webhook description" }); - const updateUrlCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-url")); - assert.ok(updateUrlCall); - assert.deepEqual(JSON.parse(updateUrlCall.bodyText ?? "{}"), { url: "https://example.com/new" }); - const updateEventTypesCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-event-types")); - assert.ok(updateEventTypesCall); - assert.deepEqual(JSON.parse(updateEventTypesCall.bodyText ?? "{}"), { eventTypes: ["content.deleted"] }); - const updateCollectionsCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-collections")); - assert.ok(updateCollectionsCall); - assert.deepEqual(JSON.parse(updateCollectionsCall.bodyText ?? "{}"), { collectionAliases: ["articles"] }); - const updateTypeSchemasCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-type-schemas")); - assert.ok(updateTypeSchemasCall); - assert.deepEqual(JSON.parse(updateTypeSchemasCall.bodyText ?? "{}"), { typeSchemaAliases: ["article"] }); - const updateHeadersCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create/commands/update-headers")); - assert.ok(updateHeadersCall); - assert.deepEqual(JSON.parse(updateHeadersCall.bodyText ?? "{}"), { - newHeaders: { authorization: "Bearer abc" } - }); -}); -test("webhook delete uses expected endpoint", async () => { - const envDir = await mkEnvDir("compose-contract-webhook-delete-"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ webhooks: [] }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "send-on-create" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/send-on-create" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const deleteCall = calls.find((c) => c.method === "DELETE" && c.url.includes("/v1/projects/proj/environments/dev/webhooks/send-on-create")); - assert.ok(deleteCall); -}); -test("type-schema update uses raw schema body at update-schema command endpoint", async () => { - const envDir = await mkEnvDir("compose-contract-type-schema-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - const desiredSchema = { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { name: { type: "string" } } - }; - await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ - alias: "article", - description: "Article", - schema: desiredSchema - }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method, bodyText) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { - return jsonResponse(200, { - typeSchemaAlias: "article", - description: "Article", - schema: { ...desiredSchema, properties: { title: { type: "string" } } } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema" && - method === "PUT") { - const parsed = JSON.parse(bodyText ?? "{}"); - return jsonResponse(200, parsed); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const updateCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article/commands/update-schema")); - assert.ok(updateCall); - const body = JSON.parse(updateCall.bodyText ?? "{}"); - assert.deepEqual(body, desiredSchema); - assert.equal("schema" in body, false); -}); -test("type-schema update uses expected update-description command payload", async () => { - const envDir = await mkEnvDir("compose-contract-type-schema-update-desc-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - const desiredSchema = { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { name: { type: "string" } } - }; - await fs.writeFile(path.join(envDir, "type-schemas", "article.schema.json"), JSON.stringify({ - alias: "article", - description: "New description", - schema: desiredSchema - }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article" && method === "GET") { - return jsonResponse(200, { - typeSchemaAlias: "article", - description: "Old description", - schema: desiredSchema - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description" && - method === "PUT") { - return jsonResponse(200, { ok: true }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const updateCall = calls.find((c) => c.method === "PUT" && - c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article/commands/update-description")); - assert.ok(updateCall); - const body = JSON.parse(updateCall.bodyText ?? "{}"); - assert.equal(body.newDescription, "New description"); -}); -test("type-schema create uses expected endpoint and payload shape", async () => { - const envDir = await mkEnvDir("compose-contract-type-schema-create-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - const desiredSchema = { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { name: { type: "string" } } - }; - await fs.writeFile(path.join(envDir, "type-schemas", "software.schema.json"), JSON.stringify({ - alias: "software", - description: "Software schema", - schema: desiredSchema - }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/software" && method === "GET") { - return jsonResponse(404, { error: "not found" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "POST") { - return jsonResponse(201, { typeSchemaAlias: "software" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const createCall = calls.find((c) => c.method === "POST" && c.url.includes("/v1/projects/proj/environments/dev/type-schemas")); - assert.ok(createCall); - const body = JSON.parse(createCall.bodyText ?? "{}"); - assert.equal(body.typeSchemaAlias, "software"); - assert.equal(body.description, "Software schema"); - assert.deepEqual(body.schema, desiredSchema); -}); -test("type-schema delete uses expected endpoint", async () => { - const envDir = await mkEnvDir("compose-contract-type-schema-delete-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "legacy-schema" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/legacy-schema" && method === "DELETE") { - return new Response(null, { status: 204 }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const deleteCall = calls.find((c) => c.method === "DELETE" && - c.url.includes("/v1/projects/proj/environments/dev/type-schemas/legacy-schema")); - assert.ok(deleteCall); -}); -test("non-environment rename commands use expected endpoints and payload keys", async () => { - const envDir = await mkEnvDir("compose-contract-entity-rename-"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - await fs.writeFile(path.join(envDir, "collections.json"), JSON.stringify({ collections: [{ alias: "content-new", description: "Main content" }] }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "type-schemas", "article-new.schema.json"), JSON.stringify({ - alias: "article-new", - description: "Article schema", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion.json"), JSON.stringify({ - functions: [ - { - alias: "map-content-new", - description: "Maps content", - scriptFile: "functions/ingestion/map-content-new.js" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "functions", "ingestion", "map-content-new.js"), "export default (x) => x;", "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted.json"), JSON.stringify({ - documents: [ - { - alias: "doc-new", - description: "Main query", - queryFile: "graphql/persisted/doc-new.gql" - } - ] - }, null, 2), "utf8"); - await fs.writeFile(path.join(envDir, "graphql", "persisted", "doc-new.gql"), "query { ping }", "utf8"); - await fs.writeFile(path.join(envDir, "webhooks.json"), JSON.stringify({ - webhooks: [ - { - alias: "hook-new", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content-new"], - typeSchemaAliases: ["article-new"], - customHeaders: { authorization: "Bearer abc" } - } - ] - }, null, 2), "utf8"); - const { calls, restore } = installFetchMock((u, method) => { - if (u.pathname === "/v1/projects/proj/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { collectionAlias: "content-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old" && method === "GET") { - return jsonResponse(200, { collectionAlias: "content-old", description: "Main content" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/collections/content-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { collectionAlias: "content-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old" && method === "GET") { - return jsonResponse(200, { - typeSchemaAlias: "article-old", - description: "Article schema", - schema: { - $schema: "https://umbracocompose.com/v1/schema", - allOf: [{ $ref: "https://umbracocompose.com/v1/node" }], - properties: { title: { type: "string" } } - } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { typeSchemaAlias: "article-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "map-content-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old" && - method === "GET") { - return jsonResponse(200, { - ingestionFunctionAlias: "map-content-old", - description: "Maps content", - script: "export default (x) => x;" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { ingestionFunctionAlias: "map-content-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old" && - method === "GET") { - return jsonResponse(200, { - persistedDocumentAlias: "doc-old", - description: "Main query", - document: "query { ping }" - }); - } - if (u.pathname === - "/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename" && - method === "POST") { - return jsonResponse(200, { persistedDocumentAlias: "doc-new" }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks" && method === "GET") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-old" } }] }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old" && method === "GET") { - return jsonResponse(200, { - webhookAlias: "hook-old", - description: "Notify hook", - url: "https://example.com/hook", - eventTypes: ["content.ingested"], - collectionAliases: ["content-new"], - typeSchemaAliases: ["article-new"], - customHeaders: { authorization: "Bearer abc" } - }); - } - if (u.pathname === "/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename" && method === "POST") { - return jsonResponse(200, { webhookAlias: "hook-new" }); - } - return jsonResponse(200, { edges: [] }); - }); - try { - await applyEnvironment({ - project: "proj", - env: "dev", - envDir, - envDescription: "Development", - baseUrl: "https://management.example", - oauth: { clientId: "client", clientSecret: "secret" }, - planOnly: false - }); - } - finally { - restore(); - } - const collectionRenameCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/collections/content-old/commands/rename")); - assert.ok(collectionRenameCall); - assert.deepEqual(JSON.parse(collectionRenameCall.bodyText ?? "{}"), { newCollectionAlias: "content-new" }); - const typeSchemaRenameCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/type-schemas/article-old/commands/rename")); - assert.ok(typeSchemaRenameCall); - assert.deepEqual(JSON.parse(typeSchemaRenameCall.bodyText ?? "{}"), { newTypeSchemaAlias: "article-new" }); - const ingestionRenameCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/functions/ingestion/map-content-old/commands/rename")); - assert.ok(ingestionRenameCall); - assert.deepEqual(JSON.parse(ingestionRenameCall.bodyText ?? "{}"), { - newIngestionFunctionAlias: "map-content-new" - }); - const persistedRenameCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/graphql/persisted-documents/doc-old/commands/rename")); - assert.ok(persistedRenameCall); - assert.deepEqual(JSON.parse(persistedRenameCall.bodyText ?? "{}"), { - newPersistedDocumentAlias: "doc-new" - }); - const webhookRenameCall = calls.find((c) => c.method === "POST" && - c.url.includes("/v1/projects/proj/environments/dev/webhooks/hook-old/commands/rename")); - assert.ok(webhookRenameCall); - assert.deepEqual(JSON.parse(webhookRenameCall.bodyText ?? "{}"), { newWebhookAlias: "hook-new" }); -}); -//# sourceMappingURL=contracts.apply-openapi-shape.test.js.map \ No newline at end of file diff --git a/test/contracts.apply-openapi-shape.test.js.map b/test/contracts.apply-openapi-shape.test.js.map deleted file mode 100644 index c9d4ab6..0000000 --- a/test/contracts.apply-openapi-shape.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"contracts.apply-openapi-shape.test.js","sourceRoot":"","sources":["contracts.apply-openapi-shape.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.d.ts b/test/contracts.pull-contract-map.test.d.ts deleted file mode 100644 index 25f9644..0000000 --- a/test/contracts.pull-contract-map.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=contracts.pull-contract-map.test.d.ts.map \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.d.ts.map b/test/contracts.pull-contract-map.test.d.ts.map deleted file mode 100644 index 81ba212..0000000 --- a/test/contracts.pull-contract-map.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"contracts.pull-contract-map.test.d.ts","sourceRoot":"","sources":["contracts.pull-contract-map.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.js b/test/contracts.pull-contract-map.test.js deleted file mode 100644 index ee5c7ff..0000000 --- a/test/contracts.pull-contract-map.test.js +++ /dev/null @@ -1,216 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import path from "node:path"; -import os from "node:os"; -import YAML from "yaml"; -import { pullCommand } from "../src/commands/pull.js"; -import { createInitialState, readComposeState, writeComposeState } from "../src/compose/state.js"; -function jsonResponse(status, body) { - return new Response(JSON.stringify(body), { - status, - headers: { "content-type": "application/json" } - }); -} -async function readContractMap() { - const filePath = path.resolve(process.cwd(), "docs/contracts/pull-contract.json"); - const text = await fs.readFile(filePath, "utf8"); - return JSON.parse(text); -} -function resolvePath(template, params) { - let out = template; - for (const [k, v] of Object.entries(params)) { - out = out.replaceAll(`{${k}}`, v); - } - return out; -} -function assertContractCall(contracts, operation, calls, params) { - const contract = contracts.operations[operation]; - assert.ok(contract, `Missing contract entry for ${operation}`); - const expectedPath = resolvePath(contract.pathTemplate, params); - const call = calls.find((c) => c.method === contract.method && c.pathname === expectedPath); - assert.ok(call, `Missing call for ${operation}: ${contract.method} ${expectedPath}`); -} -async function createFixture() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "compose-pull-contract-")); - const envDir = path.join(root, "env", "dev"); - await fs.mkdir(path.join(envDir, "type-schemas"), { recursive: true }); - await fs.mkdir(path.join(envDir, "functions", "ingestion"), { recursive: true }); - await fs.mkdir(path.join(envDir, "graphql", "persisted"), { recursive: true }); - const cfg = { - version: 1, - project: "test-project", - environments: { - dev: { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - } - }; - await fs.writeFile(path.join(root, "umbraco-compose.yaml"), YAML.stringify(cfg), "utf8"); - await writeComposeState(root, createInitialState("test-project", ["dev"])); - return { root }; -} -test("pull emitted read calls match pull contract map", async () => { - const contracts = await readContractMap(); - const { root } = await createFixture(); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - calls.push({ method, pathname: u.pathname }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") - return jsonResponse(200, { edges: [] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") - return jsonResponse(200, { typeSchemaAlias: "article", schema: {}, description: null }); - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/fn-a") - return jsonResponse(200, { ingestionFunctionAlias: "fn-a", script: "export default () => null;" }); - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/doc-a") - return jsonResponse(200, { persistedDocumentAlias: "doc-a", document: "query { __typename }" }); - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") - return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-a" } }] }); - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/hook-a") - return jsonResponse(200, { webhookAlias: "hook-a", url: "https://example.com/hook", eventTypes: [], collectionAliases: [], typeSchemaAliases: [], customHeaders: {} }); - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await pullCommand.handler({ - dir: root, - env: "dev", - clientId: "client", - clientSecret: "secret" - }); - } - finally { - globalThis.fetch = oldFetch; - } - const baseParams = { - projectAlias: "test-project", - environmentAlias: "dev", - typeSchemaAlias: "article", - ingestionFunctionAlias: "fn-a", - persistedDocumentAlias: "doc-a", - webhookAlias: "hook-a" - }; - assertContractCall(contracts, "environmentList", calls, baseParams); - assertContractCall(contracts, "collectionList", calls, baseParams); - assertContractCall(contracts, "typeSchemaList", calls, baseParams); - assertContractCall(contracts, "typeSchemaGet", calls, baseParams); - assertContractCall(contracts, "ingestionFunctionList", calls, baseParams); - assertContractCall(contracts, "ingestionFunctionGet", calls, baseParams); - assertContractCall(contracts, "persistedDocumentList", calls, baseParams); - assertContractCall(contracts, "persistedDocumentGet", calls, baseParams); - assertContractCall(contracts, "webhookList", calls, baseParams); - assertContractCall(contracts, "webhookGet", calls, baseParams); -}); -test("pull contract-map calls use state remoteAlias path when local alias differs", async () => { - const contracts = await readContractMap(); - const { root } = await createFixture(); - const composePath = path.join(root, "umbraco-compose.yaml"); - const cfgText = await fs.readFile(composePath, "utf8"); - const cfg = YAML.parse(cfgText); - cfg.environments = { - "dev-renamed": { - dir: "./env/dev", - description: "Dev", - managementBaseUrl: "https://management.example" - } - }; - await fs.writeFile(composePath, YAML.stringify(cfg), "utf8"); - const state = await readComposeState(root); - assert.ok(state); - state.environments[0].alias = "dev-renamed"; - state.environments[0].remoteAlias = "dev"; - await writeComposeState(root, state); - const calls = []; - const oldFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const method = (init?.method ?? "GET").toUpperCase(); - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const u = new URL(url); - calls.push({ method, pathname: u.pathname }); - if (u.pathname === "/v1/auth/token") - return jsonResponse(200, { access_token: "token", expires_in: 3600 }); - if (u.pathname === "/v1/projects/test-project/environments") { - return jsonResponse(200, { edges: [{ node: { environmentAlias: "dev" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/collections") { - return jsonResponse(200, { edges: [] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas") { - return jsonResponse(200, { edges: [{ node: { typeSchemaAlias: "article" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/type-schemas/article") { - return jsonResponse(200, { typeSchemaAlias: "article", schema: {}, description: null }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion") { - return jsonResponse(200, { edges: [{ node: { ingestionFunctionAlias: "fn-a" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/functions/ingestion/fn-a") { - return jsonResponse(200, { ingestionFunctionAlias: "fn-a", script: "export default () => null;" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents") { - return jsonResponse(200, { edges: [{ node: { persistedDocumentAlias: "doc-a" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/graphql/persisted-documents/doc-a") { - return jsonResponse(200, { persistedDocumentAlias: "doc-a", document: "query { __typename }" }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks") { - return jsonResponse(200, { edges: [{ node: { webhookAlias: "hook-a" } }] }); - } - if (u.pathname === "/v1/projects/test-project/environments/dev/webhooks/hook-a") { - return jsonResponse(200, { - webhookAlias: "hook-a", - url: "https://example.com/hook", - eventTypes: [], - collectionAliases: [], - typeSchemaAliases: [], - customHeaders: {} - }); - } - return jsonResponse(404, { error: "unexpected path" }); - }); - try { - await pullCommand.handler({ - dir: root, - env: "dev-renamed", - clientId: "client", - clientSecret: "secret" - }); - } - finally { - globalThis.fetch = oldFetch; - } - const baseParams = { - projectAlias: "test-project", - environmentAlias: "dev", - typeSchemaAlias: "article", - ingestionFunctionAlias: "fn-a", - persistedDocumentAlias: "doc-a", - webhookAlias: "hook-a" - }; - assertContractCall(contracts, "environmentList", calls, baseParams); - assertContractCall(contracts, "collectionList", calls, baseParams); - assertContractCall(contracts, "typeSchemaList", calls, baseParams); - assertContractCall(contracts, "typeSchemaGet", calls, baseParams); - assertContractCall(contracts, "ingestionFunctionList", calls, baseParams); - assertContractCall(contracts, "ingestionFunctionGet", calls, baseParams); - assertContractCall(contracts, "persistedDocumentList", calls, baseParams); - assertContractCall(contracts, "persistedDocumentGet", calls, baseParams); - assertContractCall(contracts, "webhookList", calls, baseParams); - assertContractCall(contracts, "webhookGet", calls, baseParams); -}); -//# sourceMappingURL=contracts.pull-contract-map.test.js.map \ No newline at end of file diff --git a/test/contracts.pull-contract-map.test.js.map b/test/contracts.pull-contract-map.test.js.map deleted file mode 100644 index 2d91e9d..0000000 --- a/test/contracts.pull-contract-map.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"contracts.pull-contract-map.test.js","sourceRoot":"","sources":["contracts.pull-contract-map.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file From 5e380edac80aa4845a1af3a0de481ef18b9b3ab1 Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 10:22:44 -0500 Subject: [PATCH 45/47] Fix TS build errors. --- src/commands/apply.ts | 3 +++ src/compose/apply.ts | 46 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/commands/apply.ts b/src/commands/apply.ts index 488b22e..cc8caca 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -142,6 +142,9 @@ export async function runApply(rawArgs: RunApplyArgs): Promise { for (const envAlias of targetEnvs) { const envCfg = cfg.environments[envAlias]; + if (!envCfg) { + throw new Error(`Environment "${envAlias}" is missing from umbraco-compose.yaml.`); + } const envDir = path.resolve(rootDir, envCfg.dir); const envDescription = envCfg.description ?? `${envAlias} Environment`; diff --git a/src/compose/apply.ts b/src/compose/apply.ts index 452ab79..477f553 100644 --- a/src/compose/apply.ts +++ b/src/compose/apply.ts @@ -388,7 +388,13 @@ async function applyCollections(client: ManagementClient, opts: ApplyOptions): P for (const alias of existingAliases.keys()) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; - out.push({ kind: "delete", env: opEnv, resource: "collection", alias, details }); + out.push({ + kind: "delete", + env: opEnv, + resource: "collection", + alias, + ...(details !== undefined ? { details } : {}) + }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/collections/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -621,7 +627,13 @@ async function applyTypeSchemas(client: ManagementClient, opts: ApplyOptions): P if (typeof alias !== "string" || desiredAliases.has(alias) || renamedFrom.has(alias)) continue; const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; - out.push({ kind: "delete", env: opEnv, resource: "type-schema", alias, details }); + out.push({ + kind: "delete", + env: opEnv, + resource: "type-schema", + alias, + ...(details !== undefined ? { details } : {}) + }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/type-schemas/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -849,7 +861,13 @@ async function applyIngestionFunctions(client: ManagementClient, opts: ApplyOpti for (const alias of existingAliases.keys()) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; - out.push({ kind: "delete", env: opEnv, resource: "ingestion-function", alias, details }); + out.push({ + kind: "delete", + env: opEnv, + resource: "ingestion-function", + alias, + ...(details !== undefined ? { details } : {}) + }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/functions/ingestion/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -1082,7 +1100,13 @@ async function applyPersistedDocs(client: ManagementClient, opts: ApplyOptions): for (const alias of existingAliases) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; - out.push({ kind: "delete", env: opEnv, resource: "persisted-doc", alias, details }); + out.push({ + kind: "delete", + env: opEnv, + resource: "persisted-doc", + alias, + ...(details !== undefined ? { details } : {}) + }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/graphql/persisted-documents/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -1418,7 +1442,13 @@ async function applyWebhooks(client: ManagementClient, opts: ApplyOptions): Prom for (const alias of existingAliases) { if (desiredAliases.has(alias) || renamedFrom.has(alias)) continue; const details = ambiguousRemoteAliases.has(alias) ? "no unique rename match" : undefined; - out.push({ kind: "delete", env: opEnv, resource: "webhook", alias, details }); + out.push({ + kind: "delete", + env: opEnv, + resource: "webhook", + alias, + ...(details !== undefined ? { details } : {}) + }); if (!opts.planOnly) { const deletePath = `${envBase(opts)}/webhooks/${encodeURIComponent(alias)}`; const deleteRes = await client.delete(deletePath); @@ -1474,8 +1504,10 @@ function computeRenameAnalysis( const ambiguousRemoteAliases = new Set(); for (const [signature, desiredAliases] of desiredBySignature.entries()) { const remoteAliases = remoteBySignature.get(signature) ?? []; - if (desiredAliases.length === 1 && remoteAliases.length === 1) { - renameFromByTo.set(desiredAliases[0], remoteAliases[0]); + const desiredAlias = desiredAliases[0]; + const remoteAlias = remoteAliases[0]; + if (desiredAliases.length === 1 && remoteAliases.length === 1 && desiredAlias && remoteAlias) { + renameFromByTo.set(desiredAlias, remoteAlias); continue; } if (desiredAliases.length > 0 && remoteAliases.length > 0) { From 609b0ea97c7c8b534c9166baba7e5105d8a5535f Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 10:27:18 -0500 Subject: [PATCH 46/47] Update docs and add changelog for v1.0.0-rc.1 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/commands.md | 6 +++--- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b0c83b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [1.0.0-rc.1] - 2026-02-13 + +### Added +- Core command set stabilized for IaC-style workflows: + - `init`, `generate`, `validate`, `clone`, `pull`, `status`, `plan`, `apply`, `env rename` +- Broad automated test coverage for: + - command behavior and exit-code semantics + - multi-environment plan/apply flows + - rename safety and alias routing + - apply/pull API contract-map checks + - OpenAPI-shape assertions for key apply endpoints/payloads +- CI and deploy GitHub Actions workflows with guarded apply behavior. +- Operational docs: + - `docs/commands.md` + - `docs/testing.md` + - `docs/ci.md` + - `docs/workflow.md` + - `docs/release-v1-checklist.md` + +### Changed +- Environment identity model hardened with `compose.state.json` (`id`, `alias`, `remoteAlias`) to support safe rename convergence. +- `plan` and `pull` use `remoteAlias` routing when a rename is pending. +- `apply` performs environment rename first, then applies nested resources through the new alias. +- Apply operation output now includes explicit environment context and per-environment/final summaries. +- Non-environment rename inference expanded and guarded: + - unique one-to-one signature matches infer rename + - ambiguous matches fall back to create/delete with explicit context +- Apply convergence expanded across managed entities, including create/update/delete and rename paths where supported. + +### Fixed +- Prevented lock/state writes when apply emits warnings. +- Improved warning/plan messaging around rename and missing nested resources. +- Tightened TypeScript compatibility under strict checks (`exactOptionalPropertyTypes`, indexed access, env config narrowing). +- Resolved test harness response-shape issues around `204` delete cases. + +## [Unreleased] +- Final `1.0.0` version bump and release tagging. diff --git a/docs/commands.md b/docs/commands.md index 5d591cf..4197ada 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -39,9 +39,9 @@ Use this baseline pipeline for pull-request validation and main-branch deploy: 1. `npm ci` 2. `npm run build` 3. `npm test` -4. `node dist/index.js compose validate --dir ./compose --strict` -5. `node dist/index.js compose plan --dir ./compose` -6. On approved/protected branch only: `node dist/index.js compose apply --dir ./compose` +4. `node dist/index.js validate --dir ./compose --strict` +5. `node dist/index.js plan --dir ./compose` +6. On approved/protected branch only: `node dist/index.js apply --dir ./compose` Notes: - keep `plan` output as a build artifact or PR comment for review From b1c8e58f11a5d605ac061a600d21b980c8dc6fcd Mon Sep 17 00:00:00 2001 From: Allen Smith Date: Fri, 13 Feb 2026 10:30:49 -0500 Subject: [PATCH 47/47] Bump version for v1.0.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c647060..c192a9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umbraco-compose-cli", - "version": "0.1.0", + "version": "1.0.0-rc.1", "type": "module", "description": "", "bin": {