diff --git a/package.json b/package.json index 9f3d9e4..de10096 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agility/cli", - "version": "1.0.0-beta.13.24", + "version": "1.0.0-beta.13.26", "description": "Agility CLI for working with your content. (Public Beta)", "repository": { "type": "git", diff --git a/src/lib/pushers/container-pusher.ts b/src/lib/pushers/container-pusher.ts index 0576a95..218d901 100644 --- a/src/lib/pushers/container-pusher.ts +++ b/src/lib/pushers/container-pusher.ts @@ -1,12 +1,23 @@ import * as mgmtApi from "@agility/management-sdk"; import { ApiClient } from "@agility/management-sdk"; import { getLoggerForGuid, state } from "core/state"; +import { fileOperations } from "core"; import { ContainerMapper } from "lib/mappers/container-mapper"; import { ModelMapper } from "lib/mappers/model-mapper"; import { Logs } from "core/logs"; import { FailureDetail, PusherResult } from "types/sourceData"; import { preflightReport } from "../preflight/preflight-report"; +/** + * Persist the target container to the local disk cache (agility-files/{targetGuid}/containers/{id}.json), + * mirroring what a download-containers.ts pull would produce. Without this, ContainerMapper.getMappedEntity + * (which only reads from disk) can't distinguish a container that was just created/updated in this same run + * from one that was genuinely deleted since the last sync (PROD-2346). + */ +function cacheTargetContainer(targetGuid: string, container: mgmtApi.Container) { + new fileOperations(targetGuid).exportFiles("containers", container.contentViewID.toString(), container); +} + /** * Container pusher with enhanced version-based comparison * Uses lastModifiedDate for intelligent update decisions @@ -104,6 +115,7 @@ export async function pushContainers( if (!state.preflight) { containerMapper.addMapping(sourceContainer, targetByRef); + cacheTargetContainer(targetGuid[0], targetByRef); } logger.container.skipped(sourceContainer, "already exists on target; mapping row created", targetGuid[0]); preflightReport.record({ @@ -208,6 +220,7 @@ export async function pushContainers( } containerMapper.updateMapping(sourceContainer, updateResult, sourceMapping); + cacheTargetContainer(targetGuid[0], updateResult); successful++; } else { logger.container.error(sourceContainer, "Failed to update container", targetGuid[0]); @@ -251,6 +264,7 @@ export async function pushContainers( if (createResult) { logger.container.created(sourceContainer, "created", targetGuid[0]); containerMapper.addMapping(sourceContainer, createResult); + cacheTargetContainer(targetGuid[0], createResult); successful++; } else { logger.container.error(sourceContainer, "Failed to create container", targetGuid[0]); diff --git a/src/lib/pushers/tests/container-pusher.test.ts b/src/lib/pushers/tests/container-pusher.test.ts index ab22c5f..b57d4c1 100644 --- a/src/lib/pushers/tests/container-pusher.test.ts +++ b/src/lib/pushers/tests/container-pusher.test.ts @@ -233,6 +233,112 @@ describe("pushContainers — adopt existing target container by referenceName (P }); }); +// ─── pushContainers — PROD-2346 local cache write ───────────────────────────── + +describe("pushContainers — writes local container cache file (PROD-2346)", () => { + function cachedContainerPath(guid: string, contentViewID: number): string { + return path.join(tmpDir, guid, "containers", `${contentViewID}.json`); + } + + it("writes the target container to disk after a successful create", async () => { + const created = makeContainer({ + contentViewID: 501, + contentDefinitionID: 1, + lastModifiedDate: "01/01/2024 10:00AM", + }); + const saveContainer = jest.fn().mockResolvedValue(created); + state.cachedApiClient = { containerMethods: { saveContainer } } as any; + + const { pushContainers } = await import("../container-pusher"); + + const sourceContainer = makeContainer({ contentDefinitionID: 1 }); + + const result = await pushContainers([sourceContainer], []); + expect(result.successful).toBe(1); + + const cachePath = cachedContainerPath("tgt-cont-u", 501); + expect(fs.existsSync(cachePath)).toBe(true); + expect(JSON.parse(fs.readFileSync(cachePath, "utf8"))).toMatchObject({ + contentViewID: 501, + referenceName: created.referenceName, + lastModifiedDate: "01/01/2024 10:00AM", + }); + }); + + it("writes the target container to disk after a successful update", async () => { + const referenceName = "UpdateCacheTest"; + + // First run: create the container, establishing the mapping and initial cache file. + const created = makeContainer({ + contentViewID: 502, + contentDefinitionID: 1, + referenceName, + lastModifiedDate: "01/01/2024 10:00AM", + }); + const saveContainer = jest.fn().mockResolvedValue(created); + state.cachedApiClient = { containerMethods: { saveContainer } } as any; + + const { pushContainers } = await import("../container-pusher"); + + const sourceV1 = makeContainer({ + contentViewID: 900, + contentDefinitionID: 1, + referenceName, + lastModifiedDate: "01/01/2024 10:00AM", + }); + await pushContainers([sourceV1], []); + + // Second run: source changed, target unchanged → update path. + const updated = makeContainer({ + contentViewID: 502, + contentDefinitionID: 1, + referenceName, + lastModifiedDate: "06/01/2025 10:00AM", + }); + saveContainer.mockResolvedValue(updated); + + const sourceV2 = { ...sourceV1, lastModifiedDate: "06/01/2025 10:00AM" }; + const targetUnchanged = { ...created }; + + const result = await pushContainers([sourceV2], [targetUnchanged]); + expect(result.successful).toBe(1); + + const cachePath = cachedContainerPath("tgt-cont-u", 502); + expect(fs.existsSync(cachePath)).toBe(true); + expect(JSON.parse(fs.readFileSync(cachePath, "utf8"))).toMatchObject({ + contentViewID: 502, + referenceName, + lastModifiedDate: "06/01/2025 10:00AM", + }); + }); + + it("writes the target container to disk after adopting by referenceName (PROD-2307)", async () => { + const saveContainer = jest.fn(); + state.cachedApiClient = { containerMethods: { saveContainer } } as any; + + const { pushContainers } = await import("../container-pusher"); + + const source = makeContainer({ referenceName: "AdoptCacheTest", contentDefinitionID: 500 }); + const target = makeContainer({ + contentViewID: 503, + referenceName: "AdoptCacheTest", + contentDefinitionID: 500, + lastModifiedDate: "01/01/2024 10:00AM", + }); + + const result = await pushContainers([source], [target]); + expect(result.skipped).toBe(1); + expect(saveContainer).not.toHaveBeenCalled(); + + const cachePath = cachedContainerPath("tgt-cont-u", 503); + expect(fs.existsSync(cachePath)).toBe(true); + expect(JSON.parse(fs.readFileSync(cachePath, "utf8"))).toMatchObject({ + contentViewID: 503, + referenceName: "AdoptCacheTest", + }); + }); +}); + // ─── pushContainers — result shape ──────────────────────────────────────────── describe("pushContainers — result shape", () => {