Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
14 changes: 14 additions & 0 deletions src/lib/pushers/container-pusher.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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]);
Expand Down
106 changes: 106 additions & 0 deletions src/lib/pushers/tests/container-pusher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading