Files
PaperClipAI/server/src/services/managed-environments.test.ts
T
Nicky LeachandClaude Opus 5.5 913102870d fix(environments): fill the managed instance default once, under a lock (#19632)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - On a managed instance under `enableManagedSandboxOnly`, boot points
the instance default at the managed sandbox when nothing deliberate is
set
> - That write read the default and wrote it later without a lock. Two
overlapping boots, or a boot and an admin write, could overwrite a value
the other had just committed
> - A revert path also reset that stamped default whenever the mode was
turned off, so a platform-filled default could change again later
> - A planned plugin-environment default should fill only an unset
default and never change a set one, which needs every instance's default
to stay set
> - This pull request makes the fill one locked transaction and removes
the revert, so a filled default is an ordinary set value
> - The benefit is a single, race-free rule: boot fills an unset default
once, and nothing changes a set default afterwards

## Linked Issues or Issue Description

Refs #19551 (the plugin-environment default that builds on this rule)

**Subsystem affected**
Server environments: the managed environment bootstrap and the instance
settings route.

**Problem or motivation**
`applyManagedEnvironments` read the instance default, decided, and wrote
it in separate steps with no lock, so a concurrent boot or admin write
could be overwritten. When `enableManagedSandboxOnly` was not declared,
a revert reset a default the platform had filled. That needed a
`managedDefaultStamped` marker, plus a marker clear on every admin
default write. A later boot could therefore change a default that had
already been set.

**Proposed solution**
Fill the default in one transaction that locks the instance default row
and rechecks it. Fill only an unset, local or deleted default. Remove
the revert and its marker, so a filled default behaves like any other
set value.

**Alternatives considered**
Keeping the revert and adding locks to it. A revert on a mode change
would still move an existing instance's default later, which a fill-only
plugin default cannot tolerate.

**Roadmap alignment**
This narrows existing managed-environment behaviour. It adds no product
surface.

## What Changed

- New `server/src/services/managed-instance-default.ts`:
`fillUnsetInstanceDefault(db, environmentId)`. In one transaction it
creates the settings row if needed, locks the instance default row (`FOR
UPDATE`), and rechecks the current value. It writes only when the
default is unset, names the local environment, or names a deleted
environment. Any other set default is kept.
- `server/src/services/managed-environments.ts`:
- Under `enableManagedSandboxOnly`, the managed sandbox fill now uses
that helper, through a new `fillInstanceDefault` test seam.
- The mode-off revert and the `managedDefaultStamped` marker are
removed.
- The unused `instanceSettings` seam option is removed, along with the
`getById`, `update` and `findManagedSandboxEnvironment` methods of the
environments seam.
- `server/src/routes/instance-settings.ts`: an explicit default write is
a plain `update` again. The marker clear and its transaction are gone.
- `doc/DEVELOPING.md`: the "Managed environments" section states the
fill rule.

Existing rows may still carry `managedDefaultStamped` in their metadata.
Nothing reads it any more, and it is not part of the managed stock hash.

## Verification

- `cd server && npx vitest run
src/__tests__/managed-instance-default.test.ts` (embedded Postgres):
- The fill writes an unset default, including on a fresh instance with
no settings row. It also writes a default that names the local
environment, or one whose environment was deleted.
- It never changes a default set to an ssh, plugin or other sandbox
environment, and leaves a default that already names the target alone.
- Controlled overlap: an uncommitted admin default write makes the fill
wait, and the fill then keeps the admin's value. This test fails without
the row lock.
  - Two overlapping fills: exactly one writes.
- Real boot pass: the default is filled under managed-sandbox-only and
stays the same after a boot with the mode off. A set default stays on
every boot.
- `npx vitest run src/services/managed-environments.test.ts
src/services/managed-environments-plugin-entries.test.ts`: the fill runs
only under managed-sandbox-only, with the managed sandbox row. It never
runs for a plugin environment. Nothing is written when the mode is off.
A failed fill does not fail the ensure.
- `npx vitest run src/__tests__/instance-settings-routes.test.ts`: an
explicit default write is a plain update that touches no environment
row.
- `npx vitest run src/__tests__/managed-plugin-environments.test.ts
src/__tests__/environment-routes.test.ts
src/__tests__/server-startup-feedback-export.test.ts
src/__tests__/instance-settings-service.test.ts`: pass.
- `pnpm --filter @paperclipai/server typecheck` passes.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them.

## Risks

- Behaviour change: when `enableManagedSandboxOnly` is turned off for an
instance, a default the platform filled with the managed sandbox now
stays. Local execution is still allowed then, but it is no longer the
default unless an admin picks it.
- No schema change. Stale `managedDefaultStamped` keys stay in some row
metadata and are ignored.
- The fill holds the instance default row lock for one short transaction
at boot.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (file edits, shell, test runs).

## 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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:01:58 -08:00

443 lines
16 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { applyManagedEnvironments } from "./managed-environments.js";
import {
environmentRow,
environmentsSeam,
noDb,
parsedConfig,
readyDriverResolver,
reconciliationResult,
recoveryHandle,
runningWorkerManager,
tick,
} from "./managed-environments.test-support.js";
describe("applyManagedEnvironments", () => {
it("no-ops for self-hosted instances and for documents without environments", async () => {
expect(await applyManagedEnvironments(noDb, null)).toBeNull();
expect(await applyManagedEnvironments(noDb, parsedConfig())).toBeNull();
});
it("refuses startup when a forced execution mode also claims the managed sandbox slot", async () => {
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
await expect(
applyManagedEnvironments(noDb, config, {
env: { PAPERCLIP_EXECUTION_MODE: "kubernetes" },
}),
).rejects.toThrow(/mutually exclusive/);
});
it("ensures each declared environment through the provider-agnostic service call", async () => {
const ensureManagedSandboxEnvironment = vi
.fn()
.mockResolvedValue(reconciliationResult());
const config = parsedConfig({
environments: [
{
name: "Daytona",
description: "Managed Daytona sandbox.",
provider: "daytona",
config: { target: "us" },
},
],
});
const resolveSandboxProviderDriver = readyDriverResolver();
const workerManager = runningWorkerManager();
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager,
environments: environmentsSeam({ ensureManagedSandboxEnvironment }),
resolveSandboxProviderDriver,
});
expect(result).toMatchObject({ ensured: 1, failed: 0, added: 1, skipped: 0 });
expect(result).toMatchObject({ removed: 0, backedUp: 0 });
expect(resolveSandboxProviderDriver).toHaveBeenCalledWith({ db: noDb, driverKey: "daytona" });
expect(workerManager.isRunning).toHaveBeenCalledWith("plugin-1");
expect(ensureManagedSandboxEnvironment).toHaveBeenCalledTimes(1);
expect(ensureManagedSandboxEnvironment).toHaveBeenCalledWith({
name: "Daytona",
description: "Managed Daytona sandbox.",
provider: "daytona",
config: { target: "us" },
stockVersion: "2026.720.0",
platformFullyManaged: true,
});
// The frozen parsed config must not leak into the service (the row's
// config is mutated downstream when the provider key is forced in).
const passedConfig = ensureManagedSandboxEnvironment.mock.calls[0]?.[0]?.config;
expect(Object.isFrozen(passedConfig)).toBe(false);
});
it("waits for the bundled-plugin startup pass before ensuring anything", async () => {
const ensureManagedSandboxEnvironment = vi
.fn()
.mockResolvedValue(reconciliationResult());
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
let releasePlugins!: () => void;
const pluginsReady = new Promise<void>((resolve) => {
releasePlugins = resolve;
});
const pending = applyManagedEnvironments(noDb, config, {
env: {},
pluginsReady,
workerManager: runningWorkerManager(),
environments: environmentsSeam({ ensureManagedSandboxEnvironment }),
resolveSandboxProviderDriver: readyDriverResolver(),
});
// Give the ensure every chance to (incorrectly) run early.
await new Promise((resolve) => setImmediate(resolve));
expect(ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
releasePlugins();
expect(await pending).toMatchObject({ ensured: 1, failed: 0, added: 1 });
expect(ensureManagedSandboxEnvironment).toHaveBeenCalledTimes(1);
});
it("reports operator-modified rows as skipped with a stock update still available", async () => {
const environments = environmentsSeam({
ensureManagedSandboxEnvironment: vi.fn().mockResolvedValue(reconciliationResult({
action: "skipped",
stockStatus: "operator_modified",
updateAvailable: true,
})),
});
const result = await applyManagedEnvironments(noDb, parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
}), {
env: {},
workerManager: runningWorkerManager(),
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
expect(result).toMatchObject({
ensured: 0,
failed: 0,
added: 0,
updated: 0,
unchanged: 0,
skipped: 1,
outcomes: [{
action: "skipped",
stockStatus: "operator_modified",
updateAvailable: true,
}],
});
});
it("skips an entry whose provider plugin is missing and archives its stale row", async () => {
const environments = environmentsSeam({
archiveManagedSandboxEnvironment: vi.fn().mockResolvedValue(environmentRow({ status: "archived" })),
});
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager: runningWorkerManager(),
environments,
resolveSandboxProviderDriver: vi.fn(async () => null),
});
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
expect(environments.archiveManagedSandboxEnvironment).toHaveBeenCalledWith({
provider: "daytona",
});
});
it("skips (and counts failed) an entry whose provider plugin is not ready", async () => {
const environments = environmentsSeam();
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager: runningWorkerManager(),
environments,
resolveSandboxProviderDriver: readyDriverResolver("disabled"),
});
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
expect(environments.archiveManagedSandboxEnvironment).toHaveBeenCalledWith({
provider: "daytona",
});
});
it("skips an entry whose plugin record is ready but whose worker is not running", async () => {
const environments = environmentsSeam();
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const workerManager = runningWorkerManager(false);
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager,
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(workerManager.isRunning).toHaveBeenCalledWith("plugin-1");
expect(environments.ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
expect(environments.archiveManagedSandboxEnvironment).toHaveBeenCalledWith({
provider: "daytona",
});
// No registered handle → no respawn is coming; degraded until next boot.
expect(workerManager.getWorker).toHaveBeenCalledWith("plugin-1");
});
it("reactivates the archived environment once when the provider worker recovers", async () => {
const environments = environmentsSeam();
const config = parsedConfig({
environments: [
{ name: "Daytona", provider: "daytona", config: { target: "us" } },
],
});
const { handle, fireReady } = recoveryHandle();
const workerManager = {
isRunning: vi.fn(() => false),
getWorker: vi.fn(() => handle),
};
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager,
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
// The boot pass itself stays degraded: archived, counted failed.
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.archiveManagedSandboxEnvironment).toHaveBeenCalledWith({
provider: "daytona",
});
expect(environments.ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
// Worker manager respawns the worker → the handle re-emits `ready`.
fireReady();
fireReady();
await tick();
expect(environments.ensureManagedSandboxEnvironment).toHaveBeenCalledTimes(1);
expect(environments.ensureManagedSandboxEnvironment).toHaveBeenCalledWith({
name: "Daytona",
description: undefined,
provider: "daytona",
config: { target: "us" },
stockVersion: "2026.720.0",
platformFullyManaged: true,
});
expect(handle.off).toHaveBeenCalledTimes(1);
});
it("re-checks isRunning after subscribing so a recovery during the archive is not missed", async () => {
const environments = environmentsSeam();
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const { handle } = recoveryHandle();
// Down at the gate check, already back up by the time the listener is
// registered — the `ready` event has been missed.
const workerManager = {
isRunning: vi
.fn()
.mockReturnValueOnce(false)
.mockReturnValue(true),
getWorker: vi.fn(() => handle),
};
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager,
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
await tick();
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.ensureManagedSandboxEnvironment).toHaveBeenCalledTimes(1);
});
it("does not subscribe for recovery when the plugin record is missing or not ready", async () => {
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
for (const resolveSandboxProviderDriver of [
vi.fn(async () => null),
readyDriverResolver("disabled"),
]) {
const workerManager = runningWorkerManager(false);
await applyManagedEnvironments(noDb, config, {
env: {},
workerManager,
environments: environmentsSeam(),
resolveSandboxProviderDriver,
});
expect(workerManager.getWorker).not.toHaveBeenCalled();
}
});
it("logs, not throws, when the recovery re-ensure fails", async () => {
const environments = environmentsSeam({
ensureManagedSandboxEnvironment: vi.fn().mockRejectedValue(new Error("db exploded")),
});
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const { handle, fireReady } = recoveryHandle();
const workerManager = {
isRunning: vi.fn(() => false),
getWorker: vi.fn(() => handle),
};
await applyManagedEnvironments(noDb, config, {
env: {},
workerManager,
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
fireReady();
await tick();
// The rejection is swallowed into a log line; reaching this point without
// an unhandled rejection is the assertion.
expect(environments.ensureManagedSandboxEnvironment).toHaveBeenCalledTimes(1);
});
it("fails closed when no worker manager is provided", async () => {
const environments = environmentsSeam();
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const result = await applyManagedEnvironments(noDb, config, {
env: {},
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
});
it("counts a skipped entry once even when archiving its stale row fails", async () => {
const environments = environmentsSeam({
archiveManagedSandboxEnvironment: vi.fn().mockRejectedValue(new Error("db exploded")),
});
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager: runningWorkerManager(),
environments,
resolveSandboxProviderDriver: vi.fn(async () => null),
});
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.archiveManagedSandboxEnvironment).toHaveBeenCalledTimes(1);
});
it("is fail-safe per entry: an ensure failure is counted, not thrown", async () => {
const environments = environmentsSeam({
ensureManagedSandboxEnvironment: vi.fn().mockRejectedValue(new Error("db exploded")),
});
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
const result = await applyManagedEnvironments(noDb, config, {
env: {},
workerManager: runningWorkerManager(),
environments,
resolveSandboxProviderDriver: readyDriverResolver(),
});
expect(result).toMatchObject({ ensured: 0, failed: 1 });
expect(environments.archiveManagedSandboxEnvironment).not.toHaveBeenCalled();
});
it("fills the instance default with the managed sandbox under managed-sandbox-only", async () => {
const fillInstanceDefault = vi.fn(async () => ({ filled: true, previousDefaultEnvironmentId: null }));
const config = parsedConfig({
features: { enableManagedSandboxOnly: true },
environments: [{ name: "Daytona", provider: "daytona" }],
});
await applyManagedEnvironments(noDb, config, {
env: {},
workerManager: runningWorkerManager(),
fillInstanceDefault,
environments: environmentsSeam(),
resolveSandboxProviderDriver: readyDriverResolver(),
});
// The locked fill (`fillUnsetInstanceDefault`) decides whether to write.
expect(fillInstanceDefault).toHaveBeenCalledWith("env-1");
});
it("leaves the instance default alone while managed-sandbox-only is not declared", async () => {
// Without the mode, local execution is a legitimate default; provisioning
// the managed environment must not silently move runs into the sandbox.
const fillInstanceDefault = vi.fn(async () => ({ filled: true, previousDefaultEnvironmentId: null }));
const config = parsedConfig({
environments: [{ name: "Daytona", provider: "daytona" }],
});
await applyManagedEnvironments(noDb, config, {
env: {},
workerManager: runningWorkerManager(),
fillInstanceDefault,
environments: environmentsSeam(),
resolveSandboxProviderDriver: readyDriverResolver(),
});
expect(fillInstanceDefault).not.toHaveBeenCalled();
});
it("never reverts a filled default when managed-sandbox-only is not declared", async () => {
// A filled default is an ordinary set value. With the mode off, and even
// with no environments declared, boot writes nothing to the default.
const fillInstanceDefault = vi.fn(async () => ({ filled: true, previousDefaultEnvironmentId: null }));
const environments = environmentsSeam();
expect(await applyManagedEnvironments(noDb, parsedConfig({}), {
env: {},
workerManager: runningWorkerManager(),
fillInstanceDefault,
environments,
})).toBeNull();
expect(fillInstanceDefault).not.toHaveBeenCalled();
expect(environments.ensureManagedSandboxEnvironment).not.toHaveBeenCalled();
});
it("keeps the environment ensured when filling the default fails", async () => {
const result = await applyManagedEnvironments(noDb, parsedConfig({
features: { enableManagedSandboxOnly: true },
environments: [{ name: "Daytona", provider: "daytona" }],
}), {
env: {},
workerManager: runningWorkerManager(),
fillInstanceDefault: vi.fn().mockRejectedValue(new Error("db exploded")),
environments: environmentsSeam(),
resolveSandboxProviderDriver: readyDriverResolver(),
});
expect(result).toMatchObject({ ensured: 1, failed: 0 });
});
});