Skip to content

fix(ci): get Frigg CI green on next - #710

Merged
seanspeaks merged 44 commits into
nextfrom
fix/ci-green-next
Oct 24, 2026
Merged

seanspeaks merged 44 commits into
nextfrom
fix/ci-green-next

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Gets Frigg CI green on next (runbook G0). On next, CI had failed on every recent run. Core's suite never ran, and devtools had about 30 failing suites. The tests had gone stale while CI stayed red, and a few real bugs were hiding underneath.

Mongo root cause and fix

  • Root cause. mongodb-memory-server 8.x defaults to MongoDB 5.0.19. Its only Linux builds target Ubuntu 20.04 and link OpenSSL 1.1. ubuntu-latest is now 24.04, which has no libcrypto.so.1.1. Core's jest globalSetup started that server before every run, so a failed start (StdoutInstanceError) meant none of core's ~200 suites ran.
  • Fix, in two parts:
    1. Core's globalSetup no longer starts MongoDB at all. Nothing in the default suite used it; repository tests mock Prisma. It keeps only the STAGE=dev default. Unit suites must not need a database.
    2. @friggframework/test now uses mongodb-memory-server 11. TestMongo pins MongoDB 8.0.17, which has native ubuntu2404 builds; MONGOMS_VERSION still overrides it. TestMongo stays as an opt-in helper.
  • The lockfile diff is limited to the mongodb-memory-server subtree, its mongodb driver and bson. Unrelated peer-flag churn was left out.

Real-database tests split out (*.db.test.js, npm run test:db)

  • The suites that talk to real databases are now named *.db.test.js. test:all excludes them, and a new parallel db job runs them:
    • the three encryption fix-verification suites;
    • the two repository parity suites, which run every adapter (MongoDB, DocumentDB-on-Mongo, PostgreSQL) through the same cases, about 1,400 tests;
    • a TestMongo smoke test.
  • The db job provides MongoDB 8.0 as a replica set (Prisma needs one for MongoDB) and a postgres:16 service, with migrations applied and the mongodb-memory-server binary cached.
  • Follow-up: per-port repository contract tests. That means one shared suite per repository interface, run against each adapter (Mongo, Postgres, and Mongo standing in for DocumentDB), plus in-memory fakes for use-case tests. The parity suites are the seed of that.
  • There is no tenant-isolation suite on next yet. The Postgres service in the db job is what it will use when it lands (runbook DG3); it only needs FRIGG_TEST_POSTGRES_URL, which the job already sets.

Bugs found by the revived tests

  • fix(core): API-module encryption.credentialFields were never registered. A wrong relative require path threw, and the error was swallowed at debug level, so those credential fields were stored unencrypted. Fixing the path exposed two more problems, both fixed:

    • a module listing a core field (e.g. api_key) failed validation and dropped every module's fields;
    • module fields replaced the app's own custom schema instead of merging with it.

    Values already stored as plaintext still read back.

  • fix(core): QueuerUtil.batchSend reused and cleared one array across every SQS batch command.

  • fix(devtools): VpcBuilder attached only the first of vpc.securityGroupIds.

  • fix(devtools): VpcBuilder applied legacy VPC options under managementMode: 'managed' even though it warns that it ignores them. Managed + isolated with no stack VPC then failed.

Stale tests updated

Each commit names the change that made the test stale. In short:

  • core: encryption registry and DocumentDB envelope format, { externalId, type } API, Boom status codes, 32-byte keys, AWS SDK v3 SQS mock, deterministic timing, and Node 22 warnings in the require-graph probe.
  • devtools:
    • composer and postgres tests rewritten for the domain-builder composer (they required modules deleted in 7b36f0f);
    • composer tests no longer run a real npm install into layers/prisma, and template-generation no longer runs real AWS discovery;
    • health, VPC and discovery tests updated to the current shapes;
    • frigg-cli tests: Jest 30, timezone, a hung deploy, a 42 s UI suite now 0.15 s, git identity on Ubuntu;
    • a jest config that stops collecting helper files and management-ui's Vitest suites.

Skips (both describe.skip with a comment and an issue)

Suite Tests Reason Issue
devtools/infrastructure/scripts/build-time-discovery.test.js 23 Its subject requires the deleted aws-discovery module and nothing calls it; decide whether to delete or port it #708
devtools/.../orphan-detection-relationship-analysis.test.js 4 Red-phase TDD spec for a feature that was never implemented #709

Also not in this PR:

  • Two test files were deleted rather than skipped, because their subjects no longer exist: devtools/frigg-cli/index.test.js and devtools/infrastructure/integration.test.js.
  • management-ui's Vitest suites are excluded from devtools' jest. management-ui isn't an npm workspace, so its dependencies are never installed; running them in CI is a follow-up.
  • Pre-existing skips are untouched: 2 it.skip in core's requester tests, plus one skipped suite and others in devtools.

Workflow changes (one commit each, so any can be dropped)

  • (f) on.push includes next.
  • (a) Node matrix is 22.x and 24.x, with fail-fast: false.
  • speed: shallow checkout (the full fetch pulled thousands of tags), setup-node npm cache, no global npm reinstall, and newer pushes cancel in-flight runs.
  • (b) lint:fix plus git-auto-commit is replaced by a read-only lint job (prettier --check + eslint). It is non-blocking (continue-on-error) because the backlog is large: core alone has about 390 files that need prettier and 96 eslint errors. No bot pushes to PR branches any more.
  • DB job: see above.
  • (c) test-cli.yml is retired. It failed on every run: wrong path (packages/frigg-cli moved) and Node 18/20/21. Its unit tests already run in test:all. The frigg <cmd> --help checks are now a step in Frigg CI. The Windows/macOS legs and the Codecov gate are dropped.
  • (d) codesee-arch-diagram.yml is left as is. CodeSee's action repo turned the action into a no-op and then reverted that; runs still succeed and codesee.io still responds. I couldn't confirm the service is defunct, so that's the maintainer's call.
  • release.yml and codeql.yml are untouched.

Review follow-ups

  • These follow-ups came out of the adversarial review; the review reply has per-item detail.
    • Encryption registry: errors are no longer swallowed at debug level, and an invalid encryption config fails closed in deployed stages.
    • Prisma encryption path: it no longer mistakes plaintext containing colons for ciphertext, and values keep their type (a numeric string stays a string).
    • CI concurrency: runs are cancelled only for superseded PR pushes, never on next.
    • Route-association tests: the disputed tests are skipped pending devtools: VpcBuilder renames private subnet route-table associations on existing stacks (upgrade risk) #714.
    • MongoDB binary: @friggframework/test now uses mongodb-memory-server-core, so nothing downloads a binary at install time.
    • Smaller CI and docs fixes: CI and TESTING.md nits.
  • QueuerUtil.batchSend (cd616a5): this fix only affects what tests and other observers see. The SQS requests were already correct, so it isn't a production fix.
  • Not changed here, needs an issue: vpc.selfHeal: false no longer throws when an existing NAT gateway is misplaced. Since 7b36f0f, VpcBuilder silently creates a new, billable NAT gateway instead (vpc-builder.js ~240, ~1706). The removed composer tests covered that guard; the vpc-builder tests cover NAT placement but not this.

