diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5170d3..27450ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v6 - name: Set up pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Set up Node.js uses: actions/setup-node@v6 @@ -38,8 +38,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Verify application - run: pnpm verify + - name: Typecheck + run: pnpm typecheck + + - name: Run unit tests + run: pnpm test + + - name: Build application + run: pnpm build - name: Set up Terraform uses: hashicorp/setup-terraform@v4 diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 3b2d213..eb46645 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -16,6 +16,7 @@ env: ECR_REGISTRY: 401696231252.dkr.ecr.ap-southeast-1.amazonaws.com API_REPOSITORY: mem9-node-prod-api WORKER_REPOSITORY: mem9-node-prod-worker + API_HEALTH_URL: https://napi.mem9.ai/health/ready TF_STATE_BUCKET: mem9-node-prod-terraform-state TF_STATE_KMS_KEY_ARN: arn:aws:kms:ap-southeast-1:401696231252:key/4cd4c7a1-c1a6-47ad-9205-548678b84d86 TERRAFORM_VERSION: 1.15.4 @@ -242,6 +243,20 @@ jobs: --services "$API_SERVICE" "$WORKER_SERVICE" \ --region "$AWS_REGION" + - name: Verify production API health + run: | + curl \ + --fail-with-body \ + --silent \ + --show-error \ + --retry 10 \ + --retry-delay 5 \ + --retry-all-errors \ + --connect-timeout 5 \ + --max-time 10 \ + "$API_HEALTH_URL" \ + | jq -e '.status == "ready"' + - name: Record successful release run: | API_TASK_DEFINITION=$(terraform -chdir="$TF_WORKING_DIR" output -raw api_task_definition_arn) diff --git a/apps/api/src/deep-analysis-source-preparation.service.spec.ts b/apps/api/src/deep-analysis-source-preparation.service.spec.ts index d7473c4..547a408 100644 --- a/apps/api/src/deep-analysis-source-preparation.service.spec.ts +++ b/apps/api/src/deep-analysis-source-preparation.service.spec.ts @@ -78,6 +78,9 @@ describe('deep analysis source preparation service', () => { enqueueLlmMessage: jest.fn(), } as never, ); + const errorSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(() => undefined); await service.prepareAndEnqueue({ reportId: 'dar_1', @@ -94,5 +97,6 @@ describe('deep analysis source preparation service', () => { errorCode: 'DEEP_ANALYSIS_SOURCE_PREP_FAILED', }), ); + expect(errorSpy).toHaveBeenCalled(); }); }); diff --git a/apps/api/src/memory-analysis/memory-analysis.service.spec.ts b/apps/api/src/memory-analysis/memory-analysis.service.spec.ts index 8d3914d..a8b0a39 100644 --- a/apps/api/src/memory-analysis/memory-analysis.service.spec.ts +++ b/apps/api/src/memory-analysis/memory-analysis.service.spec.ts @@ -446,6 +446,9 @@ describe('memory analysis report service', () => { throw new Error('sqs down'); }), }); + const errorSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(() => undefined); await expect(service.createReport(reportContext, { createdAfter: '2026-06-01T00:00:00.000Z', @@ -459,6 +462,7 @@ describe('memory analysis report service', () => { reportStage: 'failed', failCode: 'MEMORY_ANALYSIS_QUEUE_ENQUEUE_FAILED', })); + expect(errorSpy).toHaveBeenCalled(); }); it('lists memory analysis reports by type', async () => { diff --git a/apps/worker/src/deep-analysis-report-processor.service.spec.ts b/apps/worker/src/deep-analysis-report-processor.service.spec.ts index 6073848..88ab1da 100644 --- a/apps/worker/src/deep-analysis-report-processor.service.spec.ts +++ b/apps/worker/src/deep-analysis-report-processor.service.spec.ts @@ -1122,12 +1122,18 @@ describe('deep analysis report processor service', () => { createConfig() as never, ); + const errorSpy = jest + .spyOn(processor['logger'], 'error') + .mockImplementation(() => undefined); + await processor.process({ messageType: 'deep_report', reportId: 'dar_trim_failure', traceId: 'trace_trim_failure', }); + expect(errorSpy).toHaveBeenCalled(); + expect(repository.updateDeepAnalysisReport).toHaveBeenLastCalledWith( 'dar_trim_failure', expect.objectContaining({ diff --git a/apps/worker/src/memory-analysis-report-runner.service.spec.ts b/apps/worker/src/memory-analysis-report-runner.service.spec.ts index 98c7922..c9ae07d 100644 --- a/apps/worker/src/memory-analysis-report-runner.service.spec.ts +++ b/apps/worker/src/memory-analysis-report-runner.service.spec.ts @@ -119,6 +119,9 @@ describe('MemoryAnalysisReportRunnerService', () => { repository as never, new FakeRedis() as never, ); + const errorSpy = jest + .spyOn(runner['logger'], 'error') + .mockImplementation(() => undefined); await runner.generateReport(reportContext, 1, { createdAfter: '2026-06-01T00:00:00.000Z', @@ -131,6 +134,7 @@ describe('MemoryAnalysisReportRunnerService', () => { failCode: 'QWEN_NOT_CONFIGURED', failReason: 'Qwen API key or model is not configured.', })); + expect(errorSpy).toHaveBeenCalled(); }); it('uses the cache version for period summary cache lookups and writes', async () => { diff --git a/apps/worker/src/sqs-consumer.service.spec.ts b/apps/worker/src/sqs-consumer.service.spec.ts index 611bb91..488de00 100644 --- a/apps/worker/src/sqs-consumer.service.spec.ts +++ b/apps/worker/src/sqs-consumer.service.spec.ts @@ -128,6 +128,9 @@ describe('sqs consumer service', () => { } as never, createConfig(), ); + const errorSpy = jest + .spyOn(consumer['logger'], 'error') + .mockImplementation(() => undefined); const setIntervalSpy = jest.spyOn(global, 'setInterval'); consumer['running'] = true; @@ -140,6 +143,7 @@ describe('sqs consumer service', () => { expect(Sentry.captureException).toHaveBeenCalledWith( expect.any(SyntaxError), ); + expect(errorSpy).toHaveBeenCalled(); }); it('keeps consuming after one processor failure', async () => { @@ -205,6 +209,9 @@ describe('sqs consumer service', () => { } as never, createConfig(), ); + const errorSpy = jest + .spyOn(consumer['logger'], 'error') + .mockImplementation(() => undefined); consumer['running'] = true; await consumer['consumeBatchLoop'](); @@ -214,6 +221,7 @@ describe('sqs consumer service', () => { expect(queue.deleteMessage).toHaveBeenCalledWith('rh_2'); expect(Sentry.captureException).toHaveBeenCalledTimes(1); expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error)); + expect(errorSpy).toHaveBeenCalled(); }); it('dispatches memory analysis report llm messages', async () => {