From fd9671aaa5eaef3472f6138d0a9151f410aefb1b Mon Sep 17 00:00:00 2001 From: Jules Exel Date: Tue, 23 Jun 2026 15:52:57 -0400 Subject: [PATCH 1/9] Update asset reference extractor to include custom CDNs not just hard coded agility --- package.json | 2 +- src/lib/assets/asset-reference-extractor.ts | 20 +++-- .../tests/asset-reference-extractor.test.ts | 77 +++++++++++++++++++ src/types/syncAnalysis.ts | 4 +- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 8b302381..02b393e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agility/cli", - "version": "1.0.0-beta.13.16", + "version": "1.0.0-beta.13.18", "description": "Agility CLI for working with your content. (Public Beta)", "repository": { "type": "git", diff --git a/src/lib/assets/asset-reference-extractor.ts b/src/lib/assets/asset-reference-extractor.ts index 865e1a70..902e6c12 100644 --- a/src/lib/assets/asset-reference-extractor.ts +++ b/src/lib/assets/asset-reference-extractor.ts @@ -12,6 +12,7 @@ import { AssetReference, ReferenceExtractionService, } from "../../types/syncAnalysis"; +import { AssetMapper } from "../mappers/asset-mapper"; export class AssetReferenceExtractor implements ReferenceExtractionService { private context?: SyncAnalysisContext; @@ -26,27 +27,30 @@ export class AssetReferenceExtractor implements ReferenceExtractionService { /** * Extract asset references from content fields */ - extractReferences(fields: any): AssetReference[] { - return this.extractAssetReferences(fields); + extractReferences(fields: any, assetMapper?: AssetMapper): AssetReference[] { + return this.extractAssetReferences(fields, assetMapper); } /** * Extract asset references from content fields */ - extractAssetReferences(fields: any): AssetReference[] { + extractAssetReferences(fields: any, assetMapper?: AssetMapper): AssetReference[] { const references: AssetReference[] = []; if (!fields || typeof fields !== "object") { return references; } - // Helper to check if a string is an asset URL - // Matches any subdomain of aglty.io or agilitycms.com (e.g., cdn-usa2.aglty.io, cdn-eu.aglty.io, etc.) + // Helper to check if a string is an asset URL. Matches: + // - any Agility-managed CDN subdomain (cdn.aglty.io, cdn-usa2.aglty.io, *.agilitycms.com, etc.) + // - any URL whose prefix matches a container URL loaded into the asset mapper (supports custom CDN hosts) const isAssetUrl = (url: string): boolean => { if (typeof url !== "string") return false; - // Check for Agility CMS asset URL patterns - match any subdomain - // Examples: cdn-usa2.aglty.io, cdn-eu.aglty.io, cdn.aglty.io, origin.aglty.io, etc. - return url.includes(".aglty.io") || url.includes(".agilitycms.com"); + return ( + url.includes(".aglty.io") || + url.includes(".agilitycms.com") || + assetMapper?.isKnownAssetUrl(url) === true + ); }; const scanForAssets = (obj: any, path: string) => { diff --git a/src/lib/assets/tests/asset-reference-extractor.test.ts b/src/lib/assets/tests/asset-reference-extractor.test.ts index d2347951..3df59dbd 100644 --- a/src/lib/assets/tests/asset-reference-extractor.test.ts +++ b/src/lib/assets/tests/asset-reference-extractor.test.ts @@ -1,5 +1,6 @@ import { resetState } from "core/state"; import { AssetReferenceExtractor } from "lib/assets/asset-reference-extractor"; +import { AssetMapper } from "lib/mappers/asset-mapper"; import { AssetReference, SourceEntities, SyncAnalysisContext } from "types/syncAnalysis"; beforeEach(() => { @@ -23,6 +24,11 @@ const makeContext = (overrides: Partial = {}): SyncAnalysis ...overrides, }); +// Lightweight AssetMapper stub — the extractor only ever calls isKnownAssetUrl. +// Avoids the real constructor, which loads mapping files from disk. +const makeAssetMapper = (isKnownAssetUrl: jest.Mock): AssetMapper => + ({ isKnownAssetUrl } as unknown as AssetMapper); + // ─── extractAssetReferences / extractReferences ─────────────────────────────── describe("AssetReferenceExtractor.extractAssetReferences", () => { @@ -164,6 +170,65 @@ describe("AssetReferenceExtractor.extractAssetReferences", () => { expect(refs[0].url).toBe("https://cdn.aglty.io/guid/assets/bg.jpg"); }); }); + + describe("custom CDN hosts via assetMapper.isKnownAssetUrl", () => { + const CUSTOM_CDN_URL = "https://media.contoso.com/guid/assets/photo.jpg"; + + it("does NOT recognize a custom-CDN URL when no assetMapper is supplied", () => { + const refs = extractor.extractAssetReferences({ image: CUSTOM_CDN_URL }); + expect(refs).toHaveLength(0); + }); + + it("recognizes a custom-CDN URL when the assetMapper reports it as known", () => { + const isKnownAssetUrl = jest.fn().mockReturnValue(true); + const refs = extractor.extractAssetReferences({ image: CUSTOM_CDN_URL }, makeAssetMapper(isKnownAssetUrl)); + + expect(refs).toHaveLength(1); + expect(refs[0]).toEqual({ url: CUSTOM_CDN_URL, fieldPath: "image" }); + expect(isKnownAssetUrl).toHaveBeenCalledWith(CUSTOM_CDN_URL); + }); + + it("ignores a non-asset URL when the assetMapper reports it as unknown", () => { + const isKnownAssetUrl = jest.fn().mockReturnValue(false); + const refs = extractor.extractAssetReferences( + { link: "https://example.com/page" }, + makeAssetMapper(isKnownAssetUrl) + ); + + expect(refs).toHaveLength(0); + expect(isKnownAssetUrl).toHaveBeenCalledWith("https://example.com/page"); + }); + + it("still matches built-in aglty.io domains without consulting the assetMapper", () => { + const isKnownAssetUrl = jest.fn().mockReturnValue(false); + const refs = extractor.extractAssetReferences( + { image: "https://cdn.aglty.io/guid/assets/photo.jpg" }, + makeAssetMapper(isKnownAssetUrl) + ); + + expect(refs).toHaveLength(1); + // Short-circuits on the .aglty.io check before reaching the mapper. + expect(isKnownAssetUrl).not.toHaveBeenCalled(); + }); + + it("recognizes a known custom-CDN URL nested inside an object's url property", () => { + const isKnownAssetUrl = jest.fn().mockReturnValue(true); + const refs = extractor.extractAssetReferences( + { attachment: { url: CUSTOM_CDN_URL } }, + makeAssetMapper(isKnownAssetUrl) + ); + + const assetRef = refs.find((r) => r.url === CUSTOM_CDN_URL); + expect(assetRef).toBeDefined(); + expect(assetRef!.fieldPath).toBe("attachment.url"); + }); + + it("treats an undefined isKnownAssetUrl result as not-an-asset (=== true guard)", () => { + const isKnownAssetUrl = jest.fn().mockReturnValue(undefined); + const refs = extractor.extractAssetReferences({ image: CUSTOM_CDN_URL }, makeAssetMapper(isKnownAssetUrl)); + expect(refs).toHaveLength(0); + }); + }); }); // ─── extractReferences (public alias) ──────────────────────────────────────── @@ -174,6 +239,18 @@ describe("AssetReferenceExtractor.extractReferences", () => { const fields = { image: "https://cdn.aglty.io/guid/assets/pic.jpg" }; expect(extractor.extractReferences(fields)).toEqual(extractor.extractAssetReferences(fields)); }); + + it("forwards the assetMapper through to extractAssetReferences", () => { + const extractor = new AssetReferenceExtractor(); + const isKnownAssetUrl = jest.fn().mockReturnValue(true); + const customUrl = "https://media.contoso.com/guid/assets/pic.jpg"; + + const refs = extractor.extractReferences({ image: customUrl }, makeAssetMapper(isKnownAssetUrl)); + + expect(refs).toHaveLength(1); + expect(refs[0].url).toBe(customUrl); + expect(isKnownAssetUrl).toHaveBeenCalledWith(customUrl); + }); }); // ─── initialize ─────────────────────────────────────────────────────────────── diff --git a/src/types/syncAnalysis.ts b/src/types/syncAnalysis.ts index 52803957..81bad9ea 100644 --- a/src/types/syncAnalysis.ts +++ b/src/types/syncAnalysis.ts @@ -2,6 +2,8 @@ * Shared TypeScript interfaces and types for sync analysis system */ +import { AssetMapper } from "lib/mappers/asset-mapper"; + /** * Model tracking to prevent duplicates across all chain displays */ @@ -53,7 +55,7 @@ export interface ReferenceExtractionService extends SyncAnalysisService { /** * Extract references from the given data structure */ - extractReferences(data: any): any[]; + extractReferences(data: any, assetMapper?: AssetMapper): any[]; } /** From 0724d3fae4696b8ff99866ed2ca17b8fdcab17ba Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jun 2026 20:02:55 -0400 Subject: [PATCH 2/9] add another cdn to match urls --- src/lib/assets/asset-reference-extractor.ts | 8 ++++---- src/lib/content/content-field-mapper.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/assets/asset-reference-extractor.ts b/src/lib/assets/asset-reference-extractor.ts index 902e6c12..b104cf34 100644 --- a/src/lib/assets/asset-reference-extractor.ts +++ b/src/lib/assets/asset-reference-extractor.ts @@ -27,14 +27,14 @@ export class AssetReferenceExtractor implements ReferenceExtractionService { /** * Extract asset references from content fields */ - extractReferences(fields: any, assetMapper?: AssetMapper): AssetReference[] { - return this.extractAssetReferences(fields, assetMapper); + extractReferences(fields: any): AssetReference[] { + return this.extractAssetReferences(fields); } /** * Extract asset references from content fields */ - extractAssetReferences(fields: any, assetMapper?: AssetMapper): AssetReference[] { + extractAssetReferences(fields: any): AssetReference[] { const references: AssetReference[] = []; if (!fields || typeof fields !== "object") { @@ -49,7 +49,7 @@ export class AssetReferenceExtractor implements ReferenceExtractionService { return ( url.includes(".aglty.io") || url.includes(".agilitycms.com") || - assetMapper?.isKnownAssetUrl(url) === true + url.includes(".ilotteryservices.com") ); }; diff --git a/src/lib/content/content-field-mapper.ts b/src/lib/content/content-field-mapper.ts index 9ffea9cd..eb47975a 100644 --- a/src/lib/content/content-field-mapper.ts +++ b/src/lib/content/content-field-mapper.ts @@ -138,6 +138,7 @@ export class ContentFieldMapper { return ( value.includes(".aglty.io") || value.includes(".agilitycms.com") || + value.includes(".ilotteryservices.com") || context?.assetMapper?.isKnownAssetUrl(value) === true ); } From fa084e748be1e1d8264eccb21c99b637aef1f7d2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jun 2026 20:46:14 -0400 Subject: [PATCH 3/9] rm unused code, pass target and source guid down for asset mapper --- src/lib/assets/asset-reference-extractor.ts | 8 +- src/lib/content/content-field-mapper.ts | 1 - src/lib/content/content-field-validation.ts | 420 ----------------- src/lib/content/index.ts | 1 - .../tests/content-field-validation.test.ts | 424 ------------------ .../models/model-dependency-tree-builder.ts | 11 +- src/lib/pushers/guid-data-loader.ts | 8 +- 7 files changed, 16 insertions(+), 857 deletions(-) delete mode 100644 src/lib/content/content-field-validation.ts delete mode 100644 src/lib/content/tests/content-field-validation.test.ts diff --git a/src/lib/assets/asset-reference-extractor.ts b/src/lib/assets/asset-reference-extractor.ts index b104cf34..902e6c12 100644 --- a/src/lib/assets/asset-reference-extractor.ts +++ b/src/lib/assets/asset-reference-extractor.ts @@ -27,14 +27,14 @@ export class AssetReferenceExtractor implements ReferenceExtractionService { /** * Extract asset references from content fields */ - extractReferences(fields: any): AssetReference[] { - return this.extractAssetReferences(fields); + extractReferences(fields: any, assetMapper?: AssetMapper): AssetReference[] { + return this.extractAssetReferences(fields, assetMapper); } /** * Extract asset references from content fields */ - extractAssetReferences(fields: any): AssetReference[] { + extractAssetReferences(fields: any, assetMapper?: AssetMapper): AssetReference[] { const references: AssetReference[] = []; if (!fields || typeof fields !== "object") { @@ -49,7 +49,7 @@ export class AssetReferenceExtractor implements ReferenceExtractionService { return ( url.includes(".aglty.io") || url.includes(".agilitycms.com") || - url.includes(".ilotteryservices.com") + assetMapper?.isKnownAssetUrl(url) === true ); }; diff --git a/src/lib/content/content-field-mapper.ts b/src/lib/content/content-field-mapper.ts index eb47975a..9ffea9cd 100644 --- a/src/lib/content/content-field-mapper.ts +++ b/src/lib/content/content-field-mapper.ts @@ -138,7 +138,6 @@ export class ContentFieldMapper { return ( value.includes(".aglty.io") || value.includes(".agilitycms.com") || - value.includes(".ilotteryservices.com") || context?.assetMapper?.isKnownAssetUrl(value) === true ); } diff --git a/src/lib/content/content-field-validation.ts b/src/lib/content/content-field-validation.ts deleted file mode 100644 index 1c287405..00000000 --- a/src/lib/content/content-field-validation.ts +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Content Field Validation Service - * - * Validates and sanitizes content fields before mapping to ensure: - * - Proper reference types and structures - * - Asset URL validity - * - Content ID reference validation - * - Field type compliance with Agility CMS expectations - */ - -import { LinkTypeDetector } from "../shared"; - -export interface FieldValidationResult { - isValid: boolean; - field: any; - warnings: string[]; - errors: string[]; - sanitizedField?: any; -} - -export interface ContentValidationOptions { - sourceAssets?: any[]; - sourceContainers?: any[]; - modelDefinitions?: any[]; - strictMode?: boolean; // If true, invalid references cause errors; if false, warnings -} - -export class ContentFieldValidator { - private linkTypeDetector: LinkTypeDetector; - - constructor() { - this.linkTypeDetector = new LinkTypeDetector(); - } - - /** - * Validate all fields in a content item - */ - public validateContentFields( - fields: any, - options: ContentValidationOptions = {} - ): { - isValid: boolean; - validatedFields: any; - totalWarnings: number; - totalErrors: number; - fieldResults: Map; - } { - if (!fields || typeof fields !== "object") { - return { - isValid: true, - validatedFields: fields, - totalWarnings: 0, - totalErrors: 0, - fieldResults: new Map(), - }; - } - - const fieldResults = new Map(); - const validatedFields: any = {}; - let totalWarnings = 0; - let totalErrors = 0; - let overallValid = true; - - for (const [fieldKey, fieldValue] of Object.entries(fields)) { - const result = this.validateSingleField(fieldKey, fieldValue, options); - fieldResults.set(fieldKey, result); - - validatedFields[fieldKey] = result.sanitizedField ?? result.field; - totalWarnings += result.warnings.length; - totalErrors += result.errors.length; - - if (!result.isValid) { - overallValid = false; - } - } - - return { - isValid: overallValid, - validatedFields, - totalWarnings, - totalErrors, - fieldResults, - }; - } - - /** - * Validate a single field with type-specific rules - */ - private validateSingleField( - fieldKey: string, - fieldValue: any, - options: ContentValidationOptions - ): FieldValidationResult { - const result: FieldValidationResult = { - isValid: true, - field: fieldValue, - warnings: [], - errors: [], - }; - - // Handle null/undefined - always valid - if (fieldValue === null || fieldValue === undefined) { - return result; - } - - // Validate object fields (content references, nested structures) - if (typeof fieldValue === "object" && fieldValue !== null) { - return this.validateObjectField(fieldKey, fieldValue, options); - } - - // Validate string fields (asset URLs, text content) - if (typeof fieldValue === "string") { - return this.validateStringField(fieldKey, fieldValue, options); - } - - // Validate numeric fields - if (typeof fieldValue === "number") { - return this.validateNumericField(fieldKey, fieldValue, options); - } - - // Primitive fields (boolean, etc.) are always valid - return result; - } - - /** - * Validate object fields with content references - */ - private validateObjectField( - fieldKey: string, - fieldValue: any, - options: ContentValidationOptions - ): FieldValidationResult { - const result: FieldValidationResult = { - isValid: true, - field: fieldValue, - warnings: [], - errors: [], - }; - - // Validate contentid/contentID references - if ("contentid" in fieldValue || "contentID" in fieldValue) { - const contentId = fieldValue.contentid || fieldValue.contentID; - if (typeof contentId !== "number" || contentId <= 0) { - result.errors.push(`Invalid content ID: ${contentId} in field ${fieldKey}`); - result.isValid = false; - } - } - - // Validate LinkedContentDropdown pattern - if (fieldValue.referencename && fieldValue.sortids) { - const sortIds = fieldValue.sortids.toString(); - - // Validate sortids format (comma-separated numbers) - const ids = sortIds.split(",").map((id) => id.trim()); - const invalidIds = ids.filter((id) => isNaN(parseInt(id)) || parseInt(id) <= 0); - - if (invalidIds.length > 0) { - result.errors.push(`Invalid sort IDs in field ${fieldKey}: ${invalidIds.join(", ")}`); - result.isValid = false; - } - - // Validate reference name if containers are available - if (options.sourceContainers) { - const containerExists = options.sourceContainers.some((c) => c.referenceName === fieldValue.referencename); - if (!containerExists) { - result.warnings.push(`Container reference ${fieldValue.referencename} not found in field ${fieldKey}`); - } - } - } - - // Validate gallery references - if (fieldValue.mediaGroupingID) { - const galleryId = fieldValue.mediaGroupingID; - if (typeof galleryId !== "number" || galleryId <= 0) { - result.errors.push(`Invalid gallery ID: ${galleryId} in field ${fieldKey}`); - result.isValid = false; - } - } - - // Recursive validation for nested objects/arrays - if (Array.isArray(fieldValue)) { - fieldValue.forEach((item, index) => { - if (typeof item === "object" && item !== null) { - const nestedResult = this.validateObjectField(`${fieldKey}[${index}]`, item, options); - result.warnings.push(...nestedResult.warnings); - result.errors.push(...nestedResult.errors); - if (!nestedResult.isValid) { - result.isValid = false; - } - } - }); - } - - return result; - } - - /** - * Validate string fields - */ - private validateStringField( - fieldKey: string, - fieldValue: string, - options: ContentValidationOptions - ): FieldValidationResult { - const result: FieldValidationResult = { - isValid: true, - field: fieldValue, - warnings: [], - errors: [], - }; - - // Validate asset URLs - if (this.isAssetUrl(fieldValue, options)) { - if (!this.isValidAssetUrl(fieldValue)) { - result.errors.push(`Invalid asset URL format in field ${fieldKey}: ${fieldValue}`); - result.isValid = false; - } else if (options.sourceAssets) { - // Check if asset exists in source data - const assetExists = options.sourceAssets.some( - (asset) => asset.originUrl === fieldValue || asset.url === fieldValue || asset.edgeUrl === fieldValue - ); - if (!assetExists) { - result.warnings.push(`Asset URL not found in source data for field ${fieldKey}: ${fieldValue}`); - } - } - } - - // Validate content ID strings (CategoryID, ValueField patterns) - if (this.isContentIdField(fieldKey, fieldValue)) { - const contentIds = fieldValue.includes(",") ? fieldValue.split(",").map((id) => id.trim()) : [fieldValue.trim()]; - - const invalidIds = contentIds.filter((id) => isNaN(parseInt(id)) || parseInt(id) <= 0); - if (invalidIds.length > 0) { - result.errors.push(`Invalid content IDs in field ${fieldKey}: ${invalidIds.join(", ")}`); - result.isValid = false; - } - } - - // Validate against maximum field length - if (fieldValue.length > 10000) { - // Agility CMS typical max field length - result.warnings.push(`Field ${fieldKey} exceeds recommended length (${fieldValue.length} chars)`); - } - - return result; - } - - /** - * Validate numeric fields - */ - private validateNumericField( - fieldKey: string, - fieldValue: number, - options: ContentValidationOptions - ): FieldValidationResult { - const result: FieldValidationResult = { - isValid: true, - field: fieldValue, - warnings: [], - errors: [], - }; - - // Validate range for ID fields - if (fieldKey.toLowerCase().includes("id") || fieldKey.toLowerCase().includes("contentid")) { - if (fieldValue <= 0) { - result.errors.push(`Invalid ID value in field ${fieldKey}: ${fieldValue}`); - result.isValid = false; - } - } - - return result; - } - - /** - * Check if string field contains content ID references - */ - private isContentIdField(fieldKey: string, fieldValue: string): boolean { - const lowercaseKey = fieldKey.toLowerCase(); - return ( - (lowercaseKey.includes("categoryid") || - lowercaseKey.includes("valuefield") || - lowercaseKey.includes("tags") || - lowercaseKey.includes("links")) && - /^\d+(,\d+)*$/.test(fieldValue.trim()) - ); - } - - /** - * Domain-agnostic check for asset URL strings. Matches: - * - any Agility-managed CDN subdomain (cdn.aglty.io, cdn-usa2.aglty.io, *.agilitycms.com, etc.) - * - any URL that exactly matches one of the source assets passed in options - * (so customer-supplied custom CDN domains are recognized when source data is available) - */ - private isAssetUrl(value: string, options: ContentValidationOptions): boolean { - if (value.includes(".aglty.io") || value.includes(".agilitycms.com")) return true; - if (options.sourceAssets) { - return options.sourceAssets.some( - (asset) => asset?.originUrl === value || asset?.url === value || asset?.edgeUrl === value - ); - } - return false; - } - - /** - * Structural validity check for an asset URL — caller is responsible for - * deciding the string is asset-like in the first place (see looksLikeAssetUrl). - */ - private isValidAssetUrl(url: string): boolean { - try { - const urlObj = new URL(url); - return urlObj.pathname.length > 1; - } catch { - return false; - } - } - - /** - * Sanitize field value to ensure compatibility - */ - public sanitizeField(fieldKey: string, fieldValue: any): any { - if (fieldValue === null || fieldValue === undefined) { - return fieldValue; - } - - // Sanitize string fields - if (typeof fieldValue === "string") { - // Trim whitespace - let sanitized = fieldValue.trim(); - - // Remove null characters - sanitized = sanitized.replace(/\0/g, ""); - - // Ensure proper encoding for special characters - try { - sanitized = decodeURIComponent(encodeURIComponent(sanitized)); - } catch { - // If encoding fails, return original - return fieldValue; - } - - return sanitized; - } - - // Sanitize numeric fields - if (typeof fieldValue === "number") { - // Ensure finite numbers - if (!Number.isFinite(fieldValue)) { - return 0; - } - return fieldValue; - } - - // Sanitize object fields recursively - if (typeof fieldValue === "object" && fieldValue !== null) { - if (Array.isArray(fieldValue)) { - return fieldValue.map((item, index) => this.sanitizeField(`${fieldKey}[${index}]`, item)); - } else { - const sanitized: any = {}; - for (const [key, value] of Object.entries(fieldValue)) { - sanitized[key] = this.sanitizeField(`${fieldKey}.${key}`, value); - } - return sanitized; - } - } - - return fieldValue; - } - /** - * Get validation summary for reporting - */ - public getValidationSummary(fieldResults: Map): { - totalFields: number; - validFields: number; - fieldsWithWarnings: number; - fieldsWithErrors: number; - criticalFields: string[]; - } { - const summary = { - totalFields: fieldResults.size, - validFields: 0, - fieldsWithWarnings: 0, - fieldsWithErrors: 0, - criticalFields: [] as string[], - }; - - fieldResults.forEach((result, fieldKey) => { - if (result.isValid) { - summary.validFields++; - } - if (result.warnings.length > 0) { - summary.fieldsWithWarnings++; - } - if (result.errors.length > 0) { - summary.fieldsWithErrors++; - summary.criticalFields.push(fieldKey); - } - }); - - return summary; - } -} - -/** - * Factory function for easy usage - */ -export function createContentFieldValidator(): ContentFieldValidator { - return new ContentFieldValidator(); -} - -/** - * Quick validation function for single fields - */ -export function validateField( - fieldKey: string, - fieldValue: any, - options: ContentValidationOptions = {} -): FieldValidationResult { - const validator = new ContentFieldValidator(); - return validator["validateSingleField"](fieldKey, fieldValue, options); -} diff --git a/src/lib/content/index.ts b/src/lib/content/index.ts index bcd93f82..3fe17ff3 100644 --- a/src/lib/content/index.ts +++ b/src/lib/content/index.ts @@ -1,3 +1,2 @@ export * from "./content-classifier"; export * from "./content-field-mapper"; -export * from "./content-field-validation"; diff --git a/src/lib/content/tests/content-field-validation.test.ts b/src/lib/content/tests/content-field-validation.test.ts deleted file mode 100644 index a52c2a91..00000000 --- a/src/lib/content/tests/content-field-validation.test.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { resetState } from "core/state"; -import { - ContentFieldValidator, - createContentFieldValidator, - validateField, - FieldValidationResult, -} from "lib/content/content-field-validation"; - -beforeEach(() => { - resetState(); - jest.spyOn(console, "log").mockImplementation(() => {}); - jest.spyOn(console, "warn").mockImplementation(() => {}); - jest.spyOn(console, "error").mockImplementation(() => {}); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -// ─── createContentFieldValidator / validateField factories ──────────────────── - -describe("createContentFieldValidator", () => { - it("returns a ContentFieldValidator instance", () => { - expect(createContentFieldValidator()).toBeInstanceOf(ContentFieldValidator); - }); -}); - -describe("validateField (standalone helper)", () => { - it("returns isValid:true for a plain string", () => { - const result = validateField("title", "Hello"); - expect(result.isValid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it("returns isValid:false for a non-positive contentid", () => { - const result = validateField("ref", { contentid: -1 }); - expect(result.isValid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); -}); - -// ─── validateContentFields ──────────────────────────────────────────────────── - -describe("ContentFieldValidator.validateContentFields", () => { - let validator: ContentFieldValidator; - - beforeEach(() => { - validator = new ContentFieldValidator(); - }); - - describe("null / non-object inputs", () => { - it.each([ - ["null", null], - ["undefined", undefined], - ["a number", 42], - ["a string", "text"], - ])("returns isValid:true with zero counts for %s", (_label, input) => { - const result = validator.validateContentFields(input as any); - expect(result.isValid).toBe(true); - expect(result.totalWarnings).toBe(0); - expect(result.totalErrors).toBe(0); - expect(result.fieldResults.size).toBe(0); - }); - }); - - describe("primitive fields", () => { - it("validates a plain string field as valid", () => { - const result = validator.validateContentFields({ title: "My Title" }); - expect(result.isValid).toBe(true); - expect(result.totalErrors).toBe(0); - expect(result.validatedFields.title).toBe("My Title"); - }); - - it("validates a number field without an id-like name as valid", () => { - const result = validator.validateContentFields({ count: 5 }); - expect(result.isValid).toBe(true); - }); - - it("validates a boolean field as valid", () => { - const result = validator.validateContentFields({ active: true }); - expect(result.isValid).toBe(true); - expect(result.totalErrors).toBe(0); - }); - - it("validates null and undefined field values as valid", () => { - const result = validator.validateContentFields({ a: null, b: undefined }); - expect(result.isValid).toBe(true); - }); - }); - - describe("numeric id fields", () => { - it("returns an error for a non-positive ID field", () => { - const result = validator.validateContentFields({ categoryId: -1 }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - - it("returns an error for a zero ID field", () => { - const result = validator.validateContentFields({ contentid: 0 }); - expect(result.isValid).toBe(false); - }); - - it("passes a positive numeric id field", () => { - const result = validator.validateContentFields({ categoryId: 10 }); - expect(result.isValid).toBe(true); - expect(result.totalErrors).toBe(0); - }); - }); - - describe("asset URL string fields", () => { - it("validates a well-formed cdn.aglty.io URL", () => { - const result = validator.validateContentFields({ - image: "https://cdn.aglty.io/guid/assets/photo.jpg", - }); - expect(result.totalErrors).toBe(0); - }); - - it('returns an error for a malformed cdn.aglty.io "URL"', () => { - const result = validator.validateContentFields({ - image: "not-a-url-cdn.aglty.io", - }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - - it("returns a warning when sourceAssets is provided but the URL is not found", () => { - const result = validator.validateContentFields( - { image: "https://cdn.aglty.io/guid/assets/missing.jpg" }, - { sourceAssets: [] } - ); - expect(result.totalWarnings).toBeGreaterThan(0); - }); - - it("does not warn when the URL is present in sourceAssets via url property", () => { - const url = "https://cdn.aglty.io/guid/assets/photo.jpg"; - const result = validator.validateContentFields({ image: url }, { sourceAssets: [{ url }] }); - expect(result.totalWarnings).toBe(0); - }); - - it("does not warn when the URL is present in sourceAssets via originUrl", () => { - const url = "https://cdn.aglty.io/guid/assets/photo.jpg"; - const result = validator.validateContentFields({ image: url }, { sourceAssets: [{ originUrl: url }] }); - expect(result.totalWarnings).toBe(0); - }); - - it("warns for a field that exceeds the recommended length", () => { - const longString = "a".repeat(10001); - const result = validator.validateContentFields({ body: longString }); - expect(result.totalWarnings).toBeGreaterThan(0); - }); - - it("validates a regional .aglty.io subdomain (cdn-usa2.aglty.io) as an asset URL", () => { - // Regression check: the old `includes("cdn.aglty.io")` check missed cdn-usa2.aglty.io. - const result = validator.validateContentFields({ - config: "https://cdn-usa2.aglty.io/brightstar-qa/mobile/config.json", - }); - expect(result.totalErrors).toBe(0); - }); - - it("validates a *.agilitycms.com subdomain as an asset URL", () => { - const result = validator.validateContentFields({ - hero: "https://cdndev.agilitycms.com/fcukahpf/posts/photo.jpg", - }); - expect(result.totalErrors).toBe(0); - }); - - it("recognizes a custom CDN URL by container-prefix match against sourceAssets", () => { - // Customer custom CDN host. The validator should derive a container prefix - // (host + first path segment) from the assets and accept URLs that share it. - const result = validator.validateContentFields( - { config: "https://cdn.ilotteryservices.com/8f5ad099/mobile/config.json" }, - { - sourceAssets: [{ url: "https://cdn.ilotteryservices.com/8f5ad099/mobile/other.json" }], - } - ); - // Both URLs share the prefix → no warning about asset-not-found. - expect(result.totalWarnings).toBe(0); - expect(result.totalErrors).toBe(0); - }); - - it("does not treat a non-asset URL as an asset URL", () => { - // String is a URL but no Agility domain and no sourceAsset prefix match. - const result = validator.validateContentFields( - { link: "https://www.example.com/article/123" }, - { sourceAssets: [{ url: "https://cdn.ilotteryservices.com/8f5ad099/file.json" }] } - ); - expect(result.totalErrors).toBe(0); - expect(result.totalWarnings).toBe(0); - }); - }); - - describe("content ID string fields", () => { - it("validates a valid comma-separated categoryid string", () => { - const result = validator.validateContentFields({ categoryid: "1,2,3" }); - expect(result.isValid).toBe(true); - expect(result.totalErrors).toBe(0); - }); - - it("does not error for a categoryid string containing non-numeric parts because isContentIdField guards on the pattern", () => { - // isContentIdField only triggers when the value already matches /^\d+(,\d+)*$/ - // so '1,abc,3' is treated as a plain string and passes through without error - const result = validator.validateContentFields({ categoryid: "1,abc,3" }); - expect(result.isValid).toBe(true); - expect(result.totalErrors).toBe(0); - }); - - it("returns an error for a zero ID in a categoryid string (zero fails parseInt(id) > 0 check)", () => { - const result = validator.validateContentFields({ categoryid: "0,2" }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - }); - - describe("content reference object fields (contentid / contentID)", () => { - it("returns an error for a non-positive contentid in an object field", () => { - const result = validator.validateContentFields({ ref: { contentid: -5 } }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - - it("returns an error for a non-positive contentID in an object field", () => { - const result = validator.validateContentFields({ ref: { contentID: 0 } }); - expect(result.isValid).toBe(false); - }); - - it("passes a positive contentid in an object field", () => { - const result = validator.validateContentFields({ ref: { contentid: 42 } }); - expect(result.totalErrors).toBe(0); - }); - - it("passes a positive contentID in an object field", () => { - const result = validator.validateContentFields({ ref: { contentID: 42 } }); - expect(result.totalErrors).toBe(0); - }); - - it("returns an error for a string contentid in an object field", () => { - const result = validator.validateContentFields({ ref: { contentid: "bad" } }); - expect(result.isValid).toBe(false); - }); - }); - - describe("referencename + sortids pattern", () => { - it("passes valid sortids with a referencename", () => { - const result = validator.validateContentFields({ - items: { referencename: "my-list", sortids: "1,2,3" }, - }); - expect(result.totalErrors).toBe(0); - }); - - it("returns an error for invalid sortids", () => { - const result = validator.validateContentFields({ - items: { referencename: "my-list", sortids: "1,abc" }, - }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - - it("returns an error for zero sortids", () => { - const result = validator.validateContentFields({ - items: { referencename: "my-list", sortids: "0,2" }, - }); - expect(result.isValid).toBe(false); - }); - - it("warns when the container reference is not found in sourceContainers", () => { - const result = validator.validateContentFields( - { items: { referencename: "ghost-list", sortids: "1" } }, - { sourceContainers: [{ referenceName: "other-list" }] } - ); - expect(result.totalWarnings).toBeGreaterThan(0); - }); - - it("does not warn when the container reference IS found in sourceContainers", () => { - const result = validator.validateContentFields( - { items: { referencename: "known-list", sortids: "1" } }, - { sourceContainers: [{ referenceName: "known-list" }] } - ); - expect(result.totalWarnings).toBe(0); - }); - }); - - describe("gallery reference fields", () => { - it("returns an error for a non-positive mediaGroupingID", () => { - const result = validator.validateContentFields({ gallery: { mediaGroupingID: -1 } }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - - it("passes a positive mediaGroupingID", () => { - const result = validator.validateContentFields({ gallery: { mediaGroupingID: 10 } }); - expect(result.totalErrors).toBe(0); - }); - }); - - describe("array field validation", () => { - it("validates each item in an array field recursively", () => { - const result = validator.validateContentFields({ - items: [{ contentid: 1 }, { contentid: -5 }], - }); - expect(result.isValid).toBe(false); - expect(result.totalErrors).toBeGreaterThan(0); - }); - - it("passes when all array items are valid", () => { - const result = validator.validateContentFields({ - items: [{ contentid: 1 }, { contentid: 2 }], - }); - expect(result.totalErrors).toBe(0); - }); - }); - - describe("fieldResults map", () => { - it("contains an entry per validated field", () => { - const result = validator.validateContentFields({ a: "x", b: "y" }); - expect(result.fieldResults.size).toBe(2); - expect(result.fieldResults.has("a")).toBe(true); - expect(result.fieldResults.has("b")).toBe(true); - }); - }); -}); - -// ─── sanitizeField ──────────────────────────────────────────────────────────── - -describe("ContentFieldValidator.sanitizeField", () => { - let validator: ContentFieldValidator; - - beforeEach(() => { - validator = new ContentFieldValidator(); - }); - - it("returns null unchanged", () => { - expect(validator.sanitizeField("f", null)).toBeNull(); - }); - - it("returns undefined unchanged", () => { - expect(validator.sanitizeField("f", undefined)).toBeUndefined(); - }); - - it("trims whitespace from string fields", () => { - expect(validator.sanitizeField("title", " hello ")).toBe("hello"); - }); - - it("removes null characters from string fields", () => { - expect(validator.sanitizeField("body", "hello\0world")).toBe("helloworld"); - }); - - it("returns 0 for non-finite numbers", () => { - expect(validator.sanitizeField("val", Infinity)).toBe(0); - expect(validator.sanitizeField("val", NaN)).toBe(0); - }); - - it("preserves finite numbers unchanged", () => { - expect(validator.sanitizeField("count", 42)).toBe(42); - expect(validator.sanitizeField("price", -3.14)).toBe(-3.14); - }); - - it("sanitizes string values inside nested objects recursively", () => { - const result = validator.sanitizeField("obj", { text: " padded " }); - expect(result.text).toBe("padded"); - }); - - it("sanitizes string values inside arrays recursively", () => { - const result = validator.sanitizeField("arr", [" a ", " b "]); - expect(result).toEqual(["a", "b"]); - }); - - it("passes through boolean values unchanged", () => { - expect(validator.sanitizeField("flag", true)).toBe(true); - expect(validator.sanitizeField("flag", false)).toBe(false); - }); -}); - -// ─── getValidationSummary ───────────────────────────────────────────────────── - -describe("ContentFieldValidator.getValidationSummary", () => { - let validator: ContentFieldValidator; - - beforeEach(() => { - validator = new ContentFieldValidator(); - }); - - it("returns zero counts for an empty map", () => { - const summary = validator.getValidationSummary(new Map()); - expect(summary.totalFields).toBe(0); - expect(summary.validFields).toBe(0); - expect(summary.fieldsWithWarnings).toBe(0); - expect(summary.fieldsWithErrors).toBe(0); - expect(summary.criticalFields).toHaveLength(0); - }); - - it("counts valid, warned, and errored fields correctly", () => { - const fieldResults = new Map([ - ["ok", { isValid: true, field: "x", warnings: [], errors: [] }], - ["warned", { isValid: true, field: "y", warnings: ["w1"], errors: [] }], - ["errored", { isValid: false, field: "z", warnings: [], errors: ["e1"] }], - ]); - const summary = validator.getValidationSummary(fieldResults); - expect(summary.totalFields).toBe(3); - expect(summary.validFields).toBe(2); - expect(summary.fieldsWithWarnings).toBe(1); - expect(summary.fieldsWithErrors).toBe(1); - expect(summary.criticalFields).toContain("errored"); - expect(summary.criticalFields).not.toContain("ok"); - }); - - it("includes a field in criticalFields when it has errors", () => { - const fieldResults = new Map([ - ["badField", { isValid: false, field: null, warnings: [], errors: ["bad content ID"] }], - ]); - const { criticalFields } = validator.getValidationSummary(fieldResults); - expect(criticalFields).toContain("badField"); - }); - - it("derives summary from validateContentFields output", () => { - const { fieldResults } = validator.validateContentFields({ - title: "Good", - ref: { contentid: -1 }, - }); - const summary = validator.getValidationSummary(fieldResults); - expect(summary.totalFields).toBe(2); - expect(summary.fieldsWithErrors).toBe(1); - expect(summary.criticalFields).toContain("ref"); - }); -}); diff --git a/src/lib/models/model-dependency-tree-builder.ts b/src/lib/models/model-dependency-tree-builder.ts index 6e217bed..320d7cef 100644 --- a/src/lib/models/model-dependency-tree-builder.ts +++ b/src/lib/models/model-dependency-tree-builder.ts @@ -12,6 +12,7 @@ import { SourceData } from "../../types/sourceData"; import ansiColors from "ansi-colors"; import { SitemapHierarchy } from "../pushers/page-pusher/sitemap-hierarchy"; import { AssetReferenceExtractor } from "../assets/asset-reference-extractor"; +import { AssetMapper } from "lib/mappers/asset-mapper"; export interface ModelDependencyTree { models: Set; // Model reference names @@ -27,9 +28,11 @@ export interface ModelDependencyTree { export class ModelDependencyTreeBuilder { private static hasLoggedBreakdown = false; private assetExtractor: AssetReferenceExtractor; + private assetMapper: AssetMapper; - constructor(private sourceData: SourceData) { + constructor(private sourceData: SourceData, targetGuid: string, sourceGuid: string) { this.assetExtractor = new AssetReferenceExtractor(); + this.assetMapper = new AssetMapper(targetGuid, sourceGuid); } /** @@ -563,7 +566,7 @@ export class ModelDependencyTreeBuilder { const urls: string[] = []; if (contentItem.fields) { - const assetReferences = this.assetExtractor.extractAssetReferences(contentItem.fields); + const assetReferences = this.assetExtractor.extractAssetReferences(contentItem.fields, this.assetMapper); assetReferences.forEach((ref) => { if (ref.url) { urls.push(ref.url); @@ -583,7 +586,7 @@ export class ModelDependencyTreeBuilder { // Scan page zones for asset references if (page.zones) { - const zoneReferences = this.assetExtractor.extractAssetReferences(page.zones); + const zoneReferences = this.assetExtractor.extractAssetReferences(page.zones, this.assetMapper); zoneReferences.forEach((ref) => { if (ref.url) { urls.push(ref.url); @@ -593,7 +596,7 @@ export class ModelDependencyTreeBuilder { // Scan page content if it exists if (page.content) { - const contentReferences = this.assetExtractor.extractAssetReferences(page.content); + const contentReferences = this.assetExtractor.extractAssetReferences(page.content, this.assetMapper); contentReferences.forEach((ref) => { if (ref.url) { urls.push(ref.url); diff --git a/src/lib/pushers/guid-data-loader.ts b/src/lib/pushers/guid-data-loader.ts index 7e4131b2..1d3ee7ee 100644 --- a/src/lib/pushers/guid-data-loader.ts +++ b/src/lib/pushers/guid-data-loader.ts @@ -123,7 +123,7 @@ export class GuidDataLoader { // Apply model filtering if requested if (filterOptions) { - return await this.applyModelFiltering(guidEntities, filterOptions, locale); + return await this.applyModelFiltering(guidEntities, filterOptions, locale, state.targetGuid[0], state.sourceGuid[0]); } return guidEntities; @@ -135,7 +135,9 @@ export class GuidDataLoader { private async applyModelFiltering( guidEntities: GuidEntities, filterOptions: ModelFilterOptions, - locale: string + locale: string, + targetGuid: string, + sourceGuid: string ): Promise { // Determine which filtering mode to use let modelNames: string[] = []; @@ -174,7 +176,7 @@ export class GuidDataLoader { // Import and use ModelDependencyTreeBuilder with complete data const { ModelDependencyTreeBuilder } = await import("../models/model-dependency-tree-builder"); - const treeBuilder = new ModelDependencyTreeBuilder(useFullDependencyTree ? completeEntities! : guidEntities); + const treeBuilder = new ModelDependencyTreeBuilder(useFullDependencyTree ? completeEntities! : guidEntities, targetGuid, sourceGuid); // Validate that specified models exist const validation = treeBuilder.validateModels(modelNames, (completeEntities ?? guidEntities).models); From 6a648efb41304d8cb425beac8538c78c846acd88 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Jun 2026 10:25:10 -0400 Subject: [PATCH 4/9] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 02b393e9..98102e1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agility/cli", - "version": "1.0.0-beta.13.18", + "version": "1.0.0-beta.13.19", "description": "Agility CLI for working with your content. (Public Beta)", "repository": { "type": "git", From 0b4eb64b8470cc0157ac4dbb9d2f9c9b67459e95 Mon Sep 17 00:00:00 2001 From: Jules Exel Date: Wed, 24 Jun 2026 11:12:41 -0400 Subject: [PATCH 5/9] Update asset mapping for legacy assets Update asset mapping for legacy assets --- src/lib/mappers/asset-mapper.ts | 31 +++++++- src/lib/mappers/tests/asset-mapper.test.ts | 72 ++++++++++++++++++- .../models/model-dependency-tree-builder.ts | 8 ++- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/lib/mappers/asset-mapper.ts b/src/lib/mappers/asset-mapper.ts index 2007a279..499e0e3a 100644 --- a/src/lib/mappers/asset-mapper.ts +++ b/src/lib/mappers/asset-mapper.ts @@ -102,6 +102,10 @@ export class AssetMapper { * Returns true if the given URL starts with any container edge/origin URL * known to the mapper (source or target). Lets callers identify asset URLs * without hardcoding CDN domains — supports custom CDN hosts. + * + * Falls back to the URL origin (protocol + host) of the per-asset + * sourceUrl/targetUrl for legacy mapping files written before container URLs + * were tracked, so custom-CDN assets are still recognized without a re-map. */ isKnownAssetUrl(url: string): boolean { if (!url || typeof url !== "string") return false; @@ -114,10 +118,35 @@ export class AssetMapper { mapping.targetContainerOriginUrl, ].filter((containerUrl): containerUrl is string => typeof containerUrl === "string"); - return knownContainerUrls.some((containerUrl) => url.startsWith(containerUrl)); + if (knownContainerUrls.length > 0) { + // New-format mapping: match strictly on the container prefix so assets + // from a different account on the same CDN host are not falsely matched. + return knownContainerUrls.some((containerUrl) => url.startsWith(containerUrl)); + } + + // Legacy fallback: this mapping predates container-URL tracking and only + // stored the full asset URLs (sourceUrl/targetUrl). Match on the URL + // origin so any asset on the same CDN host is recognized without a re-map. + const legacyOrigins = [mapping.sourceUrl, mapping.targetUrl] + .map((assetUrl) => this.getUrlOrigin(assetUrl)) + .filter((origin): origin is string => origin !== null); + + return legacyOrigins.some((origin) => url.startsWith(origin)); }); } + /** + * Extract the origin (protocol + host) from a URL, or null if it can't be parsed. + */ + private getUrlOrigin(url: string | undefined): string | null { + if (!url || typeof url !== "string") return null; + try { + return new URL(url).origin; + } catch { + return null; + } + } + getMappedEntity(mapping: AssetMapping, type: "source" | "target"): mgmtApi.Media | null { const guid = type === "source" ? mapping.sourceGuid : mapping.targetGuid; const mediaID = type === "source" ? mapping.sourceMediaID : mapping.targetMediaID; diff --git a/src/lib/mappers/tests/asset-mapper.test.ts b/src/lib/mappers/tests/asset-mapper.test.ts index d67a8a0f..ea548715 100644 --- a/src/lib/mappers/tests/asset-mapper.test.ts +++ b/src/lib/mappers/tests/asset-mapper.test.ts @@ -312,21 +312,91 @@ describe("AssetMapper.isKnownAssetUrl", () => { expect(mapper.isKnownAssetUrl("https://unrelated.example.com/file.jpg")).toBe(false); }); - it("ignores mappings whose container URLs are undefined", () => { + it("returns false when a mapping has no container URLs and no asset URLs", () => { const mapper = makeMapper(); const src = makeAsset({ mediaID: 1, + edgeUrl: undefined as any, containerEdgeUrl: undefined as any, containerOriginUrl: undefined as any, }); const tgt = makeAsset({ mediaID: 2, + edgeUrl: undefined as any, containerEdgeUrl: undefined as any, containerOriginUrl: undefined as any, }); mapper.addMapping(src, tgt); expect(mapper.isKnownAssetUrl("https://cdn.aglty.io/anything.jpg")).toBe(false); }); + + // Legacy fallback: mapping files written before container URLs were tracked + // only stored the full per-asset sourceUrl/targetUrl. Detection must still + // work off the URL origin so custom-CDN assets are recognized without a re-map. + describe("legacy mapping files (no container URLs)", () => { + it("recognizes a custom-CDN URL via the source asset URL origin", () => { + const mapper = makeMapper(); + // Simulate a legacy entry: container URLs absent, only edge/origin asset URLs. + const src = makeAsset({ + mediaID: 1, + edgeUrl: "https://cdn.ilotteryservices.com/brightstar-tns-cat/acl/en-us1.json", + containerEdgeUrl: undefined as any, + containerOriginUrl: undefined as any, + }); + const tgt = makeAsset({ + mediaID: 2, + edgeUrl: "https://cdn-usa2.aglty.io/de5185c3/acl/en-us1.json", + containerEdgeUrl: undefined as any, + containerOriginUrl: undefined as any, + }); + mapper.addMapping(src, tgt); + + // A different file on the same custom CDN host is recognized. + expect( + mapper.isKnownAssetUrl( + "https://cdn.ilotteryservices.com/brightstar-tns-cat/mobile/configuration/draw-games/quickPickConfig.json" + ) + ).toBe(true); + }); + + it("recognizes a URL via the target asset URL origin", () => { + const mapper = makeMapper(); + const src = makeAsset({ + mediaID: 1, + edgeUrl: "https://cdn.ilotteryservices.com/brightstar-tns-cat/acl/en-us1.json", + containerEdgeUrl: undefined as any, + containerOriginUrl: undefined as any, + }); + const tgt = makeAsset({ + mediaID: 2, + edgeUrl: "https://cdn-usa2.aglty.io/de5185c3/acl/en-us1.json", + containerEdgeUrl: undefined as any, + containerOriginUrl: undefined as any, + }); + mapper.addMapping(src, tgt); + + expect(mapper.isKnownAssetUrl("https://cdn-usa2.aglty.io/de5185c3/mobile/other.json")).toBe(true); + }); + + it("does not match a URL on an unrelated host", () => { + const mapper = makeMapper(); + const src = makeAsset({ + mediaID: 1, + edgeUrl: "https://cdn.ilotteryservices.com/brightstar-tns-cat/acl/en-us1.json", + containerEdgeUrl: undefined as any, + containerOriginUrl: undefined as any, + }); + const tgt = makeAsset({ + mediaID: 2, + edgeUrl: "https://cdn-usa2.aglty.io/de5185c3/acl/en-us1.json", + containerEdgeUrl: undefined as any, + containerOriginUrl: undefined as any, + }); + mapper.addMapping(src, tgt); + + expect(mapper.isKnownAssetUrl("https://cdn.competitor.com/brightstar-tns-cat/file.json")).toBe(false); + }); + }); }); // ─── addMapping / updateMapping ─────────────────────────────────────────────── diff --git a/src/lib/models/model-dependency-tree-builder.ts b/src/lib/models/model-dependency-tree-builder.ts index 320d7cef..cd4f4f80 100644 --- a/src/lib/models/model-dependency-tree-builder.ts +++ b/src/lib/models/model-dependency-tree-builder.ts @@ -30,9 +30,13 @@ export class ModelDependencyTreeBuilder { private assetExtractor: AssetReferenceExtractor; private assetMapper: AssetMapper; - constructor(private sourceData: SourceData, targetGuid: string, sourceGuid: string) { + constructor( + private sourceData: SourceData, + targetGuid: string, + sourceGuid: string + ) { this.assetExtractor = new AssetReferenceExtractor(); - this.assetMapper = new AssetMapper(targetGuid, sourceGuid); + this.assetMapper = new AssetMapper(sourceGuid, targetGuid); } /** From c529c68b423d89d3f1cf1133e12c95c7690cbb04 Mon Sep 17 00:00:00 2001 From: Jules Exel Date: Wed, 24 Jun 2026 11:13:47 -0400 Subject: [PATCH 6/9] update package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98102e1e..ea484ef1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agility/cli", - "version": "1.0.0-beta.13.19", + "version": "1.0.0-beta.13.20", "description": "Agility CLI for working with your content. (Public Beta)", "repository": { "type": "git", From 89c798b6452c7654991e3551a52a68b3b765b7b1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 2 Jul 2026 11:50:09 -0400 Subject: [PATCH 7/9] fix test, rm setup --- jest.config.js | 1 - .../model-dependency-tree-builder.test.ts | 99 ++++++++++--------- src/tests/setup.ts | 20 ---- 3 files changed, 53 insertions(+), 67 deletions(-) delete mode 100644 src/tests/setup.ts diff --git a/jest.config.js b/jest.config.js index 4e7cc44c..cafd060d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,6 @@ module.exports = { // Default: unit tests only (exclude integration tests) testMatch: ["**/src/**/tests/**/*.test.ts"], testPathIgnorePatterns: ["/node_modules/", "/dist/", "/src/index.ts", "integration\\.test\\.ts"], - setupFilesAfterEnv: ["/src/tests/setup.ts"], // Map TypeScript path aliases to actual paths moduleNameMapper: { "^core/(.*)$": "/src/core/$1", diff --git a/src/lib/models/tests/model-dependency-tree-builder.test.ts b/src/lib/models/tests/model-dependency-tree-builder.test.ts index 015d0953..1ac60192 100644 --- a/src/lib/models/tests/model-dependency-tree-builder.test.ts +++ b/src/lib/models/tests/model-dependency-tree-builder.test.ts @@ -94,6 +94,13 @@ function makeSourceData(overrides: Partial = {}): any { }; } +const TEST_TARGET_GUID = "target-guid"; +const TEST_SOURCE_GUID = "source-guid"; + +function makeBuilder(sourceData: any): ModelDependencyTreeBuilder { + return new ModelDependencyTreeBuilder(sourceData, TEST_TARGET_GUID, TEST_SOURCE_GUID); +} + // ─── resetLoggingFlags ──────────────────────────────────────────────────────── describe("ModelDependencyTreeBuilder.resetLoggingFlags", () => { @@ -102,7 +109,7 @@ describe("ModelDependencyTreeBuilder.resetLoggingFlags", () => { }); it("allows the breakdown log to fire again on a fresh builder", () => { - const builder = new ModelDependencyTreeBuilder( + const builder = makeBuilder( makeSourceData({ models: [makeModel(1, "Post")], content: [makeContent(10, "Post")] }) ); const logSpy = jest.spyOn(console, "log"); @@ -125,7 +132,7 @@ describe("ModelDependencyTreeBuilder.resetLoggingFlags", () => { describe("ModelDependencyTreeBuilder constructor", () => { it("does not throw with a valid sourceData object", () => { - expect(() => new ModelDependencyTreeBuilder(makeSourceData())).not.toThrow(); + expect(() => makeBuilder(makeSourceData())).not.toThrow(); }); }); @@ -135,7 +142,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — guard clauses", () let builder: ModelDependencyTreeBuilder; beforeEach(() => { - builder = new ModelDependencyTreeBuilder(makeSourceData()); + builder = makeBuilder(makeSourceData()); }); it("throws when modelNames is null", () => { @@ -155,7 +162,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — guard clauses", () describe("ModelDependencyTreeBuilder.buildDependencyTree — empty source data", () => { it("returns tree with only the requested model names when source data is empty", () => { - const builder = new ModelDependencyTreeBuilder(makeSourceData()); + const builder = makeBuilder(makeSourceData()); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.models).toEqual(new Set(["Post"])); @@ -168,13 +175,13 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — empty source data", }); it("seeds the models set with all supplied model names", () => { - const builder = new ModelDependencyTreeBuilder(makeSourceData()); + const builder = makeBuilder(makeSourceData()); const tree = builder.buildDependencyTree(["Alpha", "Beta", "Gamma"], "website"); expect(tree.models).toEqual(new Set(["Alpha", "Beta", "Gamma"])); }); it("returns a tree with all required keys", () => { - const builder = new ModelDependencyTreeBuilder(makeSourceData()); + const builder = makeBuilder(makeSourceData()); const tree = builder.buildDependencyTree(["M"], "website"); const keys: Array = [ "models", @@ -198,7 +205,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery models: [makeModel(1, "Post")], containers: [makeContainer(100, 1)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.containers.has(100)).toBe(true); @@ -209,7 +216,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery models: [makeModel(1, "Post"), makeModel(2, "Author")], containers: [makeContainer(100, 1), makeContainer(200, 2)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.containers.has(100)).toBe(true); @@ -221,7 +228,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery models: [makeModel(1, "Post")], containers: [makeContainer(100, 1), makeContainer(101, 1)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.containers.has(100)).toBe(true); @@ -230,13 +237,13 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery it("handles missing containers array gracefully", () => { const sourceData = makeSourceData({ models: [makeModel(1, "Post")], containers: undefined }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); it("handles missing models array gracefully", () => { const sourceData = makeSourceData({ models: undefined, containers: [makeContainer(100, 1)] }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); @@ -245,7 +252,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery models: [makeModel(1, "Post")], containers: [makeContainer(100, 1)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["NonExistent"], "website"); expect(tree.containers.size).toBe(0); }); @@ -258,7 +265,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — content discovery", const sourceData = makeSourceData({ content: [makeContent(10, "Post"), makeContent(11, "Post"), makeContent(12, "Author")], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.content.has(10)).toBe(true); @@ -268,7 +275,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — content discovery", it("handles missing content array gracefully", () => { const sourceData = makeSourceData({ content: undefined }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); @@ -276,7 +283,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — content discovery", const sourceData = makeSourceData({ content: [makeContent(10, "Post"), makeContent(20, "Author")], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post", "Author"], "website"); expect(tree.content.has(10)).toBe(true); @@ -293,7 +300,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — template discovery containers: [makeContainer(100, 1)], templates: [makeTemplate(500, [{ contentViewID: 100 }])], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.templates.has(500)).toBe(true); @@ -305,7 +312,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — template discovery containers: [makeContainer(100, 1)], templates: [makeTemplate(501, [{ itemContainerID: 100 }])], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.templates.has(501)).toBe(true); @@ -317,7 +324,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — template discovery containers: [makeContainer(100, 1)], templates: [makeTemplate(500, [{ contentViewID: 999 }])], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.templates.has(500)).toBe(false); @@ -329,7 +336,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — template discovery containers: [makeContainer(100, 1)], templates: undefined, }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); @@ -342,7 +349,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — template discovery ], pages: [{ pageID: 300, name: "blog", templateName: "MainLayout" }], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.templates.has(600)).toBe(true); @@ -359,7 +366,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — page discovery", () templates: [makeTemplate(500, [{ contentViewID: 100 }])], pages: [makePage(300, { pageTemplateID: 500 })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.pages.has(300)).toBe(true); @@ -371,7 +378,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — page discovery", () content: [makeContent(10, "Post")], pages: [makePage(300, { zones })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.pages.has(300)).toBe(true); @@ -383,7 +390,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — page discovery", () content: [makeContent(20, "Widget")], pages: [makePage(400, { zones })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Widget"], "website"); expect(tree.pages.has(400)).toBe(true); @@ -396,7 +403,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — page discovery", () templates: [makeTemplate(500, [{ contentViewID: 100 }])], pages: [makePage(300, { pageTemplateID: 500 }), makePage(999, { pageTemplateID: 888 })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.pages.has(300)).toBe(true); @@ -405,7 +412,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — page discovery", () it("handles missing pages array gracefully", () => { const sourceData = makeSourceData({ pages: undefined }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); }); @@ -419,7 +426,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — content pulled from content: [makeContent(10, "Post"), makeContent(99, "Promo")], pages: [makePage(300, { zones })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.content.has(99)).toBe(true); @@ -431,7 +438,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — content pulled from content: [makeContent(10, "Post")], pages: [makePage(300, { zones })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); // Should not throw expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); @@ -447,7 +454,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — model back-discover models: [makeModel(1, "Post"), makeModel(2, "Promo")], pages: [makePage(300, { zones })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.models.has("Promo")).toBe(true); @@ -465,7 +472,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery ], content: [makeContent(10, "Post", "news1_postlist")], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.containers.has(100)).toBe(true); @@ -480,7 +487,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — container discovery containers: [makeContainer(200, 99, "unrelated_container")], content: [makeContent(10, "Post", "news1_postlist")], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.containers.has(200)).toBe(false); @@ -494,7 +501,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — asset discovery", ( const sourceData = makeSourceData({ content: [makeContent(10, "Post", "ref-10", { image: "https://cdn.aglty.io/my-img.jpg" })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.assets.has("https://cdn.aglty.io/my-img.jpg")).toBe(true); @@ -511,7 +518,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — asset discovery", ( ), ], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.assets.has("https://origin.aglty.io/my-img.jpg")).toBe(true); @@ -525,7 +532,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — asset discovery", ( makeContent(20, "Other", "ref-20", { image: "https://cdn.aglty.io/excluded.jpg" }), ], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.assets.has("https://cdn.aglty.io/included.jpg")).toBe(true); @@ -534,7 +541,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — asset discovery", ( it("handles missing content array gracefully for asset discovery", () => { const sourceData = makeSourceData({ content: undefined }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); @@ -542,7 +549,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — asset discovery", ( const sourceData = makeSourceData({ content: [makeContent(10, "Post", "ref-10", { banner: "https://static.agilitycms.com/banner.png" })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.assets.has("https://static.agilitycms.com/banner.png")).toBe(true); @@ -557,7 +564,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — gallery discovery", content: [makeContent(10, "Post", "ref-10", { gallery: { mediaGroupingID: 55 } })], galleries: [makeGallery(55)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.galleries.has(55)).toBe(true); @@ -568,7 +575,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — gallery discovery", content: [makeContent(10, "Post", "ref-10", { pics: { galleryID: 77 } })], galleries: [makeGallery(77)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.galleries.has(77)).toBe(true); @@ -582,7 +589,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — gallery discovery", ], galleries: [makeGallery(99)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.galleries.has(99)).toBe(false); @@ -593,7 +600,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — gallery discovery", content: [makeContent(10, "Post", "ref-10", { g: { mediaGroupingID: 55 } })], galleries: undefined, }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); expect(() => builder.buildDependencyTree(["Post"], "website")).not.toThrow(); }); @@ -602,7 +609,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — gallery discovery", content: [makeContent(10, "Post", "ref-10", { items: [{ mediaGroupingID: 42 }] })], galleries: [makeGallery(42)], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.galleries.has(42)).toBe(true); @@ -624,7 +631,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — ancestor page disco content: [makeContent(10, "Post")], pages: [makePage(300, { zones, name: "child-page" }), makePage(200, { name: "parent-page" })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.pages.has(300)).toBe(true); @@ -641,7 +648,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — ancestor page disco content: [makeContent(10, "Post")], pages: [makePage(300, { zones })], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.pages.size).toBe(1); @@ -663,7 +670,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — ancestor page disco makePage(100, { name: "grandparent" }), ], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.pages.has(100)).toBe(true); @@ -677,7 +684,7 @@ describe("ModelDependencyTreeBuilder.buildDependencyTree — ancestor page disco describe("ModelDependencyTreeBuilder.buildDependencyTree — breakdown log deduplication", () => { it("only logs the breakdown once across multiple calls on the same builder", () => { const sourceData = makeSourceData({ content: [makeContent(10, "Post")] }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const logSpy = jest.spyOn(console, "log"); builder.buildDependencyTree(["Post"], "website"); @@ -694,7 +701,7 @@ describe("ModelDependencyTreeBuilder.validateModels", () => { let builder: ModelDependencyTreeBuilder; beforeEach(() => { - builder = new ModelDependencyTreeBuilder(makeSourceData()); + builder = makeBuilder(makeSourceData()); }); it("returns all names as invalid when models list is empty", () => { @@ -759,7 +766,7 @@ describe("ModelDependencyTreeBuilder — full pipeline integration", () => { assets: [makeAsset("https://cdn.aglty.io/hero.jpg")], }); - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = makeBuilder(sourceData); const tree = builder.buildDependencyTree(["Post"], "website"); expect(tree.models.has("Post")).toBe(true); diff --git a/src/tests/setup.ts b/src/tests/setup.ts deleted file mode 100644 index fc6f4903..00000000 --- a/src/tests/setup.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Jest setup file - loads environment variables for testing - * - * Test env file location: src/tests/.env.test - * Copy .env.test.example to .env.test and fill in your test credentials - */ -import dotenv from "dotenv"; -import path from "path"; - -// Load .env.test from the tests folder (not project root) -const envPath = path.resolve(__dirname, ".env"); -const result = dotenv.config({ path: envPath }); - -if (result.error) { - console.warn(` -⚠️ Test environment file not found: ${envPath} - Copy src/tests/.env.test.example to src/tests/.env - and fill in your test credentials. -`); -} From 616aa54c80f7da05afb9f23a8a604da27598cf39 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 2 Jul 2026 12:35:52 -0400 Subject: [PATCH 8/9] Fix ModelDependencyTreeBuilder call in sync test helper Pass source guid for both targetGuid and sourceGuid to match the three-arg constructor; this helper only reads tree.content. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tests/sync/helpers/dependency-filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/sync/helpers/dependency-filter.ts b/src/tests/sync/helpers/dependency-filter.ts index e548769a..c0ce6b2c 100644 --- a/src/tests/sync/helpers/dependency-filter.ts +++ b/src/tests/sync/helpers/dependency-filter.ts @@ -33,7 +33,7 @@ export function applyModelsWithDepsFilter(opts: { lists: [], }; - const builder = new ModelDependencyTreeBuilder(sourceData); + const builder = new ModelDependencyTreeBuilder(sourceData, opts.sourceGuid, opts.sourceGuid); const validation = builder.validateModels(opts.modelsWithDeps, models); if (validation.invalid.length > 0) { From a6c0c805bf681c9a957937c5150164c412f92fea Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 2 Jul 2026 13:25:09 -0400 Subject: [PATCH 9/9] Set guids in guid-data-loader test so model filtering can build AssetMapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model filtering constructs a ModelDependencyTreeBuilder → AssetMapper → fileOperations, which needs valid source/target guids. Set them in beforeEach so the filtering tests exercise validation instead of failing on an undefined path. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/pushers/tests/guid-data-loader.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/pushers/tests/guid-data-loader.test.ts b/src/lib/pushers/tests/guid-data-loader.test.ts index 7721ce26..e3633e3b 100644 --- a/src/lib/pushers/tests/guid-data-loader.test.ts +++ b/src/lib/pushers/tests/guid-data-loader.test.ts @@ -17,6 +17,9 @@ afterAll(() => { beforeEach(() => { resetState(); setState({ rootPath: tmpDir }); + // Model filtering builds a ModelDependencyTreeBuilder → AssetMapper, which needs guids. + state.sourceGuid = ["source-guid-u"]; + state.targetGuid = ["target-guid-u"]; jest.spyOn(console, "log").mockImplementation(() => {}); jest.spyOn(console, "warn").mockImplementation(() => {}); jest.spyOn(console, "error").mockImplementation(() => {});