Test plan

  • npm run test:all passes locally on Node 22 and 24.
  • npm run test:db passes against mongo:8.0 (replica set) and postgres:16.
  • devtools jest run twice; layers/prisma stays clean.
  • Frigg CI green on this PR (build 22.x / 24.x, db, lint).

🤖 Generated with Claude Code

https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7


Note

High Risk
Changes touch credential encryption registration, envelope handling, and production startup on invalid encryption config—areas where mistakes could leave secrets plaintext or block deploys; CI workflow changes also gate every next publish.

Overview
Restructures Frigg CI so next can go green: pushes on next, Node 22/24 matrix, PR concurrency cancellation, read-only tokens, a dedicated db job (Mongo replica set + Postgres, npm run test:db), read-only lint (non-blocking), and a frigg --help command-list smoke test; test-cli.yml is removed. Docs describe *.db.test.js and local DB setup.

Core test infrastructure no longer starts in-memory Mongo in global setup (fixes Ubuntu 24 / mongodb-memory-server breakage); real DB suites move to *.db.test.js / jest.db.config.js. Lockfile bumps mongodb-memory-server (and driver stack).

Field-level encryption gets stricter Cryptor envelope detection (legacy plaintext with colons no longer treated as ciphertext), JSON type-preserving encrypt/decrypt, shared isEncryptedEnvelope, and startup fails closed on invalid app encryption config in deployed stages. Module encryption.credentialFields registration is fixed (correct require path, per-field validation, merge with app custom schema, skip core duplicates)—fields that were silently unencrypted should encrypt on new writes; plaintext legacy values still read.

findIntegrationContextByExternalEntityId now takes { externalId, type } (docs/tests updated). QueuerUtil.batchSend copies each SQS batch instead of clearing a shared Entries array. Many stale unit/integration tests are updated (AWS SDK v3 SQS, Boom assertions, timing mocks, etc.).

Reviewed by Cursor Bugbot for commit 7cfcf12. Bugbot is set up for automated code reviews on this repo. Configure here.

📦 Published PR as canary version: 2.0.0--canary.710.7cfcf12.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/core@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/devtools@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/eslint-config@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/prettier-config@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/schemas@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/serverless-plugin@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/test@2.0.0--canary.710.7cfcf12.0
npm install @friggframework/ui@2.0.0--canary.710.7cfcf12.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/core@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/devtools@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/eslint-config@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/prettier-config@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/schemas@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/test@2.0.0--canary.710.7cfcf12.0
yarn add @friggframework/ui@2.0.0--canary.710.7cfcf12.0

seanspeaks and others added 30 commits October 23, 2026 23:07
mongodb-memory-server 8.x defaults to MongoDB 5.0.19, whose only Linux
builds target Ubuntu 20.04 and link against OpenSSL 1.1. GitHub's
ubuntu-latest is now 24.04, which ships OpenSSL 3 only, so every core run
died in globalSetup with `StdoutInstanceError: ... "libcrypto.so.1.1"` and
no core suite ran.

mongodb-memory-server 11 resolves native ubuntu2404 builds for MongoDB
8.x. TestMongo now pins the binary (8.0.17) explicitly instead of
inheriting the library default, so a future library bump can't silently
change the server version; MONGOMS_VERSION still overrides it.

The lockfile change is limited to the mongodb-memory-server subtree (and
its mongodb driver/bson); unrelated `peer` flag churn from a different npm
version was left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Core's jest globalSetup delegated to @friggframework/test, which starts a
MongoMemoryServer and exports MONGO_URI before any suite runs. Nothing in
the default core suite talks to that server (repository tests mock Prisma;
prisma.test.js only checks env handling), yet when the binary couldn't
start the whole core suite (~200 files) was skipped. Unit suites must not
need a database.

