From 05d3de2ef122356673d7ab914070e57481bfc9ea Mon Sep 17 00:00:00 2001 From: Franco Rezabek <43790446+rezabekf@users.noreply.github.com> Date: Fri, 18 Sep 2026 10:56:00 +0000 Subject: [PATCH] fix(infra): generate event-statuses.ts from Python at build time src/shared/event_statuses.py and infra/constants/event-statuses.ts held independently maintained copies of the same status strings, with no compiler or test link between them. The Step Functions workflow polls DynamoDB by comparing against the TypeScript copy while Lambda writes the Python copy, so a rename on the Python side wasn't a build error - it was a silent 60-minute poll timeout. Add scripts/generate-event-statuses.py, which mirrors the Python EventStatus class into TypeScript, and wire it into infra's existing prebuild step (alongside prepare-lambda/package-agent) so the generated file can never drift. The generated file is gitignored, like dist/. --- .gitignore | 1 + infra/constants/event-statuses.ts | 23 -------------- infra/package.json | 3 +- scripts/generate-event-statuses.py | 43 ++++++++++++++++++++++++++ tests/test_generate_event_statuses.py | 44 +++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 24 deletions(-) delete mode 100644 infra/constants/event-statuses.ts create mode 100644 scripts/generate-event-statuses.py create mode 100644 tests/test_generate_event_statuses.py diff --git a/.gitignore b/.gitignore index 69fc375..4c6799c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ infra/node_modules/ infra/dist/ infra/cdk.out/ infra/coverage +infra/constants/event-statuses.ts # DeepEval diff --git a/infra/constants/event-statuses.ts b/infra/constants/event-statuses.ts deleted file mode 100644 index c00ebc9..0000000 --- a/infra/constants/event-statuses.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Event status constants used by Step Functions workflow - * - * The Step Function polls DynamoDB for these statuses to determine when - * the agent background task has completed or failed. - * - * Note: Other event statuses (SESSION_INITIATED, AGENT_INVOCATION_STARTED, etc.) - * are defined in Python at src/shared/event_statuses.py and used by Lambda functions. - */ - -export const EventStatus = { - /** - * Agent background task completed successfully (Step Function polls for this) - */ - AGENT_BACKGROUND_TASK_COMPLETED: 'AGENT_BACKGROUND_TASK_COMPLETED', - - /** - * Agent background task failed (Step Function polls for this) - */ - AGENT_BACKGROUND_TASK_FAILED: 'AGENT_BACKGROUND_TASK_FAILED', -} as const; - -export type EventStatusType = (typeof EventStatus)[keyof typeof EventStatus]; diff --git a/infra/package.json b/infra/package.json index 1ebf91c..8d5e44c 100644 --- a/infra/package.json +++ b/infra/package.json @@ -2,10 +2,11 @@ "name": "infra", "private": true, "scripts": { - "prebuild": "npm run package-agent && npm run prepare-lambda", + "prebuild": "npm run generate-event-statuses && npm run package-agent && npm run prepare-lambda", "build": "tsc -p tsconfig.json", "watch": "tsc -w", "clean": "rm -rf cdk.out dist", + "generate-event-statuses": "python3 ../scripts/generate-event-statuses.py constants/event-statuses.ts", "prepare-lambda": "rm -rf dist/build-lambda && mkdir -p dist/build-lambda/src && cp ../infra/lambda/*.py dist/build-lambda/ && cp ../src/__init__.py dist/build-lambda/src/ && cp -r ../src/shared dist/build-lambda/src/", "package-agent": "rm -rf dist/build-agent && bash ../scripts/build-deployment-package.sh", "synth": "npm run build && cdk synth", diff --git a/scripts/generate-event-statuses.py b/scripts/generate-event-statuses.py new file mode 100644 index 0000000..cbc8a2a --- /dev/null +++ b/scripts/generate-event-statuses.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Generate infra/constants/event-statuses.ts from src/shared/event_statuses.py. + +Usage: generate-event-statuses.py + +src/shared/event_statuses.py is the single source of truth for event status +strings. This script mirrors it into a TypeScript module so the Step +Functions workflow (infra/lib/workflow.ts) can't drift from the Python +values that actually get written to DynamoDB. +""" + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from src.shared.event_statuses import EventStatus # noqa: E402 + +HEADER = """// GENERATED FILE — do not edit by hand. +// Source of truth: src/shared/event_statuses.py +// Regenerated automatically by `npm run generate-event-statuses` (part of `npm run build`). +""" + + +def render(statuses: dict[str, str]) -> str: + entries = "\n".join(f" {name}: '{value}'," for name, value in statuses.items()) + return ( + f"{HEADER}\n" + f"export const EventStatus = {{\n{entries}\n}} as const;\n\n" + f"export type EventStatusType = (typeof EventStatus)[keyof typeof EventStatus];\n" + ) + + +def main() -> None: + output_path = Path(sys.argv[1]) + statuses = {name: value for name, value in vars(EventStatus).items() if not name.startswith("_")} + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(render(statuses)) + + +if __name__ == "__main__": + main() diff --git a/tests/test_generate_event_statuses.py b/tests/test_generate_event_statuses.py new file mode 100644 index 0000000..5c79f05 --- /dev/null +++ b/tests/test_generate_event_statuses.py @@ -0,0 +1,44 @@ +"""Unit tests for scripts/generate-event-statuses.py.""" + +import importlib.util +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT_PATH = REPO_ROOT / "scripts" / "generate-event-statuses.py" + +_spec = importlib.util.spec_from_file_location("generate_event_statuses", SCRIPT_PATH) +generate_event_statuses = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(generate_event_statuses) + + +def test_render_mirrors_provided_statuses(): + output = generate_event_statuses.render({"FOO": "FOO", "BAR": "BAR_VALUE"}) + + assert "FOO: 'FOO'," in output + assert "BAR: 'BAR_VALUE'," in output + assert "export const EventStatus" in output + assert "as const" in output + assert "export type EventStatusType" in output + + +def test_render_preserves_insertion_order(): + output = generate_event_statuses.render({"A": "1", "B": "2", "C": "3"}) + + assert output.index("A: '1'") < output.index("B: '2'") < output.index("C: '3'") + + +def test_main_mirrors_every_real_event_status(tmp_path, monkeypatch): + from src.shared.event_statuses import EventStatus + + output_path = tmp_path / "event-statuses.ts" + monkeypatch.setattr("sys.argv", ["generate-event-statuses.py", str(output_path)]) + + generate_event_statuses.main() + + content = output_path.read_text() + expected = {name: value for name, value in vars(EventStatus).items() if not name.startswith("_")} + + assert expected, "EventStatus should not be empty" + for name, value in expected.items(): + assert f"{name}: '{value}'," in content + assert "GENERATED FILE" in content