Files
PaperClipAI/server/src/__tests__/managed-config.test.ts
T
DottaandPaperclip 9077748894 Remove the Pipelines experiment and preserve its data (#16834)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Pipelines was an unused experiment with its own UI, API, CLI, and
automation.
> - Keeping it leaves unsupported product surfaces and extra task-detail
queries.
> - Removing the experiment must preserve data that users already saved.
> - This pull request removes the feature and keeps its database schema
and migration history.
> - Cases and Routines remain available as separate features.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Remove the unused Pipelines experiment across the application.

**Current behavior**

The experiment includes pipeline boards, stage settings, case
automation, review queues, learnings, permissions, and CLI commands.
Task detail also queries pipeline links.

**Proposed behavior**

Remove all these product surfaces and their implementation. Preserve
existing tables, records, and documents. Ignore the retired boolean in
older managed configurations so upgrades still start.

**Breaking changes**

Pipeline URLs, API endpoints, CLI commands, and `pipelines:write` are
removed. Task detail no longer includes the old `linkedCases` field. The
independent Cases API remains available. No database migration or
cleanup is included.

Related feature work: #7907, #9110, #10905, #11375, #13004, #14671,
#16133. Related issues: #9663, #10310, #10312. This removal supersedes
the feature; it does not implement those pending enhancements or delete
their saved data.

## What Changed

- Remove Pipeline UI pages, navigation, gates, clients, query keys,
helpers, and unused design tokens.
- Remove Pipeline REST routes, services, automation dispatch,
task-detail queries, and OpenAPI discovery.
- Remove the CLI commands, shared contracts, experimental flag, catalog
entry, and permission defaults.
- Remove feature-specific tests, tutorials, smoke scripts, maps, and
Storybook examples.
- Preserve database schema, migration history, and the reserved document
key used by old records.
- Keep Cases routes independent and cover obsolete stored and managed
settings.

## Verification

- Passed: `pnpm build` and `pnpm -r typecheck`.
- Passed: targeted settings, managed configuration, sidebar, settings
UI, routing, and feature-catalog tests. A first sidebar run caught a
shared Workspaces icon import; the import was restored and the sidebar
rerun passed.
- Passed: 40 tests covering Cases routes, OpenAPI discovery, retained
Pipeline table constraints, and schema snapshot drift.
- Passed: token gates, map validation, generated API metadata check,
Storybook catalog validation, and 28 map/catalog tests.
- Passed: isolated local browser verification of navigation and
Experimental settings with Pipelines absent; Cases and Routines still
open normally. Live GET/POST requests to retired Pipeline endpoints
return 404, and the retired setting is absent. CLI help has no Pipeline
commands.
- Passed: 22 access-service and agent-default-grant tests after
restoring unrelated permission assertions flagged in review.
- Local full run: `pnpm test:run` reported one failure in the unchanged
workspace-runtime port-allocation test. That test passed in an isolated
rerun. The long local run was stopped after full CI passed; it is not
reported as a complete local pass.
- Passed: all 56 checks on `def9181f3a4491d95fca1d38e9739f5b821aae3e` (2
conditional checks skipped), including build, typecheck, test shards,
and browser suites. Eleven interrupted CI jobs reported “The operation
was canceled”; their one-time retry passed. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/39255211006).
- Greptile: 5/5 on the final commit, with the only review finding fixed
and resolved.
- Sandbox socket restrictions affected the first build/API test
attempts. The same commands ran with local sockets available.

## Risks

- Existing Pipeline clients must stop using the removed endpoints and
commands.
- Existing data remains in the database and backups, but the removed UI
no longer displays it.
- Historical boolean `enablePipelines` values are ignored. They cannot
restore the feature.
- Cases and Routines must retain their separate behavior. Targeted route
tests cover this boundary.

## Model Used

OpenAI Codex, GPT-6, with repository inspection, editing, shell
execution, and browser verification. The exact deployed model ID and
context-window size are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-19 08:09:46 -05:00