The core setup now only applies the STAGE=dev default and restores the
environment on teardown. TestMongo and @friggframework/test's own
globalSetup are unchanged for apps that opt in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…st:db`

Three encryption suites connect to a live MongoDB replica set or
PostgreSQL through generated Prisma clients instead of mocking Prisma:
mongo-decryption-fix-verification, postgres-decryption-fix-verification
and postgres-relation-decryption. In the default run they failed with
"Cannot find Prisma client" (or would have needed a database), so they
are now DB integration tests:

- renamed to *.db.test.js and ignored by the default jest config;
- run by `npm run test:db` (jest.db.config.js, runInBand);
- connection strings come from FRIGG_TEST_MONGODB_URL /
  FRIGG_TEST_POSTGRES_URL (defaults unchanged), so one run can reach both
  databases without fighting over DATABASE_URL.

All 16 tests pass against mongo:8.0 (replica set) and postgres:16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
loadModuleEncryptionSchemas required '../integrations/utils/map-integration-dto'
from database/encryption/, which resolves to a path that doesn't exist.
loadCustomEncryptionSchema swallows errors at debug level, so module-level
`encryption.credentialFields` declarations were silently never registered
and those credential fields were stored unencrypted. The tests for this
(never run in CI because core's globalSetup failed) caught it.

With the path fixed, two more problems surfaced and are fixed here:
- A module listing a field the core schema already encrypts (e.g.
  `api_key`) failed validation ("Cannot override core encrypted field"),
  dropping every field from every module. Core fields are now filtered out.
- registerCustomSchema replaces the custom schema, so module fields would
  have wiped the app's own `encryption.schema`. Module fields are now merged
  into the existing custom schema.
Integrations without `Definition.modules` are skipped instead of throwing.

Values already stored in plaintext still read back: decryption passes
through values that aren't in the encrypted format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
9d746f1 (feat(encryption): add API key encryption) added data.api_key,
data.apiKey, data.API_KEY_VALUE, data.password and data.client_secret to
CORE_ENCRYPTION_SCHEMA.Credential on purpose; this assertion still listed
only the three OAuth token fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
… format

DocumentDBEncryptionService only encrypts/decrypts values it recognises as
Cryptor envelopes (four base64 parts, > 50 chars) and only for fields in
the schema registry. The test's mock cryptor emitted "encrypted:<value>"
and the suite assumed User.username was encrypted without registering it,
so 11 assertions failed once core tests ran again.

The mock now emits a keyId:iv:ciphertext:encryptedKey envelope (plaintext
as the base64 ciphertext) and the suite registers User.username as a
custom field, the way an app opts in via appDefinition.encryption.schema.
Assertions are otherwise unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Three assertions tested the mocks rather than the repository:
- "stores encrypted mapping": the find mock always returned a document, so
  upsertMapping took the update path and never inserted. The first lookup
  now returns no match.
- "preserves other fields during update": read `command.update.$set`, but
  `update` is the collection name; the update document is
  `command.updates[0].u` (see documentdb-utils updateOne).
- "corrupted ciphertext": 'keyId:invalid-iv:...' isn't base64, so the
  service correctly treats it as legacy plaintext and returns it. The test
  now corrupts a real envelope (unknown key id) and expects the throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
aes-256-ctr needs a 32-byte key. The test's data key
('test-key-32-bytes-long-exactly') and AES_KEY
('test-aes-key-32-bytes-long-123') are 30 bytes, so createCipheriv threw
"Invalid key length". The "key not found" case passed a 3-part string, so
decrypt failed on the missing 4th part before it reached the key lookup.
Both fixtures now match the real formats; Cryptor itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…ests

The x-frigg-headers and adopter-JWT use cases throw
Boom.badRequest(<explanation>) / Boom.notImplemented(<explanation>). The
tests expected the *default* message ("Bad Request" / "Not Implemented")
as a substring, which only holds when no message is given, so they failed
on every Node version. They now assert output.statusCode (400 / 501),
which is what the tests are named for; the message-content tests already
exist alongside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…, type } API

1829e7d renamed externalEntityId to externalId and 29890e5 made
findIntegrationContextByExternalEntityId take { externalId, type } and
pick the integration by config.type (a7bacab) via the entity binding
(326f2a7), dropping the ENTITY_USER_NOT_FOUND path. The use-case and
command tests still passed { externalEntityId } and expected the old
error codes. They now cover EXTERNAL_ID_REQUIRED, TYPE_REQUIRED,
ENTITY_NOT_FOUND, INTEGRATION_NOT_FOUND (no integration of that type on
the entity) and the success path. The commands README examples were
still positional and are updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
batchSend reused one buffer for every SendMessageBatchCommand and cleared
it in place with splice after each send. Every command therefore pointed
at the same array, so after the loop all earlier commands' Entries held
the final partial batch. The request had already gone out, so delivery
was correct, but anything holding the command (SDK middleware, retries,
the test's aws-sdk-client-mock recorder) saw the wrong entries; the
"10 + 10 + 5" test failed with 5/5/5. Each full batch is now spliced out
into its own array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
2ef107f (feat(database): add DocumentDB support) made documentdb a valid
dbType and the message "dbType must be postgresql, mongodb, or
documentdb"; the test still matched the two-database wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
The postgres and mongodb "should measure actual response time" tests
slept 30/50 ms with setTimeout and asserted the measured time was >= the
delay. Timers can fire a millisecond early relative to Date.now() (seen
locally: 29 ms), and loaded runners can blow the upper bound. The tests
now stub Date.now() around an immediately-resolving ping and assert the
exact difference, which is what pingDatabase computes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
QueuerUtil moved to @aws-sdk/client-sqs, but this test still mocked
`aws-sdk` (v2) SQS, so every queueing step made a real SQSClient call and
failed with "Could not load credentials from any providers". It now uses
aws-sdk-client-mock on SQSClient (as queuer-util.test.js does) and reads
the SendMessageCommand inputs.

Two expectations were also stale:
- The default ON_WEBHOOK handler no longer logs "Webhook received:" (debug
  logging was removed in b74f21e); it's a documented no-op, so the test
  asserts dispatch resolves.
- The fixture's onWebhookReceived passed `req.params.integrationId`
  through as undefined; it now normalises to null like the base handler,
  which is what "webhooks without integration ID" checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
The ADR-048 require-graph probe reports CLEAN/DIRTY on the child's
stderr. On Node 22, requiring core's index emits a DEP0040 (punycode)
deprecation warning on stderr too, so the assertion saw
"CLEAN(node:…) [DEP0040] …" and failed on the 22.x leg only. The probe
now runs the child with --no-warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
TestMongo is now opt-in (core no longer starts it), so nothing in the
default run would notice if the pinned binary stopped downloading or
starting. mongodb.db.test.js starts it and checks MONGO_URI accepts a
connection. It follows the same convention as core: `*.db.test.js` is
excluded from `npm test` and run by `npm run test:db` (the CI DB job).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
`next` is where PRs land and pre-releases are cut from, but CI only ran
on pushes to main, so next itself never had a status of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
2.0.1 supports Node 22 and 24, so CI covers both. fail-fast is off so a
failure on one line still reports the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
- fetch-depth: 0 was there for super-linter, which this workflow no
  longer runs; the full fetch pulled thousands of release tags (~2k lines
  of log) on every run. Default shallow checkout instead.
- The hand-rolled ~/.npm cache is what `actions/setup-node` `cache: npm`
  does, keyed on the lockfile.
- `npm run use:npm` reinstalled npm globally to satisfy engines.npm
  ">=10"; Node 22 and 24 already ship npm >= 10.
- A newer push to the same ref cancels the in-flight run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
The build job ran `npm run lint:fix --workspaces` and then
git-auto-commit-action pushed "Apply lint auto-fixes" to the branch. A
bot must not push to PR branches (it rewrites contributors' branches,
and its commits don't re-trigger CI, so they land untested), and it
only ran after tests passed, which they never did.

Lint is now its own job: `prettier --check` + `eslint` in each package
that defines lint:fix, with no writes. The step is continue-on-error
because the existing backlog is large (core alone: ~390 files need
prettier, 96 eslint errors); the job shows the findings without blocking
merges. Flip it to blocking once the backlog is cleared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Real-database suites are now `*.db.test.js`, excluded from test:all and
run by `npm run test:db`. This job provides what they need:
- MongoDB 8.0 started as a single-node replica set (Prisma requires one
  for MongoDB writes; service containers can't pass --replSet);
- a postgres:16 service, with the prisma-postgresql migrations applied;
- generated Prisma clients;
- a cache for the mongodb-memory-server binary used by TestMongo's smoke
  test (MONGOMS_DOWNLOAD_DIR points it at the cached path).
It runs in parallel with the unit matrix. The Postgres service is also
what the tenant-isolation suite (FRIGG_TEST_POSTGRES_URL) needs when it
lands on next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
test-cli.yml failed on every run: it cd's into packages/frigg-cli, which
moved to packages/devtools/frigg-cli, and its matrix still tested Node
18/20/21, which 2.0.1 doesn't support. What it ran:
- unit/integration tests and coverage: already run by devtools' jest in
  test:all (frigg-cli/__tests__), so dropped here;
- test:e2e / test:unit / test:integration / test:coverage scripts: no
  longer exist in the CLI package;
- `frigg <command> --help` checks: kept, as a step in the build job, for
  every current command;
- Windows/macOS legs: dropped. Nothing else in CI covers them and they
  never ran green; re-add a targeted OS matrix if cross-platform CLI
  support becomes a requirement;
- Codecov upload with fail_ci_if_error and an 85% coverage gate: dropped
  (no token configured, gate never met).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
integration-repository-entities-parity and
integration-mapping-repository-query-parity run every adapter (MongoDB,
DocumentDB on MongoDB, PostgreSQL) through the same cases, but only when
their *_PARITY_* URLs are set, which no CI job did; in test:all they
showed up as 476 skipped tests. They are renamed to *.db.test.js (still
env-gated) and the DB job sets their URLs, so they now run on every PR:
1,422 tests, green locally against mongo:8.0 (replica set) and
postgres:16. These are the seed of per-port repository contract tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
VpcBuilder's EXTERNAL security-group branch read decision.physicalId,
which createExternalDecision sets to the first ID only (the full list is
in physicalIds), so all but the first of vpc.securityGroupIds were
silently dropped from the Lambda VpcConfig. Use physicalIds when present.
Covered by the composer test's use-existing security-group case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…'managed'

warnIgnoredOptions() tells the user that vpc.management,
subnets.management, natGateway.management and shareAcrossStages are
ignored in managed mode, but translateLegacyConfig still applied them.
With managed + isolated and no VPC in the stack, a leftover
vpc.management flipped ownership back to 'auto' and the build failed
with "VPC discovery failed: No VPC found". The four options are now
applied only outside managed mode; natGateway.id handling is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…ec.js

devtools had no jest config, so Jest's default testMatch treated every
file under __tests__/ as a suite: six fixture/helper modules
(infrastructure/__tests__/{fixtures,helpers}, frigg-cli/__tests__/utils)
failed with "must contain at least one test". It also collected
management-ui's Vitest/JSX suites, which need their own runner and
dependencies (management-ui is not an npm workspace, so npm ci never
installs them). management-ui and the vendored Prisma layer are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
… never build the Prisma layer or call AWS

- A composer test spied fs.existsSync to return false and failed before
  mockRestore(), so every later test saw no .build-complete marker and
  ensurePrismaLayerExists() ran a real npm install into the tracked
  layers/prisma (~40 s, dirty tree). template-generation had no mocks and
  ran real AWS discovery. Both now stub prisma-layer-manager and
  gatherDiscoveredResources, as scaffold-default-infrastructure.test.js
  already does; the suites take under a second.
- infrastructure-composer.test.js still targeted the pre-refactor
  monolith (7b36f0f): it required the deleted aws-discovery module and
  asserted old logical IDs (VPCEndpointS3, FriggNATRoute), the
  jetpack/kms-grants plugins, a manual dbMigrate function (replaced in
  3bae110) and composer-thrown validation messages. It now asserts the
  current IDs (FriggS3VPCEndpoint, FriggPrivateRoute,
  FriggVPCEndpointSecurityGroup), the KMS env-var fallback, the migration
  router/worker pipeline and the orchestrator's validation flow.
- 15 tests were removed because their behaviour moved to a builder and is
  covered there: NAT placement/management (vpc-builder.test.js,
  vpc-resolver.test.js), discovery gating (resource-discovery.test.js),
  plus a CIDR fallback removed from source and one duplicate.
- template-generation's VPC cases use management: 'create-new'; with no
  VPC found, 'auto' is an explicit error since 37a354e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…leted serverless-template

Both suites failed to load since the hexagonal refactor (7b36f0f)
removed aws-discovery.js and serverless-template.js.
- postgres-config.test.js is rewritten as a composer-level check of the
  current AuroraBuilder contract: off, external, managed (cluster,
  instance, snapshot policy, private subnet group, URL-safe secret,
  DATABASE_URL, IAM, 5432 ingress), custom settings, use-existing,
  discover found/not found, invalid mode, and skipping Aurora under
  frigg start. Discovery and the Prisma layer are stubbed.
- integration.test.js is deleted: its subject (BuildTimeDiscovery env
  plumbing into a synchronous serverless-template with
  ${self:custom.vpc...} strings) no longer exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
… and reconciliation

- Progress monitors: the .rejects matcher was attached after advancing
  fake timers, so Jest 30 reported an unhandled rejection; attach first.
- Drift mocks used camelCase propertyPath; CloudFormation's
  PropertyDifference is PropertyPath: '/X'.
- Follow intentional source changes: Lambda resource-mode reconciliation
  reports supportsResourceUpdate: true (6a0bba7); the orphan scan covers
  EC2, RDS and KMS (6c75188), so all three clients are mocked; import
  de-dup only warns about duplicates dropped for the same logical ID
  (6a0bba7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 24, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@netlify

netlify Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for friggframework-org canceled.

Name Link
🔨 Latest commit 7cfcf12
🔍 Latest deploy log https://app.netlify.com/projects/friggframework-org/deploys/6adc33fdf68b9e75ae55a5b8

@gitguardian

gitguardian Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

# can't take the --replSet argument, so start it directly.
- name: Start MongoDB replica set 🍃
run: |
docker run -d --name mongo -p 27017:27017 mongo:8.0 --replSet rs0 --bind_ip_all
node-version: 22.x
cache: npm

- name: Install Dependencies 🛠
Comment thread .github/workflows/frigg-ci.js.yml Fixed
Comment thread .github/workflows/frigg-ci.js.yml Fixed
Comment thread .github/workflows/frigg-ci.js.yml Fixed
node-version: 22.x
cache: npm

- name: Install Dependencies 🛠
Comment thread .github/workflows/frigg-ci.js.yml Fixed
Comment thread .github/workflows/frigg-ci.js.yml Fixed
Comment thread .github/workflows/frigg-ci.js.yml Fixed
Comment thread .github/workflows/frigg-ci.js.yml Fixed
The mongodb-memory-server lockfile update dropped node_modules/encoding
(an optional peer of node-fetch) along with md5-file. The npm bundled
with Node 24.21 rejects that lockfile in `npm ci` ("Missing:
encoding@0.1.14 from lock file"), although Node 22's npm accepts it.
Restore the two entries exactly as they are on next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
The db job's Postgres uses trust auth, so the workflow carries no credential
for secret scanners to flag. npx calls use --no-install so CI never fetches a
package at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7

@seanspeaks seanspeaks left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verdict: no blockers. The CI changes and most of the test updates hold up. Fix the two encryption should-fixes before this ships in a release, because the registry fix turns encryption on for fields that have only ever held plaintext.

Reviewed at 9045bbc. Everything below was checked locally, either by running it or by reading the source. Anything I couldn't confirm is labelled.

Blocking

None.

Should-fix

1. Module encryption errors are still swallowed at debug level, so one bad entry leaves every module field unencrypted. packages/core/database/encryption/encryption-schema-registry.js:274, also :149
The root cause in 2a2adff was a require error hidden by logger.debug. That handler is unchanged, so the next failure will be just as quiet. Repro: a module declares encryption: { credentialFields: ['api_key', 42] }. Then field.startsWith throws TypeError: field.startsWith is not a function at :149. loadCustomEncryptionSchema catches it at debug level, and no module field from any module gets registered. The fields are written in plaintext and nothing reaches the logs at the default level. An invalid app encryption.schema has the same effect, because it throws before modules load. Suggested fix:

  • log at warn or error when an app definition was found but loading failed;
  • validate or skip each module's entries one at a time, so one bad module can't drop the rest.

2. Some plaintext values don't survive the newly registered module fields on the Prisma path. packages/core/database/encryption/field-encryption-service.js:149-155, :239-249
The commit says plaintext values still read back. That holds on the DocumentDB service, which needs exactly 4 base64 parts and more than 50 chars, but not on the Prisma FieldEncryptionService. I checked with an AES Cryptor and a module listing webhook_secret and pin:

  • Values with 3 or more colons are treated as ciphertext. Example: 'a:b:c:d', or any URL with userinfo and a port. _isEncrypted only checks split(':').length >= 4, which causes two problems:

    • on write, _shouldEncrypt skips the value, so it stays plaintext even though the field is registered;
    • on read, cryptor.decrypt throws Encryption key not found, and prisma-encryption-extension.js doesn't catch it, so loading the whole Credential fails.

    Before this PR these module fields weren't registered, so such values read fine. After upgrading, they break existing credentials.

  • Numeric strings come back as numbers. '123456' is stored encrypted and reads back as 123456, because _deserializeAfterDecryption runs JSON.parse on it. A module that calls string methods on the field will throw.

Both problems already exist for core fields; this PR exposes every module field to them. Suggested fix: share the DocumentDB _isEncryptedValue check, and make the read path keep the value's type, at least for primitives that were strings before encryption.

3. The concurrency group also cancels CI on pushes to next. .github/workflows/frigg-ci.js.yml:15-17
group: ${{ github.workflow }}-${{ github.ref }} with cancel-in-progress: true means two quick merges to next cancel the first commit's run. release.yml publishes a prerelease on every push to next with no CI gate, so that published version never gets a CI result. Suggested fix: cancel-in-progress: ${{ github.event_name == 'pull_request' }}.

4. Two VpcBuilder tests were flipped to match the source on a disputed backwards-compatibility point. packages/devtools/infrastructure/domains/networking/vpc-builder.test.js:839-843, :1606-1611
On next, both tests asserted that a stack already containing FriggSubnet{1,2}RouteAssociation keeps those logical IDs. The second test's comment says "We MUST use these same logical IDs to avoid AlreadyExists errors". The source at vpc-builder.js:1825-1829 always emits FriggPrivateSubnet{1,2}RouteTableAssociation. The PR rewrote both tests to bless the source behaviour. The tests and that source logic came from the same commit (37a354e), so the intent isn't clear from history.

The risk is an EC2 constraint I didn't deploy to confirm. A subnet can have only one explicit route-table association, and CloudFormation creates the new logical resource before deleting the old one. So on a stack whose old associations are intact, the update can fail with Resource.AlreadyAssociated and roll back. Rather than settle this in a CI-green PR, it.skip these two with an issue, or get the 37a354e author to confirm that new IDs are intended.

Nits

  • packages/test/mongodb.js:9: the pinned version means two MongoDB downloads. The pin is 8.0.17, but mongodb-memory-server 11's postinstall downloads its own default, 8.2.6. After npm ci, 8.2.6 was in node_modules/.cache/mongodb-memory-server, and TestMongo then downloaded 8.0.17. The pinned version is never pre-fetched, and the default is fetched but never used. That happens:

    • on every CI build and lint run, which aren't cached;
    • in every app, since @friggframework/devtools has @friggframework/test as a runtime dependency.

    Pin to the library default, or set MONGOMS_VERSION / MONGOMS_DISABLE_POSTINSTALL where it matters.

  • frigg-ci.js.yml:104-108: the replica-set wait can time out without failing the step. If the node never becomes primary, the loop ends with status 0 (the last command is sleep 1), and the failure only appears later as a confusing prisma db push error. Add a check after the loop. The first loop is fine: if mongosh never answers, rs.initiate fails the step.

  • frigg-ci.js.yml:56-58: the CLI smoke loop can't catch a removed command. npx frigg nonexistent-cmd --help exits 0, because commander prints the top-level help. Also, frigg-cli/index.js:78-89 requires every command module eagerly, so frigg --help alone already proves they load. To catch a dropped or renamed command, grep the --help output for each name.

  • The build job has no permissions: block. Now that git-auto-commit is gone, add a top-level permissions: contents: read.

  • docs/TESTING.md:241-256 is stale. It still says integration tests automatically get an in-memory MongoDB with MONGO_URI. That's no longer true for core; point to *.db.test.js, npm run test:db and opt-in TestMongo.

  • cd616a5 calls the batchSend issue a delivery-path bug, but it isn't one. The old in-place splice ran after await sqs.send(...) returned, so SDK middleware and retries saw the right entries; only later observers such as the mock recorder saw 5/5/5. The fix is correct; just don't list it as a production fix in release notes.

  • 65014bc says the NAT tests it removed are "covered" in vpc-builder tests, which is only partly true. Never reusing a NAT gateway placed in a private subnet is covered (vpc-builder.test.js:850-896). Throwing when selfHeal: false is not: that guard no longer exists. The current code (vpc-builder.js:240, :1706) silently creates a new, billable NAT gateway. That changed in 7b36f0f, not here, but it deserves an issue.

Tests that no longer run, with verdicts

What Tests Verdict
devtools/infrastructure/scripts/build-time-discovery.test.js, describe.skip 23 OK. Its subject requires the deleted aws-discovery. Note that build-time-discovery.js and run-discovery.js still ship and would throw if run (#708).
orphan-detection-relationship-analysis.test.js, describe.skip 4 OK. findOrphanedResourcesWithRelationships doesn't exist (#709).
devtools/frigg-cli/index.test.js, deleted 3 OK. It asserted the pre-#639 install flow, which install.test.js now covers.
devtools/infrastructure/integration.test.js, deleted 9 OK. Its subject, serverless-template and BuildTimeDiscovery plumbing, is gone.
management-ui Vitest suites, excluded by devtools/jest.config.js 4 files OK as stated, but nothing runs them now.
infrastructure-composer.test.js, 91 → 76 tests 15 Mostly OK. Discovery gating is in resource-discovery.test.js, NAT placement is in the vpc-builder tests, and the CIDR fallback is gone from the source. The exception is the selfHeal throw (nit above).
postgres-config.test.js, 26 → 17 tests 9 OK. Deletion protection, CloudWatch log exports and the VPC/2-subnet errors are no longer in AuroraBuilder.
find-integration-context…test.js: "entity user missing" removed 1 OK. The use case no longer looks up the user (a7bacab).
webhook-flow.integration.test.js: default ON_WEBHOOK log assertion 1 OK. The base handler has been a no-op since b74f21e.
Core *.db.test.js (3 encryption suites, 2 parity suites) and the TestMongo smoke test — Moved to the db job, which does run them. I pointed the Postgres URL at a dead port and npm run test:db exited 1, so failures fail the job. There's no continue-on-error.

I found no .only, no raised timeouts and no new passWithNoTests. The other updated assertions match the intended behaviour in source: Boom status codes, the DocumentDB envelope, { externalId, type }, 32-byte keys, Lambda supportsResourceUpdate, the drift PropertyPath shape, the default-SG reuse from 37a354e, and the KMS env-var fallback (kms-builder.js:222-233).

Checked and fine

  • Encryption merge. Module fields merge with the app's custom schema, and calling the loader repeatedly gives the same result. A module that lists a core field like api_key is filtered out rather than failing validation. Modules can only add fields: the opt-out comes only from the app's encryption.disable, and nothing reads opt-outs from module definitions.
  • VpcBuilder fixes. The securityGroupIds fix is correct and covered (infrastructure-composer.test.js:425-433). The managed-mode legacy fix is correct and covered: reverting 82ddf5c makes vpc-builder.test.js fail.
  • Global Mongo removal. No code or unit test in the monorepo relies on MONGO_URI or global.testMongo from core's global setup; prisma.test.js sets its own. @friggframework/test's exports and globalSetup/globalTeardown are unchanged.
  • Lockfile. There are no downgrades, and the changes stay within the mongodb-memory-server subtree. The hoisted mongodb moves from 4.17.2 to 7.5.0, but nothing in the workspace requires mongodb.
  • Workflows.
    • pull_request: branches: ["**"] covers stacked PRs.
    • continue-on-error on lint applies only to the step.
    • test-cli.yml was path-filtered on packages/frigg-cli/**, which no longer exists, so it only ever ran when the workflow file itself changed. Nothing is lost by deleting it.
    • No secrets are used and no pull_request_target was added.

What I ran

  • npm ci on Node 24.10.0 and 22.11.0: both pass. On 22.11 there are only engine warnings that already existed.
  • npm run test:all on both Node versions: everything passes. Core has 198 suites (2620 passed, 2 skipped), devtools 90 + 3 skipped (1944 passed, 37 skipped), admin-scripts 278, schemas 129 and serverless-plugin 46.
  • npm run test:db on Node 24 against mongo:8.0 (replica set) and postgres:16 in Docker, with migrations and db push applied as the workflow does: core passed 5 suites (1438 tests) and test passed 1 suite.
  • The CLI smoke loop.
  • Small probes of the encryption registry and service, using a real AES Cryptor.

🤖 Generated with Claude Code

@seanspeaks seanspeaks left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verdict: no blockers. The CI changes and most of the test updates hold up. Fix the two encryption should-fixes before this ships in a release, because the registry fix turns encryption on for fields that have only ever held plaintext.

Reviewed at 9045bbc. Everything below was checked locally, either by running it or by reading the source. Anything I couldn't confirm is labelled.

Blocking

None.

Should-fix

1. Module encryption errors are still swallowed at debug level, so one bad entry leaves every module field unencrypted. packages/core/database/encryption/encryption-schema-registry.js:274, also :149
The root cause in 2a2adff was a require error hidden by logger.debug. That handler is unchanged, so the next failure will be just as quiet. Repro: a module declares encryption: { credentialFields: ['api_key', 42] }. Then field.startsWith throws TypeError: field.startsWith is not a function at :149. loadCustomEncryptionSchema catches it at debug level, and no module field from any module gets registered. The fields are written in plaintext and nothing reaches the logs at the default level. An invalid app encryption.schema has the same effect, because it throws before modules load. Suggested fix:

  • log at warn or error when an app definition was found but loading failed;
  • validate or skip each module's entries one at a time, so one bad module can't drop the rest.

2. Some plaintext values don't survive the newly registered module fields on the Prisma path. packages/core/database/encryption/field-encryption-service.js:149-155, :239-249
The commit says plaintext values still read back. That holds on the DocumentDB service, which needs exactly 4 base64 parts and more than 50 chars, but not on the Prisma FieldEncryptionService. I checked with an AES Cryptor and a module listing webhook_secret and pin:

  • Values with 3 or more colons are treated as ciphertext. Example: 'a:b:c:d', or any URL with userinfo and a port. _isEncrypted only checks split(':').length >= 4, which causes two problems:

    • on write, _shouldEncrypt skips the value, so it stays plaintext even though the field is registered;
    • on read, cryptor.decrypt throws Encryption key not found, and prisma-encryption-extension.js doesn't catch it, so loading the whole Credential fails.

    Before this PR these module fields weren't registered, so such values read fine. After upgrading, they break existing credentials.

  • Numeric strings come back as numbers. '123456' is stored encrypted and reads back as 123456, because _deserializeAfterDecryption runs JSON.parse on it. A module that calls string methods on the field will throw.

Both problems already exist for core fields; this PR exposes every module field to them. Suggested fix: share the DocumentDB _isEncryptedValue check, and make the read path keep the value's type, at least for primitives that were strings before encryption.

3. The concurrency group also cancels CI on pushes to next. .github/workflows/frigg-ci.js.yml:15-17
group: ${{ github.workflow }}-${{ github.ref }} with cancel-in-progress: true means two quick merges to next cancel the first commit's run. release.yml publishes a prerelease on every push to next with no CI gate, so that published version never gets a CI result. Suggested fix: cancel-in-progress: ${{ github.event_name == 'pull_request' }}.

4. Two VpcBuilder tests were flipped to match the source on a disputed backwards-compatibility point. packages/devtools/infrastructure/domains/networking/vpc-builder.test.js:839-843, :1606-1611
On next, both tests asserted that a stack already containing FriggSubnet{1,2}RouteAssociation keeps those logical IDs. The second test's comment says "We MUST use these same logical IDs to avoid AlreadyExists errors". The source at vpc-builder.js:1825-1829 always emits FriggPrivateSubnet{1,2}RouteTableAssociation. The PR rewrote both tests to bless the source behaviour. The tests and that source logic came from the same commit (37a354e), so the intent isn't clear from history.

The risk is an EC2 constraint I didn't deploy to confirm. A subnet can have only one explicit route-table association, and CloudFormation creates the new logical resource before deleting the old one. So on a stack whose old associations are intact, the update can fail with Resource.AlreadyAssociated and roll back. Rather than settle this in a CI-green PR, it.skip these two with an issue, or get the 37a354e author to confirm that new IDs are intended.

Nits

  • packages/test/mongodb.js:9: the pinned version means two MongoDB downloads. The pin is 8.0.17, but mongodb-memory-server 11's postinstall downloads its own default, 8.2.6. After npm ci, 8.2.6 was in node_modules/.cache/mongodb-memory-server, and TestMongo then downloaded 8.0.17. The pinned version is never pre-fetched, and the default is fetched but never used. That happens:

    • on every CI build and lint run, which aren't cached;
    • in every app, since @friggframework/devtools has @friggframework/test as a runtime dependency.

    Pin to the library default, or set MONGOMS_VERSION / MONGOMS_DISABLE_POSTINSTALL where it matters.

  • frigg-ci.js.yml:104-108: the replica-set wait can time out without failing the step. If the node never becomes primary, the loop ends with status 0 (the last command is sleep 1), and the failure only appears later as a confusing prisma db push error. Add a check after the loop. The first loop is fine: if mongosh never answers, rs.initiate fails the step.

  • frigg-ci.js.yml:56-58: the CLI smoke loop can't catch a removed command. npx frigg nonexistent-cmd --help exits 0, because commander prints the top-level help. Also, frigg-cli/index.js:78-89 requires every command module eagerly, so frigg --help alone already proves they load. To catch a dropped or renamed command, grep the --help output for each name.

  • The build job has no permissions: block. Now that git-auto-commit is gone, add a top-level permissions: contents: read.

  • docs/TESTING.md:241-256 is stale. It still says integration tests automatically get an in-memory MongoDB with MONGO_URI. That's no longer true for core; point to *.db.test.js, npm run test:db and opt-in TestMongo.

  • cd616a5 calls the batchSend issue a delivery-path bug, but it isn't one. The old in-place splice ran after await sqs.send(...) returned, so SDK middleware and retries saw the right entries; only later observers such as the mock recorder saw 5/5/5. The fix is correct; just don't list it as a production fix in release notes.

  • 65014bc says the NAT tests it removed are "covered" in vpc-builder tests, which is only partly true. Never reusing a NAT gateway placed in a private subnet is covered (vpc-builder.test.js:850-896). Throwing when selfHeal: false is not: that guard no longer exists. The current code (vpc-builder.js:240, :1706) silently creates a new, billable NAT gateway. That changed in 7b36f0f, not here, but it deserves an issue.

Tests that no longer run, with verdicts

What Tests Verdict
devtools/infrastructure/scripts/build-time-discovery.test.js, describe.skip 23 OK. Its subject requires the deleted aws-discovery. Note that build-time-discovery.js and run-discovery.js still ship and would throw if run (#708).
orphan-detection-relationship-analysis.test.js, describe.skip 4 OK. findOrphanedResourcesWithRelationships doesn't exist (#709).
devtools/frigg-cli/index.test.js, deleted 3 OK. It asserted the pre-#639 install flow, which install.test.js now covers.
devtools/infrastructure/integration.test.js, deleted 9 OK. Its subject, serverless-template and BuildTimeDiscovery plumbing, is gone.
management-ui Vitest suites, excluded by devtools/jest.config.js 4 files OK as stated, but nothing runs them now.
infrastructure-composer.test.js, 91 → 76 tests 15 Mostly OK. Discovery gating is in resource-discovery.test.js, NAT placement is in the vpc-builder tests, and the CIDR fallback is gone from the source. The exception is the selfHeal throw (nit above).
postgres-config.test.js, 26 → 17 tests 9 OK. Deletion protection, CloudWatch log exports and the VPC/2-subnet errors are no longer in AuroraBuilder.
find-integration-context…test.js: "entity user missing" removed 1 OK. The use case no longer looks up the user (a7bacab).
webhook-flow.integration.test.js: default ON_WEBHOOK log assertion 1 OK. The base handler has been a no-op since b74f21e.
Core *.db.test.js (3 encryption suites, 2 parity suites) and the TestMongo smoke test — Moved to the db job, which does run them. I pointed the Postgres URL at a dead port and npm run test:db exited 1, so failures fail the job. There's no continue-on-error.

I found no .only, no raised timeouts and no new passWithNoTests. The other updated assertions match the intended behaviour in source: Boom status codes, the DocumentDB envelope, { externalId, type }, 32-byte keys, Lambda supportsResourceUpdate, the drift PropertyPath shape, the default-SG reuse from 37a354e, and the KMS env-var fallback (kms-builder.js:222-233).

Checked and fine

  • Encryption merge. Module fields merge with the app's custom schema, and calling the loader repeatedly gives the same result. A module that lists a core field like api_key is filtered out rather than failing validation. Modules can only add fields: the opt-out comes only from the app's encryption.disable, and nothing reads opt-outs from module definitions.
  • VpcBuilder fixes. The securityGroupIds fix is correct and covered (infrastructure-composer.test.js:425-433). The managed-mode legacy fix is correct and covered: reverting 82ddf5c makes vpc-builder.test.js fail.
  • Global Mongo removal. No code or unit test in the monorepo relies on MONGO_URI or global.testMongo from core's global setup; prisma.test.js sets its own. @friggframework/test's exports and globalSetup/globalTeardown are unchanged.
  • Lockfile. There are no downgrades, and the changes stay within the mongodb-memory-server subtree. The hoisted mongodb moves from 4.17.2 to 7.5.0, but nothing in the workspace requires mongodb.
  • Workflows.
    • pull_request: branches: ["**"] covers stacked PRs.
    • continue-on-error on lint applies only to the step.
    • test-cli.yml was path-filtered on packages/frigg-cli/**, which no longer exists, so it only ever ran when the workflow file itself changed. Nothing is lost by deleting it.
    • No secrets are used and no pull_request_target was added.

What I ran

  • npm ci on Node 24.10.0 and 22.11.0: both pass. On 22.11 there are only engine warnings that already existed.
  • npm run test:all on both Node versions: everything passes. Core has 198 suites (2620 passed, 2 skipped), devtools 90 + 3 skipped (1944 passed, 37 skipped), admin-scripts 278, schemas 129 and serverless-plugin 46.
  • npm run test:db on Node 24 against mongo:8.0 (replica set) and postgres:16 in Docker, with migrations and db push applied as the workflow does: core passed 5 suites (1438 tests) and test passed 1 suite.
  • The CLI smoke loop.
  • Small probes of the encryption registry and service, using a real AES Cryptor.

🤖 Generated with Claude Code

seanspeaks and others added 3 commits October 24, 2026 00:20
…when deployed

Review of #710: loadCustomEncryptionSchema wrapped everything in one
catch that logged at debug level, the same handler that hid the broken
require fixed in 2a2adff. One non-string entry in a module's
`encryption.credentialFields` (`field.startsWith is not a function`) or an
invalid app `encryption.schema` dropped every module field, and those
credentials were written in plaintext with nothing in the logs.

- credentialFields are validated entry by entry: an invalid entry is
  skipped with a warning naming the module; valid entries from the same
  and other modules are still registered.
- Only "no backend / no app definition" stays at debug level. Once an app
  definition is found, an invalid encryption.schema, encryption.disable or
  module schema is a configuration error: in deployed stages it is logged
  at error level and thrown as EncryptionSchemaError; in dev/test/local it
  is a warning and loading continues.
- prisma.js rethrows EncryptionSchemaError instead of "Continuing without
  encryption", so startup fails rather than silently writing plaintext.
  Other extension-initialisation failures are unchanged here (#663 covers
  them).

New tests fail on the previous source (4 of 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…a encryption path

Review of #710: registering module credentialFields turns encryption on
for fields that have only ever held plaintext, and the Prisma
FieldEncryptionService mishandled two kinds of such values.

- Colons: _isEncrypted treated any string with 3+ colons as ciphertext.
  Plaintext like 'a:b:c:d' or a URL with userinfo and a port was never
  encrypted on write, and on read cryptor.decrypt threw ("Encryption key
  not found"), failing the whole Credential load. Both paths now share
  ciphertext-format.isEncryptedEnvelope, the strict check the DocumentDB
  path already used (exactly 4 base64-alphabet parts, > 50 chars). Values
  that don't match are plaintext; a real envelope that fails to decrypt
  still throws, so key errors aren't hidden.
- Types: primitives were encrypted as String(value) and read back through
  JSON.parse, so '123456' came back as the number 123456. Every value is
  now JSON-encoded before encryption ('123456' -> "\"123456\""), so
  strings stay strings and numbers stay numbers. Ciphertext written by
  earlier versions still reads: raw strings that aren't JSON come back
  unchanged (a legacy raw string that is valid JSON, e.g. '123456', parses
  as it always did). Older readers also handle the new form, since
  JSON.parse('"x"') is 'x'.

The new extension test (real AES Cryptor) fails on the previous source
(3 of 5). The service and extension unit tests' mock cryptors now emit the
real envelope shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Review of #710: with cancel-in-progress on every ref, two quick merges to
next cancelled the first commit's run, and release.yml publishes a
prerelease for every push to next, so that version never got a CI
result. Superseded PR runs are still cancelled; pushes to main/next are
not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
seanspeaks and others added 6 commits October 24, 2026 00:24
…ending #714

Review of #710: two VpcBuilder tests had been flipped to accept that a
stack with FriggSubnet{1,2}RouteAssociation gets
FriggPrivateSubnet{1,2}RouteTableAssociation instead.

History doesn't settle it. 37a354e changed the source to always emit the
new IDs, with a comment saying this is deliberate (force CloudFormation to
recreate drifted associations), and in the same commit kept tests that
expect the old IDs "to avoid AlreadyExists errors". A subnet takes only one
explicit route-table association and CloudFormation creates the new
logical resource before deleting the old one, so on a stack whose old
associations are intact the rename could fail with
Resource.AlreadyAssociated and roll back. Choosing between that and the
drift fix is a product call, so the source is left alone and:
- "should add UpdateReplacePolicy…" is back to its original old-ID
  assertions and it.skip'ped, pointing at #714;
- the backwards-compatibility test keeps running for the NAT route and
  VPC endpoint IDs but no longer asserts the association IDs either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…MongoDB at install

Review of #710: mongodb-memory-server's postinstall downloads the
library's default binary (8.2.6) on every `npm ci`, while TestMongo pins
and downloads 8.0.17 at first use, so every install fetched a ~100 MB
binary that nothing used. Because @friggframework/devtools depends on
@friggframework/test at runtime, that happened in every Frigg app too,
and MONGOMS_DISABLE_POSTINSTALL or package.json config in this repo
can't reach an app's install (the hook reads INIT_CWD's package.json).

@friggframework/test now depends on mongodb-memory-server-core, the same
library without the install hook; TestMongo only ever fetches the pinned
version. The lockfile change is just the dropped wrapper package (and its
tslib) plus the renamed dependency.

install-footprint.test.js fails on the previous dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Review of #710: `frigg <unknown> --help` exits 0 (commander prints the
top-level help), so the per-command --help loop could never fail for a
missing command. `frigg --help` already loads every command module; the
step now also checks that each expected command is listed in it. Checked
locally: the list passes, and adding a nonexistent command fails the step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Review of #710: the wait loop ended with `sleep 1`, so a node that never
became primary left the step green and the failure surfaced later as a
confusing `prisma db push` error. The step now fails with an error and
the container log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Review of #710: the build job had no permissions block. With the lint
auto-commit gone no job needs write access, so contents: read is set at
workflow level (db and lint already declared it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Review of #710: the MongoDB section still said integration tests get an
in-memory MongoDB with MONGO_URI from the test setup, which core no longer
provides. It now explains the *.db.test.js convention, npm run test:db,
the FRIGG_TEST_* URLs, how CI and a local run provide the databases, and
TestMongo as an opt-in helper for apps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
@seanspeaks

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Every should-fix and nit is addressed in the commits listed below. Each code fix comes with a test that fails on the previous source. CI is green at 7cfcf12: https://github.com/friggframework/frigg/actions/runs/40062491876

1. Encryption-registry errors swallowed at debug level (325d9af)

  • Module credentialFields are validated one entry at a time. An invalid entry, such as 42, is skipped with a warning that names the module. Valid fields from that module and from other modules are still registered.
  • Debug level is now used only when there is no backend or app definition. Once an app definition is found, an invalid encryption.schema, encryption.disable or module schema is a configuration error:
    • in deployed stages it is logged at error level and thrown as EncryptionSchemaError;
    • in dev, test and local it is a warning, and loading continues.
  • EncryptionConfigurationError isn't on next yet; feat(core)!: fail closed on field-level encryption in deployed stages #663 adds it.
  • prisma.js now rethrows EncryptionSchemaError instead of logging "Continuing without encryption", so startup fails rather than writing plaintext. Other extension-initialisation failures are left to feat(core)!: fail closed on field-level encryption in deployed stages #663.
  • Tests: encryption-schema-registry-loading.test.js and prisma-encryption-startup.test.js. 4 of their 5 tests fail on the old source.

2. Plaintext and typed values on the Prisma path (3993629)

  • Colons: both paths now share ciphertext-format.isEncryptedEnvelope, the strict check from the DocumentDB service: exactly 4 base64-alphabet parts and more than 50 characters.
    • 'a:b:c:d' and URLs with userinfo and a port are encrypted on write and read back as plaintext.
    • A real envelope that fails to decrypt still throws.
  • Types: every value is JSON-encoded before encryption, so '123456' reads back as '123456' and 1234 as 1234.
    • Raw strings written by earlier versions still read back. The exception is a legacy raw string that is itself valid JSON (such as '123456'); it still parses to a number, as it always did.
    • Older readers handle the new format, because JSON.parse('"x"') is 'x'.
  • New test: prisma-extension-legacy-values.test.js runs through the extension with a real AES Cryptor; 3 of its 5 tests fail on the old source. The unit-test mock cryptors now emit the real envelope shape.
  • The DocumentDB path still stores raw strings, so its numeric-string ambiguity remains. Out of scope here.

3. Concurrency on next (be66998)

  • cancel-in-progress: ${{ github.event_name == 'pull_request' }}, so pushes to next (which release.yml publishes) are never cancelled.

4. Route-association tests (3c342b4)

  • History doesn't settle the intent. The source comment in 37a354e says the new IDs are deliberate, to recreate drifted associations; the tests in the same commit expected the old IDs.
  • Given the AlreadyAssociated risk you describe, I left the source alone:
    • "should add UpdateReplacePolicy…" is back to its original old-ID assertions and it.skipped;
    • the backwards-compatibility test keeps its NAT-route and VPC-endpoint assertions, but no longer asserts the association IDs either way.
  • Tracked in devtools: VpcBuilder renames private subnet route-table associations on existing stacks (upgrade risk) #714.

Nits

  • Two MongoDB downloads (3126961): @friggframework/test now depends on mongodb-memory-server-core, which has no postinstall, so only the pinned 8.0.17 is ever fetched.
    • This covers apps too. MONGOMS_DISABLE_POSTINSTALL, or config in this repo, can't reach an app's install, because the hook reads INIT_CWD.
    • install-footprint.test.js fails on the old dependency.
  • Replica-set wait (bf11c4c): the step now fails with an error and the container log if no primary is elected within 30 s.
  • CLI smoke test (40c6a2a): it now checks that frigg --help lists each expected command. I checked that a missing command fails the step.
  • permissions (ced54b0): contents: read is now set at workflow level.
  • docs/TESTING.md (7cfcf12): now covers *.db.test.js, npm run test:db, the FRIGG_TEST_* URLs, local and CI setup, and TestMongo as an opt-in helper.
  • batchSend: agreed, it isn't a delivery-path bug. I'll describe it as test-observability only, not a production fix.
  • selfHeal NAT guard: noted in the PR body as pre-existing (7b36f0f), not changed here.

@seanspeaks
seanspeaks merged commit 61575cb into next Oct 24, 2026
16 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7cfcf12. Configure here.

function isLocalStage() {
const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
return ['dev', 'test', 'local'].includes(stage.toLowerCase());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Local stage check misses development

Medium Severity

isLocalStage treats development as deployed even though that is both its own fallback and the usual NODE_ENV for local Node. An invalid encryption.schema now throws and blocks startup, contrary to the lenient local-stage path. It is also case-insensitive, while Prisma’s encryption bypass is not, so STAGE=DEV can encrypt with a warned-away schema.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7cfcf12. Configure here.

@@ -1,4 +1,5 @@
const { Cryptor } = require('../encrypt/Cryptor');
const { isEncryptedEnvelope } = require('./encryption/ciphertext-format');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DocumentDB validates schema when encryption is off

Medium Severity

DocumentDBEncryptionService now runs loadCustomEncryptionSchema before it decides encryption is off. That load can throw EncryptionSchemaError, so constructing the repository fails even when the stage would bypass encryption or no KMS/AES keys are set. Prisma only loads the schema after encryption is enabled, so the same config still starts on Mongo/Postgres.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7cfcf12. Configure here.

@seanspeaks

Copy link
Copy Markdown
Contributor Author

🚀 PR was released in v2.0.0-next.122 🚀

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
17.6% Duplication on New Code (required ≤ 3%)
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants