fix(server): reactivate drifted managed sandboxes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-06 14:40:38 +00:00
1 parent d6167516db
commit 8d061f180c
2 files changed
+148 -25

No files matched your search

@@ -973,6 +973,93 @@ describeEmbeddedPostgres("environmentService leases", () => {
expect(restored.status).toBe("active");
});
it("reactivates a reconciler-archived row without replacing operator drift", async () => {
const companyId = await seedCompany();
const created = await svc.ensureManagedSandboxEnvironment({
companyId,
name: "Daytona",
description: "Managed stock",
provider: "daytona",
config: { target: "us" },
stockVersion: "v1",
});
await db
.update(environments)
.set({
description: "Operator description",
config: { provider: "daytona", target: "operator" },
})
.where(eq(environments.id, created.environment.id));
expect((await svc.archiveManagedSandboxEnvironment({ provider: "daytona" }))?.status)
.toBe("archived");
const [archivedBinding] = await db
.select()
.from(builtInManagedResources)
.where(eq(builtInManagedResources.companyId, companyId));
expect(archivedBinding?.defaultsJson).toMatchObject({
description: "Managed stock",
status: "archived",
});
const restored = await svc.ensureManagedSandboxEnvironment({
companyId,
name: "Daytona v2",
description: "Managed stock v2",
provider: "daytona",
config: { target: "eu" },
stockVersion: "v2",
});
expect(restored).toMatchObject({
action: "skipped",
stockStatus: "operator_modified",
updateAvailable: true,
environment: {
status: "active",
name: "Daytona",
description: "Operator description",
config: { provider: "daytona", target: "operator" },
},
});
const [reactivatedBinding] = await db
.select()
.from(builtInManagedResources)
.where(eq(builtInManagedResources.companyId, companyId));
expect(reactivatedBinding?.defaultsJson).toMatchObject({
description: "Managed stock",
status: "active",
});
expect(reactivatedBinding?.stockVersion).toBe("v1");
expect(restored.stockHash).not.toBe(reactivatedBinding?.stockHash);
});
it("preserves an operator-archived managed row", async () => {
const companyId = await seedCompany();
const created = await svc.ensureManagedSandboxEnvironment({
companyId,
name: "Daytona",
provider: "daytona",
config: { target: "us" },
});
await db
.update(environments)
.set({ status: "archived" })
.where(eq(environments.id, created.environment.id));
const reconciled = await svc.ensureManagedSandboxEnvironment({
companyId,
name: "Daytona",
provider: "daytona",
config: { target: "us" },
});
expect(reconciled).toMatchObject({
action: "skipped",
stockStatus: "operator_modified",
updateAvailable: true,
environment: { status: "archived" },
});
});
it("preserves an existing unmanaged sandbox row holding the desired name", async () => {
const companyId = await seedCompany();
const handMade = await svc.create({
+61 -25
View File
@@ -320,6 +320,7 @@ export function environmentService(db: Db) {
let activityCompanyIds: string[] = [];
let trackingInitialized = false;
let providerReactivated = false;
const reconciliation = await db.transaction(
async (tx): Promise<ManagedSandboxEnvironmentReconcileResult> => {
const companyIds = input.companyId
@@ -510,11 +511,59 @@ export function environmentService(db: Db) {
if (stockStatus === "operator_modified") {
const baseline = matchingBindings[0];
let baselineDefaults = baseline?.defaultsJson ?? desiredStock;
let baselineHash = baseline?.stockHash ?? latestStockHash;
// Provider unavailability is an operational state transition, not
// an operator edit. If the binding records that Paperclip archived
// this row, restore only its availability status. Keep every other
// operator-modified field intact and leave the stock update pending.
// A manually archived row still has an active binding baseline, so
// it remains operator_modified and is not reactivated here.
const archivedByReconciler = row.status === "archived" && matchingBindings.some(
(binding) => (cloneRecord(binding.defaultsJson) ?? {}).status === "archived",
);
if (archivedByReconciler) {
const reactivated = await tx
.update(environments)
.set({ status: "active", updatedAt: new Date() })
.where(and(eq(environments.id, row.id), eq(environments.status, "archived")))
.returning()
.then((rows) => rows[0] ?? null);
if (!reactivated) {
throw new Error("Managed sandbox environment changed during reactivation");
}
row = reactivated;
providerReactivated = true;
for (const binding of matchingBindings) {
const reactivatedDefaults = {
...cloneRecord(binding.defaultsJson),
status: "active",
};
const reactivatedHash = stockHash(reactivatedDefaults);
await tx
.update(builtInManagedResources)
.set({
stockHash: reactivatedHash,
defaultsJson: reactivatedDefaults,
updatedAt: new Date(),
})
.where(and(
eq(builtInManagedResources.id, binding.id),
eq(builtInManagedResources.resourceId, row.id),
));
if (binding.id === baseline?.id) {
baselineDefaults = reactivatedDefaults;
baselineHash = reactivatedHash;
}
}
}
await writeBindings(
row.id,
baseline?.stockVersion ?? input.stockVersion ?? MANAGED_ENVIRONMENT_STOCK_VERSION,
baseline?.stockHash ?? latestStockHash,
baseline?.defaultsJson ?? desiredStock,
baselineHash,
baselineDefaults,
false,
);
return {
@@ -594,6 +643,7 @@ export function environmentService(db: Db) {
stockStatus: reconciliation.stockStatus,
updateAvailable: reconciliation.updateAvailable,
stockHash: reconciliation.stockHash,
providerReactivated,
},
})));
}
@@ -651,17 +701,6 @@ export function environmentService(db: Db) {
eq(builtInManagedResources.resourceId, existing.id),
))
: [];
const keys = managedMetadataKeys({}, bindings);
const currentStock = managedEnvironmentStock({
name: existing.name,
description: existing.description ?? null,
config: existing.config,
metadata: existing.metadata,
status: existing.status,
}, keys);
const currentHash = stockHash(currentStock);
const stockControlled = bindings.some((binding) => binding.stockHash === currentHash);
const archived = await tx
.update(environments)
.set({ status: "archived", updatedAt: new Date() })
@@ -670,17 +709,14 @@ export function environmentService(db: Db) {
.then((rows) => rows[0] ?? null);
if (!archived) return null;
// Archival is a Paperclip-owned availability transition. Advance the
// installed-stock baseline only when the row was stock-controlled;
// operator-modified fields must never be laundered into stock.
if (stockControlled && bindings.length > 0) {
const archivedStock = managedEnvironmentStock({
name: archived.name,
description: archived.description ?? null,
config: archived.config,
metadata: archived.metadata,
status: archived.status,
}, keys);
// Archival is a Paperclip-owned availability transition. Record only
// that status change in each installed baseline. Deriving the new hash
// from defaultsJson keeps operator-modified row fields out of stock.
for (const binding of bindings) {
const archivedStock = {
...cloneRecord(binding.defaultsJson),
status: "archived",
};
await tx
.update(builtInManagedResources)
.set({
@@ -689,7 +725,7 @@ export function environmentService(db: Db) {
updatedAt: new Date(),
})
.where(and(
inArray(builtInManagedResources.id, bindings.map((binding) => binding.id)),
eq(builtInManagedResources.id, binding.id),
eq(builtInManagedResources.resourceId, existing.id),
));
}