373 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
MANAGED_CONFIG_ENV_KEY,
getManagedInstanceConfig,
managedFeatureKeySet,
parseManagedConfigEnv,
} from "../services/managed-config.js";
function envWith(raw: string | undefined) {
return { [MANAGED_CONFIG_ENV_KEY]: raw };
}
function validDoc(overrides: Record<string, unknown> = {}) {
return JSON.stringify({
v: 1,
mode: "cloud",
catalogVersion: "2026.720.0",
features: { enableApps: false, enableStatusCards: true },
plugins: { autoInstall: ["daytona", "kubernetes"] },
...overrides,
});
}
describe("managedFeatureKeySet", () => {
it("contains exactly the boolean flags of the experimental schema", () => {
const keys = managedFeatureKeySet();
expect(keys.has("enableApps")).toBe(true);
expect(keys.has("enableWorktreeRunExecution")).toBe(true);
// Server-managed bookkeeping fields are not overlayable features.
expect(keys.has("worktreeRunExecutionActivatedAt")).toBe(false);
expect(keys.has("worktreeRunExecutionActivationInstanceId")).toBe(false);
});
});
describe("parseManagedConfigEnv", () => {
it("returns null when the env var is absent (self-hosted)", () => {
expect(parseManagedConfigEnv({})).toBeNull();
expect(parseManagedConfigEnv(envWith(undefined))).toBeNull();
});
it("throws when the env var is present but blank (fail closed)", () => {
expect(() => parseManagedConfigEnv(envWith(""))).toThrow(/is set but blank/);
expect(() => parseManagedConfigEnv(envWith(" "))).toThrow(/is set but blank/);
expect(() => parseManagedConfigEnv(envWith("\n\t"))).toThrow(/is set but blank/);
});
it("parses a complete valid document", () => {
const config = parseManagedConfigEnv(envWith(validDoc()));
expect(config).toEqual({
v: 1,
mode: "cloud",
catalogVersion: "2026.720.0",
features: { enableApps: false, enableStatusCards: true },
plugins: { autoInstall: ["daytona", "kubernetes"] },
environments: [],
});
});
it("accepts the deprecated runner ingress key for managed-config compatibility", () => {
const config = parseManagedConfigEnv(
envWith(validDoc({ features: { enableRunnerPreviewIngress: true } })),
);
expect(config?.features).toEqual({ enableRunnerPreviewIngress: true });
});
it("accepts empty features {} and autoInstall [] sections", () => {
const config = parseManagedConfigEnv(
envWith(validDoc({ features: {}, plugins: { autoInstall: [] } })),
);
expect(config).toEqual({
v: 1,
mode: "cloud",
catalogVersion: "2026.720.0",
features: {},
plugins: { autoInstall: [] },
environments: [],
});
});
it("throws when the features section is missing (fail closed)", () => {
const doc = { v: 1, mode: "cloud", catalogVersion: "2026.720.0", plugins: { autoInstall: [] } };
expect(() => parseManagedConfigEnv(envWith(JSON.stringify(doc)))).toThrow(
/requires a "features" object/,
);
});
it("throws when the plugins section or autoInstall is missing (fail closed)", () => {
const noPlugins = {
v: 1,
mode: "cloud",
catalogVersion: "2026.720.0",
features: {},
};
expect(() => parseManagedConfigEnv(envWith(JSON.stringify(noPlugins)))).toThrow(
/requires a "plugins" object/,
);
expect(() => parseManagedConfigEnv(envWith(validDoc({ plugins: {} })))).toThrow(
/requires a "plugins.autoInstall" array/,
);
});
it("returns a frozen document", () => {
const config = parseManagedConfigEnv(envWith(validDoc()));
expect(Object.isFrozen(config)).toBe(true);
expect(Object.isFrozen(config?.features)).toBe(true);
expect(Object.isFrozen(config?.plugins.autoInstall)).toBe(true);
});
it("throws on invalid JSON", () => {
expect(() => parseManagedConfigEnv(envWith("{not json"))).toThrow(
/PAPERCLIP_MANAGED_CONFIG is not valid JSON/,
);
});
it("throws on non-object documents", () => {
expect(() => parseManagedConfigEnv(envWith("[]"))).toThrow(/must be a JSON object/);
expect(() => parseManagedConfigEnv(envWith("42"))).toThrow(/must be a JSON object/);
expect(() => parseManagedConfigEnv(envWith("null"))).toThrow(/must be a JSON object/);
expect(() => parseManagedConfigEnv(envWith('"cloud"'))).toThrow(/must be a JSON object/);
});
it("throws on an unknown top-level key", () => {
expect(() => parseManagedConfigEnv(envWith(validDoc({ extra: true })))).toThrow(
/unknown top-level key "extra"/,
);
});
it("throws on an unsupported v", () => {
expect(() => parseManagedConfigEnv(envWith(validDoc({ v: 2 })))).toThrow(
/unsupported "v" 2; this build supports v=1/,
);
expect(() => parseManagedConfigEnv(envWith(validDoc({ v: "1" })))).toThrow(/unsupported "v"/);
expect(() =>
parseManagedConfigEnv(
envWith(JSON.stringify({ mode: "cloud", catalogVersion: "x" })),
),
).toThrow(/unsupported "v"/);
});
it("throws on a non-cloud mode", () => {
expect(() => parseManagedConfigEnv(envWith(validDoc({ mode: "self-hosted" })))).toThrow(
/invalid "mode" "self-hosted"; expected "cloud"/,
);
expect(() =>
parseManagedConfigEnv(envWith(JSON.stringify({ v: 1, catalogVersion: "x" }))),
).toThrow(/invalid "mode"/);
});
it("throws on a missing or empty catalogVersion", () => {
expect(() =>
parseManagedConfigEnv(envWith(JSON.stringify({ v: 1, mode: "cloud" }))),
).toThrow(/non-empty string "catalogVersion"/);
expect(() => parseManagedConfigEnv(envWith(validDoc({ catalogVersion: "" })))).toThrow(
/non-empty string "catalogVersion"/,
);
expect(() => parseManagedConfigEnv(envWith(validDoc({ catalogVersion: 7 })))).toThrow(
/non-empty string "catalogVersion"/,
);
});
it("throws on a non-object features section", () => {
expect(() => parseManagedConfigEnv(envWith(validDoc({ features: ["enableApps"] })))).toThrow(
/"features" must be an object/,
);
});
it.each([true, false])("ignores the retired Pipelines flag (%s) from older Cloud catalogs", (enabled) => {
const config = parseManagedConfigEnv(envWith(validDoc({
features: { enablePipelines: enabled, enableCases: true },
})));
expect(config?.features).toEqual({ enableCases: true });
expect(managedFeatureKeySet().has("enablePipelines")).toBe(false);
});
it("throws on an unknown feature key", () => {
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ features: { enableTimeTravel: true } }))),
).toThrow(/unknown feature key "enableTimeTravel"/);
// A server-managed bookkeeping field is not an overlayable feature.
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ features: { worktreeRunExecutionActivatedAt: true } })),
),
).toThrow(/unknown feature key "worktreeRunExecutionActivatedAt"/);
});
it("throws on a feature key the catalog does not mark tier \"managed\"", () => {
// `enableStreamlinedLeftNavigation` is a real schema flag, but its catalog
// tier is `preference` (tenant-controllable) — a managed-config document
// targeting it has incompatible catalog semantics and must fail closed.
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ features: { enableStreamlinedLeftNavigation: true } })),
),
).toThrow(
/"features" key "enableStreamlinedLeftNavigation" has tier "preference".*only tier "managed" keys/,
);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ features: { enableDecisions: false } }))),
).toThrow(/has tier "preference"/);
});
it("throws on non-boolean feature values", () => {
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ features: { enableApps: "true" } }))),
).toThrow(/"features.enableApps" must be a boolean/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ features: { enableApps: 1 } }))),
).toThrow(/"features.enableApps" must be a boolean/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ features: { enableApps: null } }))),
).toThrow(/"features.enableApps" must be a boolean/);
});
it("throws on malformed plugins sections", () => {
expect(() => parseManagedConfigEnv(envWith(validDoc({ plugins: [] })))).toThrow(
/"plugins" must be an object/,
);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ plugins: { install: [] } }))),
).toThrow(/"plugins" has unknown key "install"/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: "daytona" } }))),
).toThrow(/"plugins.autoInstall" must be an array/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: [""] } }))),
).toThrow(/non-empty strings/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: [" daytona"] } }))),
).toThrow(/non-empty strings/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: [42] } }))),
).toThrow(/non-empty strings/);
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ plugins: { autoInstall: ["daytona", "daytona"] } })),
),
).toThrow(/duplicate entry "daytona"/);
});
});
describe("parseManagedConfigEnv environments section", () => {
const entry = (overrides: Record<string, unknown> = {}) => ({
name: "Daytona",
provider: "daytona",
config: { target: "us" },
...overrides,
});
it("defaults to an empty list when the section is absent (pre-section documents keep booting)", () => {
const config = parseManagedConfigEnv(envWith(validDoc()));
expect(config?.environments).toEqual([]);
});
it("parses a declared environment and freezes it", () => {
const config = parseManagedConfigEnv(
envWith(validDoc({ environments: [entry({ description: "Managed Daytona sandbox." })] })),
);
expect(config?.environments).toHaveLength(1);
const spec = config?.environments[0];
expect(spec?.name).toBe("Daytona");
expect(spec?.description).toBe("Managed Daytona sandbox.");
expect(spec?.provider).toBe("daytona");
expect(spec?.config).toEqual({ target: "us" });
expect(Object.isFrozen(config?.environments)).toBe(true);
expect(Object.isFrozen(spec)).toBe(true);
expect(Object.isFrozen(spec?.config)).toBe(true);
});
it("treats config and description as optional", () => {
const config = parseManagedConfigEnv(
envWith(validDoc({ environments: [{ name: "Daytona", provider: "daytona" }] })),
);
expect(config?.environments[0]?.config).toEqual({});
expect(config?.environments[0]?.description).toBeUndefined();
});
it("rejects a non-array section and non-object entries", () => {
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: {} }))),
).toThrow(/"environments" must be an array/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: ["daytona"] }))),
).toThrow(/"environments\[0\]" must be an object/);
});
it("rejects more than one entry (single managed sandbox slot)", () => {
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ environments: [entry(), entry({ name: "Other", provider: "kubernetes" })] })),
),
).toThrow(/at most one entry/);
});
it("rejects unknown entry keys", () => {
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ envVars: {} })] }))),
).toThrow(/unknown key "envVars"/);
});
it("rejects malformed names, descriptions, and providers", () => {
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ name: "" })] }))),
).toThrow(/"environments\[0\].name"/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ name: " Daytona" })] }))),
).toThrow(/"environments\[0\].name"/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ description: " " })] }))),
).toThrow(/"environments\[0\].description"/);
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ provider: 7 })] }))),
).toThrow(/"environments\[0\].provider"/);
});
it("rejects a provider that plugins.autoInstall does not provision", () => {
expect(() =>
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ provider: "modal" })] }))),
).toThrow(/not in "plugins.autoInstall"/);
});
it("rejects a config that sets provider", () => {
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ environments: [entry({ config: { provider: "daytona" } })] })),
),
).toThrow(/must not set "provider"/);
});
it("rejects secret-bearing config keys at any depth (secrets travel as env vars)", () => {
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ environments: [entry({ config: { apiKey: "not-a-real-key" } })] })),
),
).toThrow(/looks secret-bearing/);
expect(() =>
parseManagedConfigEnv(
envWith(validDoc({ environments: [entry({ config: { auth: { accessToken: "t" } } })] })),
),
).toThrow(/auth.accessToken/);
expect(() =>
parseManagedConfigEnv(
envWith(
validDoc({ environments: [entry({ config: { adapters: [{ clientSecret: "s" }] } })] }),
),
),
).toThrow(/adapters\[0\].clientSecret/);
});
});
describe("getManagedInstanceConfig", () => {
it("caches by raw env value and reparses when it changes", () => {
const raw = validDoc();
const first = getManagedInstanceConfig(envWith(raw));
const second = getManagedInstanceConfig(envWith(raw));
expect(second).toBe(first);
const changed = getManagedInstanceConfig(
envWith(validDoc({ catalogVersion: "2026.721.0" })),
);
expect(changed?.catalogVersion).toBe("2026.721.0");
expect(changed).not.toBe(first);
expect(getManagedInstanceConfig(envWith(undefined))).toBeNull();
});
it("rethrows parse failures on every call instead of caching them", () => {
expect(() => getManagedInstanceConfig(envWith("{bad"))).toThrow(/not valid JSON/);
expect(() => getManagedInstanceConfig(envWith("{bad"))).toThrow(/not valid JSON/);
});
});