mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-15 18:56:05 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A project uses one workspace storage owner. The user chooses it when they create the project. > - The **Available in** selector shows the storage owners that the user can choose. > - Sometimes the default environment cannot be used, and only one storage owner is available. In that case, the dialog shows a selector with only one option. The user must select it before they can create the project. > - A selector with only one option is not a real choice. > - This pull request uses the only available storage owner as the default. The selector appears only when the user has two or more options. > - The benefit is that project creation asks for a choice only when there is one. ## Linked Issues or Issue Description Refs #20835 **What happened?** An example is an instance in managed-sandbox-only mode that has the agent-computer plugin but no managed sandbox. The local environment is hidden, and the managed-only rule finds no default. The project-environments endpoint returns one storage group and an error. The new project dialog shows **Available in** with only one option and disables **Create project** until the user selects it. An API request without `environmentId` fails with `project_environment_unavailable`. **What did you expect?** When only one storage owner is available, the project uses it. No selector appears. **Steps to reproduce** 1. Turn on `enableManagedSandboxOnly`. Install the agent-computer plugin. Do not add a sandbox environment. 2. Open **Create project**. 3. See an **Available in** selector with one option, and a disabled **Create project** button. ## What Changed - `projectEnvironmentOptions`: when the configured default is unavailable and only one storage group exists, the first environment of that group becomes `defaultEnvironmentId`, and `error` is null. When two or more groups exist, an unavailable default still produces the error. The server does not guess. - Project create uses the same default, so an API request without `environmentId` now succeeds in the single-group case. - `NewProjectDialog` shows the selector only when `groups.length > 1`. - Tests: - A new server test covers managed-only mode with only the agent-computer environment. - A new server test covers an unavailable default with two storage groups (the server still refuses to guess). - The archived-default test now expects the server to use the only remaining storage. - A new UI test checks that the dialog never shows one option. - Docs: `doc/project-environment-compatibility.md` and `docs/api/goals-and-projects.md`. ## Verification - `pnpm vitest run src/__tests__/project-environments.test.ts` in `server/`: 16 passed. - `pnpm vitest run src/components/NewProjectDialog` in `ui/`: 12 passed. - `tsc --noEmit` in `ui/`: clean. - Server `tsc` reports 9 errors on this machine. None are in the changed files. They are in `packages/adapter-utils` and a native-runtime test, because local dependencies are stale. CI typechecks with a fresh install. ## Risks - Low risk. The change applies only when the default is unavailable and exactly one storage group exists. In that case, the server picks the group that the user would have had to choose anyway. Run placement does not depend on the project, so this does not change where runs execute. - No migration. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use (shell, file edits, 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>
273 lines
20 KiB
TypeScript
273 lines
20 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
import { eq, inArray, sql } from "drizzle-orm";
|
|
import { agents, authUsers, companyMemberships, builtInManagedResources, companies, createDb, environments, instanceSettings, plugins, userCompanyPreferences } from "@paperclipai/db";
|
|
import { projectEnvironmentOptions, withDefaultProjectStorage } from "../services/project-environments.js";
|
|
import { assertProjectStorageEnvironment, projectStorageForEnvironment } from "../services/project-workspace-provider.js";
|
|
import { issueService } from "../services/issues.js";
|
|
import { instanceSettingsService } from "../services/instance-settings.js";
|
|
import { applyDefaultIsolatedExecutionWorkspacePolicy, gateProjectExecutionWorkspacePolicy, parseProjectExecutionWorkspacePolicy, resolveExecutionWorkspaceMode, buildExecutionWorkspaceAdapterConfig, resolveEffectiveWorkspaceStrategyType } from "../services/execution-workspace-policy.js";
|
|
import { projectService } from "../services/projects.js";
|
|
import { preparePluginWorkspace } from "../services/plugin-workspaces.js";
|
|
import { projectRoutes } from "../routes/projects.js";
|
|
import { errorHandler } from "../middleware/index.js";
|
|
import { createPluginWorkspaceFixture } from "./helpers/plugin-workspaces.js";
|
|
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
|
|
|
|
const support = await getEmbeddedPostgresTestSupport();
|
|
describe.skipIf(!support.supported)("project environment compatibility", () => {
|
|
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
|
|
let db: ReturnType<typeof createDb>;
|
|
beforeAll(async () => {
|
|
database = await startEmbeddedPostgresTestDatabase("project-environments");
|
|
db = createDb(database.connectionString);
|
|
}, 120_000);
|
|
afterAll(async () => { await db?.$client.end(); await database?.cleanup(); });
|
|
beforeEach(async () => { await db.execute(sql`truncate companies, plugins, environments, instance_settings cascade`); });
|
|
|
|
async function fixture() {
|
|
const f = await createPluginWorkspaceFixture(db);
|
|
const [plugin] = await db.select().from(plugins).where(eq(plugins.id, f.pluginId));
|
|
plugin.manifestJson = { ...plugin.manifestJson,
|
|
capabilities: [...plugin.manifestJson.capabilities, "environment.drivers.register"],
|
|
environmentDrivers: [{ driverKey: "tasks", supportsTasks: true, displayName: "Computers", configSchema: { type: "object", properties: {} } }],
|
|
workspaceProvider: { ...plugin.manifestJson.workspaceProvider!, defaultForProjects: { mode: "isolated_workspace", strategy: "git_worktree" } },
|
|
};
|
|
await db.update(plugins).set({ manifestJson: plugin.manifestJson }).where(eq(plugins.id, plugin.id));
|
|
const [computer, local, sandbox, ssh] = await db.insert(environments).values([
|
|
{ name: "Computers", driver: "plugin", config: { pluginKey: plugin.pluginKey, driverKey: "tasks", driverConfig: {} } },
|
|
{ name: "Local", driver: "local" },
|
|
{ name: "Sandbox", driver: "sandbox", config: { provider: "daytona" } },
|
|
{ name: "Build host", driver: "ssh", config: { host: "build.example.com" } },
|
|
]).returning();
|
|
await db.insert(instanceSettings).values({ defaultEnvironmentId: sandbox.id });
|
|
return { ...f, plugin, computer, local, sandbox, ssh };
|
|
}
|
|
|
|
it("groups by repository storage, across driver types and provider configurations", async () => {
|
|
const f = await fixture();
|
|
const [otherComputer] = await db.insert(environments).values({ name: "Other computers", driver: "plugin",
|
|
config: { ...f.computer.config, driverConfig: { profile: "other" } } }).returning();
|
|
const [otherSandbox] = await db.insert(environments).values({ name: "Other sandbox", driver: "sandbox", config: { provider: "modal" } }).returning();
|
|
const options = await projectEnvironmentOptions(db, f.companyId);
|
|
expect(options.error).toBeNull();
|
|
expect(options.defaultEnvironmentId).toBe(f.sandbox.id);
|
|
expect(options.groups).toHaveLength(2);
|
|
expect(options.groups.find(group => group.storageProviderPluginId === null)?.environments.map(value => value.id).sort())
|
|
.toEqual([f.local.id, f.sandbox.id, f.ssh.id, otherSandbox.id].sort());
|
|
expect(options.groups.find(group => group.storageProviderPluginId === f.pluginId)?.environments.map(value => value.id).sort())
|
|
.toEqual([f.computer.id, otherComputer.id].sort());
|
|
expect(JSON.stringify(options)).not.toContain("driverConfig");
|
|
expect(JSON.stringify(options)).not.toContain("build.example.com");
|
|
});
|
|
|
|
it("does not equate two plugin drivers with different storage owners", async () => {
|
|
const f = await fixture();
|
|
const other = await createPluginWorkspaceFixture(db, f.companyId);
|
|
await db.update(plugins).set({ manifestJson: { ...f.plugin.manifestJson, id: `test.${other.pluginId}` } }).where(eq(plugins.id, other.pluginId));
|
|
await db.insert(environments).values({ name: "Separate computers", driver: "plugin", config: { pluginKey: `test.${other.pluginId}`, driverKey: "tasks" } });
|
|
expect((await projectEnvironmentOptions(db, f.companyId)).groups).toHaveLength(3);
|
|
expect(projectStorageForEnvironment({ driver: "plugin", config: { pluginKey: f.plugin.pluginKey, driverKey: "unknown" } }, [f.plugin])).toBeNull();
|
|
expect(projectStorageForEnvironment({ driver: "sandbox", config: { provider: "fake" } }, [f.plugin])).toBeNull();
|
|
});
|
|
|
|
it("uses the creating agent, the user's primary agent, then the instance default", async () => {
|
|
const f = await fixture();
|
|
await db.update(agents).set({ defaultEnvironmentId: f.computer.id }).where(eq(agents.id, f.input.agentId));
|
|
await db.insert(userCompanyPreferences).values({ companyId: f.companyId, userId: "person", primaryAgentId: f.input.agentId });
|
|
expect((await projectEnvironmentOptions(db, f.companyId)).defaultEnvironmentId).toBe(f.sandbox.id);
|
|
expect((await projectEnvironmentOptions(db, f.companyId, { userId: "person" })).defaultEnvironmentId).toBe(f.computer.id);
|
|
expect((await projectEnvironmentOptions(db, f.companyId, { agentId: f.input.agentId })).defaultEnvironmentId).toBe(f.computer.id);
|
|
expect(await withDefaultProjectStorage(db, f.companyId, null)).toBeNull();
|
|
expect(await withDefaultProjectStorage(db, f.companyId, null, { userId: "person" })).toMatchObject({ storageProviderPluginId: f.pluginId });
|
|
await db.update(agents).set({ defaultEnvironmentId: null }).where(eq(agents.id, f.input.agentId));
|
|
expect((await projectEnvironmentOptions(db, f.companyId, { userId: "person" })).defaultEnvironmentId).toBe(f.sandbox.id);
|
|
await db.update(instanceSettings).set({ defaultEnvironmentId: null });
|
|
expect((await projectEnvironmentOptions(db, f.companyId)).defaultEnvironmentId).toBe(f.local.id);
|
|
});
|
|
|
|
it("pins the selected storage, preserves explicit API choices, and rejects conflicting choices", async () => {
|
|
const f = await fixture();
|
|
const svc = projectService(db);
|
|
const sandboxProject = await svc.createWithRepositories(f.companyId, { name: "Sandbox project" }, [
|
|
{ id: "42", fullName: "example/repo", url: "https://github.com/example/repo", connections: [] },
|
|
]);
|
|
expect(sandboxProject.workspaceProvider?.pluginId).toBeNull();
|
|
const computerProject = await svc.create(f.companyId, { name: "Computer project" }, { environmentId: f.computer.id });
|
|
expect(computerProject.workspaceProvider?.pluginId).toBe(f.pluginId);
|
|
const explicit = { enabled: true, storageProviderPluginId: null };
|
|
expect(await withDefaultProjectStorage(db, f.companyId, explicit)).toEqual(explicit);
|
|
await expect(withDefaultProjectStorage(db, f.companyId, explicit, { environmentId: f.computer.id })).rejects.toMatchObject({ status: 409 });
|
|
await db.update(instanceSettings).set({ defaultEnvironmentId: f.computer.id });
|
|
expect((await svc.getById(sandboxProject.id))?.workspaceProvider?.pluginId).toBeNull();
|
|
});
|
|
|
|
it("saves storage, not a run environment, and keeps that storage after creation", async () => {
|
|
const f = await fixture();
|
|
const svc = projectService(db);
|
|
const core = await svc.create(f.companyId, { name: "Core project" }, { environmentId: f.ssh.id, expectedStorageProviderPluginId: null });
|
|
const managed = await svc.create(f.companyId, { name: "Managed project" }, { environmentId: f.computer.id });
|
|
expect(core).not.toHaveProperty("environmentId");
|
|
expect(core).not.toHaveProperty("legacyEnvironmentId");
|
|
expect(core.workspaceProvider?.pluginId).toBeNull();
|
|
expect(managed.workspaceProvider?.pluginId).toBe(f.pluginId);
|
|
await expect(svc.update(core.id, { executionWorkspacePolicy: { enabled: true, storageProviderPluginId: f.pluginId } }))
|
|
.rejects.toMatchObject({ status: 409, details: { code: "project_storage_locked" } });
|
|
await expect(svc.update(managed.id, { executionWorkspacePolicy: { enabled: true, storageProviderPluginId: null } }))
|
|
.rejects.toMatchObject({ status: 409, details: { code: "project_storage_locked" } });
|
|
expect((await svc.getById(core.id))?.workspaceProvider?.pluginId).toBeNull();
|
|
expect((await svc.getById(managed.id))?.workspaceProvider?.pluginId).toBe(f.pluginId);
|
|
});
|
|
|
|
it("drops a legacy policy environment instead of saving it", async () => {
|
|
const f = await fixture();
|
|
const svc = projectService(db);
|
|
const created = await svc.create(f.companyId, { name: "Legacy create", executionWorkspacePolicy: { enabled: false, environmentId: f.ssh.id } });
|
|
expect(created.executionWorkspacePolicy).toEqual({ enabled: false });
|
|
const updated = await svc.update(created.id, { executionWorkspacePolicy: { enabled: false, environmentId: f.local.id } });
|
|
expect(updated?.executionWorkspacePolicy).toEqual({ enabled: false });
|
|
expect(updated).not.toHaveProperty("environmentId");
|
|
});
|
|
|
|
it("preserves inherited task isolation when checking the observed core storage owner", async () => {
|
|
const f = await fixture();
|
|
const { experimental: settings } = await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true, enableIsolatedWorkspacesByDefault: true });
|
|
const project = await projectService(db).createWithRepositories(f.companyId, { name: "Inherited isolation" }, [
|
|
{ id: "42", fullName: "example/repo", url: "https://github.com/example/repo", connections: [] },
|
|
], { environmentId: f.sandbox.id, expectedStorageProviderPluginId: null });
|
|
expect(project.executionWorkspacePolicy).toBeNull();
|
|
const projectPolicy = applyDefaultIsolatedExecutionWorkspacePolicy({
|
|
projectPolicy: gateProjectExecutionWorkspacePolicy(parseProjectExecutionWorkspacePolicy(project.executionWorkspacePolicy), settings.enableIsolatedWorkspaces),
|
|
defaultIsolatedWorkspacesEnabled: settings.enableIsolatedWorkspacesByDefault,
|
|
hasProjectWorkspace: project.workspaces.length > 0,
|
|
});
|
|
const mode = resolveExecutionWorkspaceMode({ projectPolicy, issueSettings: null, legacyUseProjectWorkspace: null });
|
|
const config = buildExecutionWorkspaceAdapterConfig({ agentConfig: {}, projectPolicy, issueSettings: null, mode, legacyUseProjectWorkspace: null });
|
|
expect(mode).toBe("isolated_workspace");
|
|
expect(resolveEffectiveWorkspaceStrategyType(mode, config)).toBe("git_worktree");
|
|
});
|
|
|
|
it("fails before a provider operation when managed storage cannot serve the selected environment", async () => {
|
|
const f = await fixture();
|
|
expect(() => assertProjectStorageEnvironment(f.plugin, { ...f.sandbox, driver: "sandbox" })).toThrow('does not support the environment "Sandbox"');
|
|
await expect(preparePluginWorkspace(db, f.workers, { ...f.input, environment: { ...f.sandbox, driver: "sandbox" } }))
|
|
.rejects.toMatchObject({ status: 409, details: { code: "project_environment_incompatible", retryable: false } });
|
|
expect(f.calls).toHaveLength(0);
|
|
expect(() => assertProjectStorageEnvironment(f.plugin, { ...f.computer, driver: "plugin" })).not.toThrow();
|
|
});
|
|
|
|
it("excludes archived, company-inaccessible, and disabled plugin environments", async () => {
|
|
const f = await fixture();
|
|
const [otherCompany] = await db.insert(companies).values({ name: "Other", issuePrefix: "OTHER" }).returning();
|
|
await db.insert(builtInManagedResources).values({ companyId: otherCompany.id, bundleKey: "test", resourceKind: "environment",
|
|
resourceKey: "environment", resourceId: f.ssh.id, stockVersion: "1", stockHash: "test" });
|
|
await db.update(environments).set({ status: "archived" }).where(eq(environments.id, f.local.id));
|
|
await db.update(plugins).set({ status: "disabled" }).where(eq(plugins.id, f.pluginId));
|
|
const options = await projectEnvironmentOptions(db, f.companyId);
|
|
expect(options.groups.flatMap(group => group.environments).map(value => value.id)).toEqual([f.sandbox.id]);
|
|
for (const environmentId of [f.local.id, f.ssh.id, f.computer.id, randomUUID()]) {
|
|
await expect(withDefaultProjectStorage(db, f.companyId, null, { environmentId })).rejects.toMatchObject({ status: 409 });
|
|
}
|
|
});
|
|
|
|
it("keeps a cold worker's storage identity, and uses the only storage left after its default is archived", async () => {
|
|
const f = await fixture();
|
|
await db.update(instanceSettings).set({ defaultEnvironmentId: f.computer.id });
|
|
await db.update(plugins).set({ status: "error" }).where(eq(plugins.id, f.pluginId));
|
|
expect(await withDefaultProjectStorage(db, f.companyId, null)).toMatchObject({ storageProviderPluginId: f.pluginId });
|
|
await db.update(environments).set({ status: "archived" }).where(eq(environments.id, f.computer.id));
|
|
expect(await projectEnvironmentOptions(db, f.companyId)).toMatchObject({ defaultEnvironmentId: f.ssh.id, error: null });
|
|
expect(await withDefaultProjectStorage(db, f.companyId, null)).toBeNull();
|
|
});
|
|
|
|
it("refuses to guess storage when the default is unavailable and several storage options remain", async () => {
|
|
const f = await fixture();
|
|
const [fake] = await db.insert(environments).values({ name: "Fake", driver: "sandbox", config: { provider: "fake" } }).returning();
|
|
await db.update(instanceSettings).set({ defaultEnvironmentId: fake.id });
|
|
const options = await projectEnvironmentOptions(db, f.companyId);
|
|
expect(options.groups).toHaveLength(2);
|
|
expect(options.defaultEnvironmentId).toBeNull();
|
|
expect(options.error).toBeTruthy();
|
|
await expect(withDefaultProjectStorage(db, f.companyId, null)).rejects.toMatchObject({ status: 409 });
|
|
});
|
|
|
|
it("uses agent computer storage without a choice when managed-only leaves no other environment", async () => {
|
|
const f = await fixture();
|
|
await db.update(environments).set({ status: "archived" }).where(inArray(environments.id, [f.sandbox.id, f.ssh.id]));
|
|
await db.update(instanceSettings).set({ experimental: { enableManagedSandboxOnly: true } });
|
|
const options = await projectEnvironmentOptions(db, f.companyId);
|
|
expect(options).toEqual({
|
|
groups: [{ storageProviderPluginId: f.pluginId, environments: [{ id: f.computer.id, name: f.computer.name }] }],
|
|
defaultEnvironmentId: f.computer.id,
|
|
error: null,
|
|
});
|
|
expect(await withDefaultProjectStorage(db, f.companyId, null)).toMatchObject({ storageProviderPluginId: f.pluginId });
|
|
});
|
|
|
|
it.each(["instance", "primary"] as const)("creates repository-free private tasks despite an archived %s default", async source => {
|
|
const f = await fixture();
|
|
const userId = randomUUID();
|
|
const now = new Date();
|
|
await db.insert(authUsers).values({ id: userId, name: "Person", email: `${userId}@example.com`, createdAt: now, updatedAt: now });
|
|
await db.insert(companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: userId,
|
|
status: "active", membershipRole: "operator" });
|
|
await db.update(environments).set({ status: "archived" }).where(eq(environments.id, f.computer.id));
|
|
if (source === "instance") await db.update(instanceSettings).set({ defaultEnvironmentId: f.computer.id });
|
|
else {
|
|
await db.update(agents).set({ defaultEnvironmentId: f.computer.id }).where(eq(agents.id, f.input.agentId));
|
|
await db.insert(userCompanyPreferences).values({ companyId: f.companyId, userId, primaryAgentId: f.input.agentId });
|
|
}
|
|
const task = await issueService(db).create(f.companyId, { title: "Private note", visibility: "private", createdByUserId: userId });
|
|
expect(task.visibility).toBe("private");
|
|
const project = await projectService(db).getById(task.projectId!);
|
|
expect(project?.name).toBe("My private tasks");
|
|
expect(project?.executionWorkspacePolicy).toBeNull();
|
|
expect(project?.workspaces).toEqual([]);
|
|
});
|
|
|
|
it("honors managed-only and forced Kubernetes execution policy", async () => {
|
|
const f = await fixture();
|
|
await db.update(environments).set({ metadata: { managedByPaperclip: true, managedKubernetesSandbox: true } }).where(eq(environments.id, f.sandbox.id));
|
|
await db.update(instanceSettings).set({ defaultEnvironmentId: f.local.id, experimental: { enableManagedSandboxOnly: true } });
|
|
const managed = await projectEnvironmentOptions(db, f.companyId);
|
|
expect(managed.defaultEnvironmentId).toBe(f.sandbox.id);
|
|
expect(managed.groups.flatMap(group => group.environments).map(value => value.id)).not.toContain(f.local.id);
|
|
await db.update(instanceSettings).set({ general: { executionMode: "kubernetes" } });
|
|
const forced = await projectEnvironmentOptions(db, f.companyId);
|
|
expect(forced.groups).toEqual([{ storageProviderPluginId: null, environments: [{ id: f.sandbox.id, name: f.sandbox.name }] }]);
|
|
await expect(withDefaultProjectStorage(db, f.companyId, null, { environmentId: f.computer.id })).rejects.toMatchObject({ status: 409 });
|
|
});
|
|
|
|
it("exposes safe groups and saves a validated choice through the normal API", async () => {
|
|
const f = await fixture();
|
|
function app(actor: Express.Request["actor"]) {
|
|
const result = express(); result.use(express.json());
|
|
result.use((req, _res, next) => { req.actor = actor; next(); });
|
|
result.use("/api", projectRoutes(db)); result.use(errorHandler); return result;
|
|
}
|
|
const board = app({ type: "board", userId: "person", source: "session", companyIds: [f.companyId] });
|
|
const url = `/api/companies/${f.companyId}`;
|
|
const options = await request(board).get(`${url}/project-environments`);
|
|
expect(options.status).toBe(200);
|
|
expect(options.body.groups).toHaveLength(2);
|
|
const created = await request(board).post(`${url}/projects`).send({ name: "Selected", environmentId: f.computer.id });
|
|
expect(created.status).toBe(201);
|
|
expect(created.body.executionWorkspacePolicy).toMatchObject({ storageProviderPluginId: f.pluginId });
|
|
expect(created.body).not.toHaveProperty("environmentId");
|
|
const denied = app({ type: "agent", agentId: f.input.agentId, companyId: randomUUID() });
|
|
expect((await request(denied).get(`${url}/project-environments`)).status).toBe(403);
|
|
expect((await request(app({ type: "none" })).get(`${url}/project-environments`)).status).toBe(401);
|
|
const agent = app({ type: "agent", agentId: f.input.agentId, companyId: f.companyId });
|
|
expect((await request(agent).get(`${url}/project-environments`)).status).toBe(200);
|
|
await db.update(environments).set({ driver: "plugin", config: f.computer.config }).where(eq(environments.id, f.ssh.id));
|
|
const changed = await request(board).post(`${url}/projects`).send({ name: "Changed storage", environmentId: f.ssh.id,
|
|
expectedStorageProviderPluginId: null });
|
|
expect(changed.status).toBe(409);
|
|
expect(changed.body.details.code).toBe("project_environment_incompatible");
|
|
const invalid = await request(board).post(`${url}/projects`).send({ name: "Invalid", environmentId: randomUUID() });
|
|
expect(invalid.status).toBe(409);
|
|
});
|
|
});
|