Disable imported timer heartbeats

Prevent company imports from re-enabling scheduler heartbeats on imported agents and cover both new-company and existing-company import flows in portability tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-23 16:30:28 -05:00
parent dcead97650
commit f9927bdaaa
2 changed files with 69 additions and 1 deletions

View File

@@ -1832,6 +1832,61 @@ describe("company portability", () => {
});
});
it("disables timer heartbeats on imported agents", async () => {
const portability = companyPortabilityService({} as any);
companySvc.create.mockResolvedValue({
id: "company-imported",
name: "Imported Paperclip",
});
agentSvc.create.mockImplementation(async (_companyId: string, input: Record<string, unknown>) => ({
id: `agent-${String(input.name).toLowerCase()}`,
name: input.name,
adapterConfig: input.adapterConfig,
runtimeConfig: input.runtimeConfig,
}));
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
agentSvc.list.mockResolvedValue([]);
await portability.importBundle({
source: {
type: "inline",
rootPath: exported.rootPath,
files: exported.files,
},
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
target: {
mode: "new_company",
newCompanyName: "Imported Paperclip",
},
agents: "all",
collisionStrategy: "rename",
}, "user-1");
const createdClaude = agentSvc.create.mock.calls.find(([, input]) => input.name === "ClaudeCoder");
expect(createdClaude?.[1]).toMatchObject({
runtimeConfig: {
heartbeat: {
enabled: false,
},
},
});
});
it("imports only selected files and leaves unchecked company metadata alone", async () => {
const portability = companyPortabilityService({} as any);
@@ -1902,6 +1957,11 @@ describe("company portability", () => {
expect(agentSvc.create).toHaveBeenCalledTimes(1);
expect(agentSvc.create).toHaveBeenCalledWith("company-1", expect.objectContaining({
name: "CMO",
runtimeConfig: {
heartbeat: {
enabled: false,
},
},
}));
expect(result.company.action).toBe("unchanged");
expect(result.agents).toEqual([

View File

@@ -619,6 +619,14 @@ function clonePortableRecord(value: unknown) {
return structuredClone(value) as Record<string, unknown>;
}
function disableImportedTimerHeartbeat(runtimeConfig: unknown) {
const next = clonePortableRecord(runtimeConfig) ?? {};
const heartbeat = isPlainRecord(next.heartbeat) ? { ...next.heartbeat } : {};
heartbeat.enabled = false;
next.heartbeat = heartbeat;
return next;
}
function normalizePortableProjectWorkspaceExtension(
workspaceKey: string,
value: unknown,
@@ -3853,7 +3861,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
reportsTo: null,
adapterType: effectiveAdapterType,
adapterConfig: adapterConfigWithSkills,
runtimeConfig: manifestAgent.runtimeConfig,
runtimeConfig: disableImportedTimerHeartbeat(manifestAgent.runtimeConfig),
budgetMonthlyCents: manifestAgent.budgetMonthlyCents,
permissions: manifestAgent.permissions,
metadata: manifestAgent.metadata,