Skip to content
Draft
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
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"defaultBranch": "main"
},
"files": {
"ignoreUnknown": false
"ignoreUnknown": false,
"includes": ["**/*.ts", "**/*.js", "**/*.json", "!fixtures"]
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'includes' pattern with a negation (!fixtures) may not work as expected. Biome's includes/ignores work differently - the '!fixtures' negation should typically be in an 'ignore' field. Consider using 'ignore': ['fixtures/**'] in the files configuration instead, or verify that this pattern achieves the intended behavior of excluding fixtures from linting.

Suggested change
"includes": ["**/*.ts", "**/*.js", "**/*.json", "!fixtures"]
"includes": ["**/*.ts", "**/*.js", "**/*.json"],
"ignore": ["fixtures/**"]

Copilot uses AI. Check for mistakes.
},
"formatter": {
"enabled": true,
Expand Down
175 changes: 175 additions & 0 deletions e2e/json-parsing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { ParsePlanUseCase } from "../src/domain/usecases/ParsePlanUseCase";
import { TextFormatterUseCase } from "../src/domain/usecases/TextFormatterUseCase";

describe("E2E: JSON Parsing", () => {
describe("Plan parsing", () => {
it("should parse a plan with no changes", async () => {
const content = await readFile("./fixtures/sample-plan.json", "utf-8");
const parser = new ParsePlanUseCase();

const plan = await parser.parse(content);

expect(plan.resourceChanges).toHaveLength(0);
});

it("should parse a plan with additions", async () => {
const content = await readFile(
"./fixtures/plan-with-changes.json",
"utf-8",
);
const parser = new ParsePlanUseCase();

const plan = await parser.parse(content);

expect(plan.resourceChanges).toHaveLength(1);
expect(plan.resourceChanges[0].actions).toContain("create");
});

it("should parse a plan with updates", async () => {
const content = await readFile(
"./fixtures/plan-with-updates.json",
"utf-8",
);
const parser = new ParsePlanUseCase();

const plan = await parser.parse(content);

expect(plan.resourceChanges).toHaveLength(1);
expect(plan.resourceChanges[0].actions).toContain("update");
});

it("should parse a plan with deletions", async () => {
const content = await readFile(
"./fixtures/plan-with-deletions.json",
"utf-8",
);
const parser = new ParsePlanUseCase();

const plan = await parser.parse(content);

expect(plan.resourceChanges).toHaveLength(1);
expect(plan.resourceChanges[0].actions).toContain("delete");
});

it("should parse a plan with mixed changes", async () => {
const content = await readFile(
"./fixtures/plan-with-mixed-changes.json",
"utf-8",
);
const parser = new ParsePlanUseCase();

const plan = await parser.parse(content);

expect(plan.resourceChanges).toHaveLength(3);

const creates = plan.resourceChanges.filter((rc) =>
rc.actions.includes("create"),
);
const updates = plan.resourceChanges.filter((rc) =>
rc.actions.includes("update"),
);
const deletes = plan.resourceChanges.filter((rc) =>
rc.actions.includes("delete"),
);

expect(creates).toHaveLength(1);
expect(updates).toHaveLength(1);
expect(deletes).toHaveLength(1);
});

it("should reject malformed JSON with descriptive error", async () => {
const content = await readFile("./fixtures/malformed-plan.json", "utf-8");
const parser = new ParsePlanUseCase();

await expect(parser.parse(content)).rejects.toThrow(
"Invalid JSON in plan file",
);
});
});

describe("Text formatting", () => {
it("should format a plan with no changes", async () => {
const content = await readFile("./fixtures/sample-plan.json", "utf-8");
const parser = new ParsePlanUseCase();
const formatter = new TextFormatterUseCase();

const plan = await parser.parse(content);
const result = formatter.format(plan);

expect(result).toContain("No changes");
expect(result).toContain("0 to add");
expect(result).toContain("0 to change");
expect(result).toContain("0 to destroy");
});

it("should format a plan with additions", async () => {
const content = await readFile(
"./fixtures/plan-with-changes.json",
"utf-8",
);
const parser = new ParsePlanUseCase();
const formatter = new TextFormatterUseCase();

const plan = await parser.parse(content);
const result = formatter.format(plan);

expect(result).toContain("1 to add");
expect(result).toContain("aws_s3_bucket.example");
expect(result).toContain("create");
});

it("should format a plan with updates", async () => {
const content = await readFile(
"./fixtures/plan-with-updates.json",
"utf-8",
);
const parser = new ParsePlanUseCase();
const formatter = new TextFormatterUseCase();

const plan = await parser.parse(content);
const result = formatter.format(plan);

expect(result).toContain("1 to change");
expect(result).toContain("aws_s3_bucket.updated");
expect(result).toContain("update");
});

it("should format a plan with deletions", async () => {
const content = await readFile(
"./fixtures/plan-with-deletions.json",
"utf-8",
);
const parser = new ParsePlanUseCase();
const formatter = new TextFormatterUseCase();

const plan = await parser.parse(content);
const result = formatter.format(plan);

expect(result).toContain("1 to destroy");
expect(result).toContain("aws_s3_bucket.deleted");
expect(result).toContain("delete");
});

it("should format a plan with mixed changes", async () => {
const content = await readFile(
"./fixtures/plan-with-mixed-changes.json",
"utf-8",
);
const parser = new ParsePlanUseCase();
const formatter = new TextFormatterUseCase();

const plan = await parser.parse(content);
const result = formatter.format(plan);

expect(result).toContain("1 to add");
expect(result).toContain("1 to change");
expect(result).toContain("1 to destroy");

expect(result).toContain("aws_s3_bucket.new");
expect(result).toContain("aws_s3_bucket.updated");
expect(result).toContain("aws_s3_bucket.deleted");
});
});
});
36 changes: 18 additions & 18 deletions features/phase-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,37 +55,37 @@ This plan is designed for incremental validation, where each iteration produces

---

### Iteration 2: JSON Plan Parsing & Basic Output ⚠️
### Iteration 2: JSON Plan Parsing & Basic Output
**Goal**: Parse JSON plan files and display basic information

**Tasks**:
- ✅ Define domain entities for Terraform plan structure (Plan, ResourceChange, etc.)
- ✅ Create parser interface in domain layer
- ✅ Implement JSON plan parser use case following CLEAN architecture
- Create text formatter interface and implementation
- Use `@actions/core.summary` to output results as action step summary
- ⚠️ Add error handling for malformed JSON with descriptive error messages (partial - throws on JSON.parse but lacks descriptive messages)
- Create text formatter interface and implementation
- Use `@actions/core.summary` to output results as action step summary
- Add error handling for malformed JSON with descriptive error messages

**TDD Approach**: ⚠️
**TDD Approach**:
1. ✅ Write failing test for parsing valid JSON plan
2. ✅ Implement parser to make test pass
3. ✅ Write failing test for malformed JSON handling
4. ⚠️ Implement error handling (partial)
5. Write failing test for text formatting
6. Implement formatter
4. Implement error handling
5. Write failing test for text formatting
6. Implement formatter
7. ✅ Run `npm test` after each change

**E2E Test**:
- Script: `npm run test:e2e:json-parsing` (not found in package.json)
- Uses Vitest with fixtures for various scenarios (additions, deletions, updates, no changes)
- Validates correct parsing and change detection
- Tests error handling with malformed JSON fixtures
- Uses Chance.js for generating random test data where values don't matter
**E2E Test**:
- Script: `npm run test:e2e:json-parsing`
- Uses Vitest with fixtures for various scenarios (additions, deletions, updates, no changes)
- Validates correct parsing and change detection
- Tests error handling with malformed JSON fixtures
- Uses Chance.js for generating random test data where values don't matter

**Validation**:
- Action outputs a text summary of changes for a sample JSON plan
- Summary includes count of resources being added/changed/deleted
- Can be verified in GitHub Actions workflow summary using `@github/local-action`
**Validation**:
- Action outputs a text summary of changes for a sample JSON plan
- Summary includes count of resources being added/changed/deleted
- Can be verified in GitHub Actions workflow summary using `@github/local-action`
- ✅ `npx biome check` passes
- ✅ All tests pass with `npm test`

Expand Down
1 change: 1 addition & 0 deletions fixtures/malformed-plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ invalid json }
26 changes: 26 additions & 0 deletions fixtures/plan-with-deletions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"format_version": "1.0",
"terraform_version": "1.5.0",
"planned_values": {
"root_module": {
"resources": []
}
},
"resource_changes": [
{
"address": "aws_s3_bucket.deleted",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "deleted",
"change": {
"actions": ["delete"],
"before": {
"bucket": "old-bucket",
"acl": "private"
},
"after": null
}
}
],
"configuration": {}
}
57 changes: 57 additions & 0 deletions fixtures/plan-with-mixed-changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"format_version": "1.0",
"terraform_version": "1.5.0",
"planned_values": {
"root_module": {
"resources": []
}
},
"resource_changes": [
{
"address": "aws_s3_bucket.new",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "new",
"change": {
"actions": ["create"],
"before": null,
"after": {
"bucket": "new-bucket",
"acl": "private"
}
}
},
{
"address": "aws_s3_bucket.updated",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "updated",
"change": {
"actions": ["update"],
"before": {
"bucket": "existing-bucket",
"acl": "private"
},
"after": {
"bucket": "existing-bucket",
"acl": "public-read"
}
}
},
{
"address": "aws_s3_bucket.deleted",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "deleted",
"change": {
"actions": ["delete"],
"before": {
"bucket": "old-bucket",
"acl": "private"
},
"after": null
}
}
],
"configuration": {}
}
29 changes: 29 additions & 0 deletions fixtures/plan-with-updates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"format_version": "1.0",
"terraform_version": "1.5.0",
"planned_values": {
"root_module": {
"resources": []
}
},
"resource_changes": [
{
"address": "aws_s3_bucket.updated",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "updated",
"change": {
"actions": ["update"],
"before": {
"bucket": "my-bucket",
"acl": "private"
},
"after": {
"bucket": "my-bucket",
"acl": "public-read"
}
}
}
],
"configuration": {}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "npm run test:e2e:file-reading",
"test:e2e": "npm run test:e2e:file-reading && npm run test:e2e:json-parsing",
"test:e2e:file-reading": "vitest run e2e/file-reading.test.ts",
"test:e2e:json-parsing": "vitest run e2e/json-parsing.test.ts",
"build": "ncc build src/index.ts -o dist --minify --no-source-map-register",
"lint": "biome check .",
"lint:fix": "biome check --write .",
Expand Down
5 changes: 5 additions & 0 deletions src/domain/usecases/ITextFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Plan } from "../entities/Plan";

export interface ITextFormatter {
format(plan: Plan): string;
}
Loading
Loading