Skip to content
Open
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
142 changes: 142 additions & 0 deletions packages/blueprint-tester/__tests__/hcl-parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { describe, expect, it } from 'vitest';
import { parseTerraformFile } from '../src/hcl-parser.js';

describe('parseTerraformFile', () => {
it('parses a simple resource block', () => {
const content = `
resource "epilot_journey" "sample_journey_abc123" {
name = "My Journey"
access_mode = "PUBLIC"
}`;
const result = parseTerraformFile('main.tf', content);

expect(result.resources).toHaveLength(1);
expect(result.resources[0].type).toBe('epilot_journey');
expect(result.resources[0].name).toBe('sample_journey_abc123');
expect(result.resources[0].address).toBe('epilot_journey.sample_journey_abc123');
expect(result.resources[0].attributes.name).toBe('My Journey');
expect(result.resources[0].attributes.access_mode).toBe('PUBLIC');
});

it('parses multiple resource blocks', () => {
const content = `
resource "epilot_journey" "journey_1" {
name = "Journey 1"
}

resource "epilot_automation" "auto_1" {
name = "Automation 1"
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources).toHaveLength(2);
expect(result.resources[0].type).toBe('epilot_journey');
expect(result.resources[1].type).toBe('epilot_automation');
});

it('parses depends_on arrays', () => {
const content = `
resource "epilot_automation" "auto_1" {
name = "Automation"
depends_on = [epilot_journey.journey_1, epilot_schema.contact]
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources[0].dependsOn).toEqual([
'epilot_journey.journey_1',
'epilot_schema.contact',
]);
});

it('parses nested blocks', () => {
const content = `
resource "epilot_automation" "auto_1" {
trigger {
type = "journey_submission"
journey_id = "abc-123"
}
name = "Automation"
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources[0].attributes.trigger).toBeDefined();
expect(result.resources[0].attributes.name).toBe('Automation');
});

it('parses boolean and null values', () => {
const content = `
resource "epilot_journey" "j1" {
is_active = true
is_deleted = false
description = null
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources[0].attributes.is_active).toBe(true);
expect(result.resources[0].attributes.is_deleted).toBe(false);
expect(result.resources[0].attributes.description).toBe(null);
});

it('preserves terraform references as strings', () => {
const content = `
resource "epilot_automation" "auto_1" {
journey_id = epilot_journey.sample.journey_id
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources[0].attributes.journey_id).toBe(
'epilot_journey.sample.journey_id',
);
});

it('parses variables', () => {
const content = `
variable "manifest_id" {
type = string
}

variable "target_org_id" {
type = string
}

resource "epilot_journey" "j1" {
name = "test"
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.variables).toHaveProperty('manifest_id');
expect(result.variables).toHaveProperty('target_org_id');
});

it('tracks line numbers', () => {
const content = `
resource "epilot_journey" "j1" {
name = "test"
}

resource "epilot_automation" "a1" {
name = "auto"
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources[0].lineStart).toBe(2);
expect(result.resources[1].lineStart).toBe(6);
});

it('handles lifecycle blocks', () => {
const content = `
resource "epilot-taxonomy_taxonomy_classification" "tax_abc123" {
lifecycle {
prevent_destroy = true
}
manifest = distinct([var.manifest_id])
name = "Test Classification"
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources).toHaveLength(1);
expect(result.resources[0].attributes.name).toBe('Test Classification');
});

it('handles jsonencode function calls', () => {
const content = `
resource "epilot_journey" "j1" {
journey = jsonencode({"steps": [{"name": "step1"}]})
name = "test"
}`;
const result = parseTerraformFile('main.tf', content);
expect(result.resources[0].attributes.journey).toContain('jsonencode');
});
});
148 changes: 148 additions & 0 deletions packages/blueprint-tester/__tests__/json-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { describe, expect, it } from 'vitest';
import { fromManifestJson, fromResourceArray } from '../src/adapters/json-adapter.js';
import { validateBlueprint } from '../src/validator.js';
import type { BlueprintManifest } from '../src/types.js';

describe('JSON adapter', () => {
it('converts a manifest to BlueprintData', () => {
const manifest: BlueprintManifest = {
blueprint_id: 'test-bp-123',
name: 'Test Blueprint',
source_type: 'custom',
resources: [
{
type: 'journey',
id: 'j-001',
name: 'Onboarding Journey',
address: 'epilot_journey.onboarding',
is_root: true,
is_ready: true,
depends_on_addresses: [],
config: {
name: 'Onboarding Journey',
access_mode: 'PRIVATE',
},
},
{
type: 'automation_flow',
id: 'a-001',
name: 'Auto Flow',
address: 'epilot_automation.auto_flow',
depends_on_addresses: ['epilot_journey.onboarding'],
config: {
name: 'Auto Flow',
trigger: { type: 'journey_submission', journey_id: 'j-001' },
},
},
],
};

const data = fromManifestJson(manifest);

expect(data.format).toBe('json');
expect(data.resources).toHaveLength(2);
expect(data.metadata?.blueprintId).toBe('test-bp-123');
expect(data.resources[0].type).toBe('epilot_journey');
expect(data.resources[0].name).toBe('Onboarding Journey');
expect(data.resources[1].dependsOn).toEqual(['epilot_journey.onboarding']);
});

it('converts a resource array to BlueprintData', () => {
const data = fromResourceArray([
{
type: 'schema',
id: 's-001',
name: 'Contact Schema',
config: { slug: 'contact', name: 'Contact' },
},
]);

expect(data.format).toBe('json');
expect(data.resources).toHaveLength(1);
expect(data.resources[0].type).toBe('epilot_schema');
});

it('validates a JSON manifest for dangling UUIDs', async () => {
const manifest: BlueprintManifest = {
blueprint_id: 'bp-dangling',
resources: [
{
type: 'automation_flow',
id: 'a-001',
name: 'Auto Flow',
config: {
name: 'Auto Flow',
trigger: {
type: 'journey_submission',
journey_id: 'd11995ae-368e-4ad3-bf1c-51d6449f8afc',
},
},
},
],
};

const report = await validateBlueprint(manifest);
expect(report.valid).toBe(false);
expect(report.metadata.format).toBe('json');
expect(report.issues.some((i) => i.ruleId === 'dangling-uuid')).toBe(true);
});

it('validates a clean JSON manifest', async () => {
const manifest: BlueprintManifest = {
blueprint_id: 'bp-clean',
resources: [
{
type: 'journey',
id: 'j-001',
name: 'Journey',
config: { name: 'My Journey', access_mode: 'PRIVATE' },
},
],
};

const report = await validateBlueprint(manifest);
expect(report.valid).toBe(true);
expect(report.metadata.format).toBe('json');
expect(report.metadata.blueprintId).toBe('bp-clean');
});

it('detects org IDs in JSON manifest resources', async () => {
const manifest: BlueprintManifest = {
blueprint_id: 'bp-org',
resources: [
{
type: 'journey',
id: 'j-001',
name: 'Journey',
config: {
name: 'My Journey',
organization_id: '911690',
},
},
],
};

const report = await validateBlueprint(manifest);
expect(report.issues.some((i) => i.ruleId === 'source-org-ref')).toBe(true);
});

it('detects tokens in JSON manifest resources', async () => {
const manifest: BlueprintManifest = {
blueprint_id: 'bp-token',
resources: [
{
type: 'webhook',
id: 'wh-001',
name: 'Webhook',
config: {
url: 'https://example.com',
auth_token: 'api_5ZugdRXasLfWBypHi93Fk',
},
},
],
};

const report = await validateBlueprint(manifest);
expect(report.issues.some((i) => i.ruleId === 'token-detection')).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import { parseTerraformFile } from '../../src/hcl-parser.js';
import { crossRefIntegrityRule } from '../../src/rules/cross-ref-integrity.js';
import { buildResourceIndex } from '../../src/utils/resource-index.js';

function runRule(tfContent: string) {
const file = parseTerraformFile('main.tf', tfContent);
const files = [file];
const resourceIndex = buildResourceIndex(files);
return crossRefIntegrityRule.validate({ files, resourceIndex, options: {}, format: 'terraform' });
}

describe('cross-ref-integrity rule', () => {
it('flags depends_on referencing non-existent resource', () => {
const tf = `
resource "epilot_automation" "auto_1" {
name = "Automation"
depends_on = [epilot_journey.missing_journey]
}`;
const issues = runRule(tf);
expect(issues).toHaveLength(1);
expect(issues[0].ruleId).toBe('cross-ref-integrity');
expect(issues[0].attributePath).toBe('depends_on');
expect(issues[0].value).toBe('epilot_journey.missing_journey');
});

it('does NOT flag depends_on referencing existing resource', () => {
const tf = `
resource "epilot_journey" "journey_1" {
name = "Journey"
}

resource "epilot_automation" "auto_1" {
name = "Automation"
depends_on = [epilot_journey.journey_1]
}`;
const issues = runRule(tf);
expect(issues).toHaveLength(0);
});

it('flags terraform reference to non-existent resource', () => {
const tf = `
resource "epilot_automation" "auto_1" {
journey_id = "\${epilot_journey.missing.journey_id}"
}`;
const issues = runRule(tf);
expect(issues).toHaveLength(1);
expect(issues[0].value).toBe('epilot_journey.missing');
});

it('does NOT flag terraform reference to existing resource', () => {
const tf = `
resource "epilot_journey" "sample" {
name = "Journey"
}

resource "epilot_automation" "auto_1" {
journey_id = "\${epilot_journey.sample.journey_id}"
}`;
const issues = runRule(tf);
expect(issues).toHaveLength(0);
});

it('does NOT flag variable references', () => {
const tf = `
resource "epilot_journey" "j1" {
manifest = "\${var.manifest_id}"
}`;
const issues = runRule(tf);
expect(issues).toHaveLength(0);
});
});
Loading
Loading