Skip to content

Release: deploy unblocker + green suite + CI gate + MiniMax adapter - #11

Closed
joshberen wants to merge 11 commits into
risunCode:devfrom
joshberen:integration/20261014
Closed

joshberen wants to merge 11 commits into
risunCode:devfrom
joshberen:integration/20261014

Conversation

@joshberen

@joshberen joshberen commented Oct 14, 2026 •

Copy link
Copy Markdown

Combines the four branches above in the order they must land, so a single merge produces a verified tree.

Closed by the author.

…acy schemas

Databases created from a pre-rewrite baseline cannot complete the migration
chain. 0006 rebuilds `share_links_kind_check` over rows that still hold the
retired `monitor`/`setup` kinds, so the check fails, the transaction rolls back,
and every later migration is left unapplied (35 of them as of 0040).

Rename those rows first (`monitor`→`enroll`, `setup`→`handoff`) and retire them,
matching the rule the backup importer already applies to the same legacy kinds
("import them as inactive enrollment links so their old tokens never regain a
usable public endpoint", `src/console/backup/validate.ts`): the rows were written
for the old page contract, and an old token must not start answering the new one.

Objects introduced by editing `0000_baseline.sql` itself reach no existing
database — the snapshot is already in the ledger, and no later file adds them.
A pre-rewrite database therefore finished the chain while still missing
`telemetry_usage_totals` and the share-key columns on `api_keys`
(`key_mode`, `parent_key_id`, `issued_client_ip`, `issued_client_ip_key`), then
failed its first key lookup with `column "key_mode" does not exist`.
`0041_reconcile_legacy_schema.sql` closes that gap idempotently.

Unused legacy objects (`backup_status`, `console_users.last_login_at`,
`telemetry_payloads.redaction_applied`, four superseded indexes) are left in
place: dropping them is destructive and not required to unblock the upgrade.

Verified against a production dump: chain completes (42 migrations), the
resulting schema matches a fresh database column-for-column, a live completion
succeeds, and data is preserved.
`homeDir` re-exported `node:os`'s `homedir()`, which Bun resolves once and
freezes for the process lifetime. The injector tests redirect `HOME` to a temp
directory — the seam this module's own header documents — so every injector
kept reading and writing the operator's real home directory instead. The codex
lifecycle suite failed on a stale real `~/.codex`, and its `apply` wrote the
fixture config into that directory.

Resolve `HOME`/`USERPROFILE` on each call and keep `homedir()` as the fallback
for a process started without either variable.
Three defects kept `bun run test` red on a clean checkout, and two of them hid
each other.

`requireDatabase()` registered a `beforeAll` that called `getTestPool()`
unconditionally, so a machine without `CARTETHYIA_TEST_DATABASE_URL` failed two
suites instead of skipping them — the opposite of this module's promise that an
unconfigured service skips with a stated reason. The suites that call it were
plain `describe`, so they were not skipped either; they now use the `dbDescribe`
primitive that already existed for exactly this.

`.env.test` and `.env.test.example` pointed at
`postgres://postgres@localhost:5432/...`. `pg_hba` requires scram over TCP, so
the passwordless URL failed all twelve database suites at connect time. Both now
carry a dedicated `cartethyia_test` role, and `.env.test.example` uses the
variable name the suites and `test-db:check` actually read — it held the retired
`TEST_DATABASE_URL`, the exact mismatch `test-db-check.ts` documents.

Verified on a local PostgreSQL 18: 1992 backend + 375 dashboard tests pass with
the database reachable, and the same run is green with 142 stated skips when it
is not.
…ktop path

`dropCorruptAttachments`'s two capture-based tests read
`C:\Users\Aria\Desktop\awok..txt`, an absolute path to the author's desktop that
never entered the repository. They cannot pass anywhere else, so the suite was
red on every other machine.

The defect they pinned — an IDAT chunk whose stored CRC disagrees with its bytes
— is already reproduced from this file's PNG primitives by the
`png_crc_mismatch` test, and the well-formed counterpart by the valid 1x1
fixture. The rationale moves onto that test so the regression's origin is not
lost with the file path.
Twenty `agent.minimax.io` accounts sat in the `minimax` provider with nothing
behind them: every account verified active, the console listed no models, and
`/v1/models` advertised no MiniMax route at all. The provider needed a
dispatch adapter before any of that capacity could be reached.

`integrations/minimax.ts` drives the upstream's three-call agent handshake —
agent list, session create, then message SSE on the separate `agent-stream`
host — carrying the signature the shipped web client sends
(`md5(timestamp + salt + body)`) and the query envelope the upstream expects.
Every dispatch opens a fresh session, so concurrent requests never share the
agent's mutable server-side history, and the catalog is taken from the
upstream's own `context_limit` values rather than base-catalog guesses.

The provider row is marked `bespokeWire`, because the adapter frames its own
protocol: there is no canonical codec behind it to drop a part it cannot
encode or to translate a generation control it does not read. It builds its
own User-Agent for the same reason, so the dashboard offers no route-level
override.

The credential is the object the `minimax-agent-automation` harvester writes,
and one field in it is load-bearing rather than decorative. The upstream keys
every query envelope on the account id, and that id is the harvester's
`realUserID` — NOT the JWT's `user.id`. The two are different numbers for every
harvested account (562139107990159360 against 562139107906273285 for the same
row), and the upstream answers 401 for the token's value and for an absent
`user_id` alike, so it cannot be recovered from the token. A credential without
it is refused at parse time instead of dispatched into a bare 401, and the
harvested JSON therefore carries `{email, password, cookie, jwt, realUserID}`
as one unit: the bearer JWT, the cookie jar the WAF expects, and the id the
envelope is keyed on.

The reasoning block had to name no default. The upstream accepts only `low`,
`medium`, `high`, `xhigh`, and `max`, and answers anything else with
`invalid params, invalid reasoning_effort: "default"` — a rejection the agent
delivers as an ordinary assistant message, so the first version returned HTTP
200 whose completion was the error text, with zero usage and `stop` as the
finish reason. The block is now omitted unless the request names one of the
five, which is what the upstream does on its own.

Verified end to end against the pooled accounts: all four catalogued models
answered `SMOKE_OK` through the adapter directly, and again through the real
`/v1/chat/completions` surface, streaming and non-streaming. The suite covers
the model block's effort handling, the credential shapes including the missing
account id, the envelope's `user_id`, the chunk/final-envelope de-duplication,
the handshake call order, and an upstream rejection surfacing as a gateway
error.
`bun run test` failed intermittently with `40P01 deadlock detected`. The server
log names both sides: a migration suite's `ALTER TABLE ... ADD COLUMN` holds
`provider_routing_settings` and waits for `provider_accounts`, while a tenant
teardown's `on delete cascade` holds `provider_accounts` and waits for
`provider_routing_settings`.

Neither side is wrong and no lock order fixes it. The DDL transaction must
change two tables, so it can only ever hold one of the two locks while it waits
for the other; the cascade's order across the eleven tenant-scoped tables is the
server's choice. Measured on a pristine `origin/dev`, 3 runs in 14 failed, each
one a different suite — whichever transaction the server picked as the victim.

The runner now executes `test/persistence/migration-*.test.ts` first, with
`--parallel` off, and excludes them from the parallel pass. With no other
connection open, no other transaction can hold the lock the DDL waits for, so
the inversion cannot form. The two passes together still run every backend file
once.

`SCHEMA_DDL_DIR`/`SCHEMA_DDL_FILE` pick the files for the serial pass and
`SCHEMA_DDL_IGNORE` is the same predicate for `--path-ignore-patterns`; the glob
must stay rooted with `**` because the flag matches paths as `bun test`
discovered them, relative to the working directory. An absolute pattern matches
nothing, which runs those files twice and restores the deadlock.
With `CARTETHYIA_TEST_DATABASE_URL` unset the run was supposed to be green with
stated skips, and it was not: four cases in `remote-debug-access`, one error per
file in `static-token-detection`, `runtime-settings-legacy` and
`share-link-refusal`.

Three distinct causes, one shape. `getDb()` throws when no connection string is
configured, and a skipped suite still evaluates its body to collect the cases —
so a handle built at collection time fails the file instead of skipping it.
`remote-debug-access` was worse: it was a plain `describe` over `createWorld`,
which needs the database, so it ran and failed four assertions rather than
skipping.

Each site now resolves its handle where the work happens: inside `beforeAll` for
the two suites that keep it for the whole file, and per call for
`share-link-refusal`, whose helpers are the only consumers. `remote-debug-access`
uses the `dbDescribe` primitive that exists for exactly this. `requireDatabase()`
is gone from `share-link-refusal`: with `dbDescribe` it was the second gate on
the same condition.

No behaviour changes when a database is configured: 1992 backend + 375 dashboard
pass, same as before.
The workflow was `workflow_dispatch` only and ran two typechecks, so nothing
ever executed `test/` — even though `AGENTS.md` names `bun run test` as a gate.
Two defects sat in `dev` because of that: a test that opened an absolute path on
the author's desktop, and a `homeDir` that Bun caches on first use, which made
the injector tests write into the operator's real home directory.

Adds a `test` job with disposable PostgreSQL and Redis services carrying the
same image, credentials and host port as `docker-compose.test.yml`, so the URL
that works in CI also works for `bun run test-db:up` locally. The job exports
both service URLs; `scripts/ci-run-tests.ts` loads `.env.test` without
overriding exported variables, which is what lets CI keep the file's fixed
encryption key while pointing at the service containers.

Triggers become `push` to `dev`/`SeeYouLater`, `pull_request`, and the existing
manual dispatch, with `contents: read` and a ref-scoped concurrency group.

`bun-version` is dropped from all three jobs: `oven-sh/setup-bun` reads the
committed `.bun-version`, the same pin `packageManager` and `engines` carry,
where `latest` let CI drift onto a Bun the lockfile was never resolved against.

Verified locally against the same service shape the workflow declares
(PostgreSQL on 55432, Redis db 15): 1992 backend + 375 dashboard tests pass with
no skips, and a control run with a bogus port fails, proving an exported
`CARTETHYIA_TEST_DATABASE_URL` overrides `.env.test` as the job relies on.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-14T05:35:50.943476Z d8bd0f0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@joshberen joshberen closed this Oct 14, 2026
@joshberen
joshberen deleted the integration/20261014 branch October 14, 2026 05:41
risunCode added a commit that referenced this pull request Oct 15, 2026
The priority table had no notion of what is done, so it was starting to
mislead: several items are implemented, two are blocked on undocumented
wire shapes, and one (#5) rests on a premise that the code contradicts.

Adds a Status column and a notes section recording WHY #2, #5 and #11
are not done - all three need a capture or donor source, not effort.
Also records that the two Factory legal-risk items are excluded pending
a legal read.
risunCode added a commit that referenced this pull request Oct 15, 2026
The backend version-gates SKU availability against client_version on
/models independently of a model's declared minimal_client_version
(donor packages/catalog/src/wire/codex.ts:24-34). We sent nothing, so
discovery could omit SKUs the backend would serve at our pin - the
donor's own example: a SKU hidden at 0.155.1 and listed at 0.159.0.

buildCodexModelsUrl validates the version against ^\d+\.\d+\.\d+$ so a
resolver value can never become a query-injection vector, and returns
the bare URL when the version does not match rather than sending a
malformed param.

Our pin is unchanged - bumping it is a separate decision with its own
request-shape risk.

14 tests pass, typecheck clean.
risunCode added a commit that referenced this pull request Oct 15, 2026
#2, #5, #8, #11, #14 were each marked blocked on undocumented wire
behavior. All five were answerable from the donor checkout named in
this file's own header.

Records the lesson too, with the two concrete ways it bit: 'not in our
repo' is not 'unknown', and a guess that compiles can still be wrong on
the wire (#2 was guessed into stop_details; it is top-level).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants