Skip to content

Commit 3f7cb1b

Browse files
test(dogfood): re-pin the forced-reset gate code and the password-set ledger decisions to ruling C′ (#23512)
Fixes #23493 Clause-②: no ## What changed Two dogfood pins still stated the contract from before ruling C′ (director record `6159450584`, the maintainer's 「同意」; letter A `6165848580` sets the revocation ON by default). PR #23440 (`0f059078ec`) landed that ruling, and the hourly full run went red on **Dogfood Regression Gate (3/3)**: 2 files, 4 tests, at both `0d3ea295d1` (run `38568381233`) and `394f65e891` (run `38576826654`). This PR re-pins both files to the ruled answer. Every assertion keeps its exact `toBe` / closed-set form, and nothing is skipped, deleted or widened. Test files only. No product source changes. **1. `packages/qa/dogfood/test/bearer-lane-password-change.dogfood.test.ts`, all three lanes.** - The gate refusal for an admin-provisioned user now expects `403` + `error.code === 'PASSWORD_RESET_REQUIRED'`. It expected `PASSWORD_EXPIRED`. The test title and the file header now name the new code. - **Deciding clause:** ruling C′ item 2: 「The admin-forced change (`must_change_password`) stops reusing `PASSWORD_EXPIRED`: it gets its own code (`PASSWORD_RESET_REQUIRED`, scope `credential`)」. - **Premise check (falsifiable, verified).** The scenario is the administrator-forced change, not expiry: - The user comes from `POST /auth/admin/create-user`, whose answer asserts `mustChangePassword === true`. - The file arms only `passwordHistoryCount` (through `applyConfigPatch`), so `passwordExpiryDays` stays off. - In `AuthManager.resolveUserAuthGates` (`plugin-auth/src/auth-manager.ts`), `PASSWORD_RESET_REQUIRED` is pushed only for `must_change_password`, and `PASSWORD_EXPIRED` only when `expiryDays` is above 0 and `password_changed_at` is older than that. - So the product's `PASSWORD_RESET_REQUIRED` is the ruled answer here, not a defect. - **Downstream steps:** none needed a re-pin. The ruled revocation rides `runAdminSetUserPassword` (`/auth/admin/set-user-password`). This file never calls it: it only creates the user, then signs in, changes the password, reads and attempts reuse with the user's own credentials. Measured three ways: - both hourly logs show only the first step red in each lane; - the 12 tests of this file pass locally; - the ablation below turns exactly the three gate-refusal steps red and nothing else. **2. `packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts`, the closed decision set of `user.admin_password_set`.** - Before: `required: ['event', 'passwordGenerated']`, `optional: []`. - After: `required: ['event', 'passwordGenerated', 'revokeSessionsAndKeys']`, `optional: ['revokedSessions', 'revokedApiKeys', 'revocationComplete']`. - **Deciding clauses:** - ruling C′ item 2: the forced reset 「carries a "revoke the user's sessions and API keys" switch」; - letter A: ON by default for every forced reset; - PR #23440's producer records the decision and what landed on the ledger row. - **Required or optional: measured from the producer, per key.** `runAdminSetUserPassword` (`plugin-auth/src/admin-user-endpoints.ts`) writes the metadata as follows: - `revokeSessionsAndKeys` is the boolean switch. It is written on **every** admin password set, so it is `required`. - `revokedSessions`, `revokedApiKeys` and `revocationComplete` are spread in only when the revocation ran (`...(revocation ? {…} : {})`), so they are `optional`. - That matches the producer's own `AdminAuditDecisions` type, which declares the switch as required and the three counts with `?`. The table's existing `organizationId` follows the same convention. - The unit pin beside the producer (`admin-user-endpoints.test.ts`, `PASSWORD_SET_DECISIONS`) holds the six-key set on the default-on path. - **Deviation from the triage's reading (`6170015227`):** the triage named `revokeSessionsAndKeys` as the one missing key. The CI assertion stops at its first undeclared key, which hid three more. This fixture's reset leaves the switch at its default (on), so its row also carries the three counts. A one-key re-pin leaves this file red. The ablation's leg 2 below measures that. ## Verification All local runs were on head `8b7306a355` (base `ad5a7d7c39`), under the shared verify lock, with the dogfood dependency closure built first: `pnpm turbo run build --filter=@objectstack/dogfood --concurrency=2`, `VERDICT command-exit 0`, 65/65 tasks. - **Green:** `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/bearer-lane-password-change.dogfood.test.ts test/admin-ledger-decision-metadata.dogfood.test.ts` gave `Test Files 2 passed (2)`, `Tests 20 passed (20)` (12 bearer-lane tests, 8 ledger tests), exit 0. It ran twice: before the ablation, and again after the restore. - **Typecheck:** `pnpm --filter @objectstack/dogfood typecheck` (`tsc --noEmit`) exited 0. `tsc --listFilesOnly` lists both edited files in that program (2 of 2). ### Ablation: each re-pinned expectation reverted to its old value Each leg used `scripts/ablation-replace.mjs` in WRAP mode. Every anchor hit 1 → 0, and every mutation was proved on disk by blob change and `grep -c`. Every restore was proved: the blob equals HEAD and `git diff HEAD` is empty. | Leg | Mutation | Result | |---|---|---| | 1 | bearer `toBe('PASSWORD_RESET_REQUIRED')` → `toBe('PASSWORD_EXPIRED')`, and the ledger set back to `required: ['event', 'passwordGenerated'], optional: []` (blobs `c2170a9eaf66` → `1cd8a3348170`, `0826915c7b04` → `b8a4c082c149`) | `4 failed / 16 passed`: the 3 lanes, with `expected 'PASSWORD_RESET_REQUIRED' to be 'PASSWORD_EXPIRED'`, and the ledger control, with `expected [ 'event', 'passwordGenerated' ] to include 'revokeSessionsAndKeys'`. These are the same 4 tests and the same assertion text as both hourly runs. | | 2 | ledger: `revokeSessionsAndKeys` kept, the three counts dropped (`optional: []`; blob `0826915c7b04` → `24315e7155c7`) | `1 failed / 7 passed`: the ledger control, with `expected [ 'event', 'passwordGenerated', …(1) ] to include 'revokedSessions'` | After the restore, both blobs equal HEAD (`c2170a9eaf66`, `0826915c7b04`), `git diff HEAD` is empty, and the green run above was repeated at 20/20. ### Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 53 commands on `8b7306a355`. Each one ran with its exit code captured before any pipe. `--ran` reconciles: `53 derived, 51 run, 2 NOT-MEASURED, 0 UNRUN`. - 51 exited 0, including `check:nul-bytes`, `check:cross-package-test-inputs`, `check:test-source-alias` and `check:issue-citations`. `check:dts-closure` and `check:lean-entry-closure` exited 0 on a re-run after the closure build; their first run had no `dist/`. - 2 are NOT MEASURED (exit 3, PREREQUISITE NOT MET): - `check:dual-build-cjs-loads` needs the whole workspace built, beyond the dogfood closure. - `check:type-check-debt`'s own closure build timed out in the shared lock queue. - Both are whole-tree families, which `Lint & Repo Gates` runs on this PR. - `node scripts/check-empty-changeset.mjs`: exit 0, no changeset added. ### What this PR's CI cannot show The `Dogfood Regression Gate` jobs run only on `push` / `schedule` / `workflow_dispatch` (`ci.yml`, `github.event_name != 'pull_request' && github.event_name != 'merge_group'`). This PR's checks therefore do not run either file. The local runs above are the pre-landing evidence. The first hourly full run after this lands is the authority, and it refreshes #23493. ## Acceptance notes - **Changeset: none, and `skip-changeset` applies.** The diff is two test files in `@objectstack/dogfood`, a `private: true` package, so it publishes nothing. On this repo an empty-frontmatter changeset is refused (`check-empty-changeset`). - **`origin/main` was not merged in before opening.** It moved four commits past the base (`111b5821ce`, `5547ad451a`, `3a2adc8835`, `f9b421716d`). A diffstat of that range over `packages/qa/dogfood`, `plugin-auth`, `plugin-audit`, `core`, `rest` and `runtime` touches only `runtime/src/domains/automation.ts` and a test beside it (the automation flow doors). Neither is on the auth gate or the ledger path these pins read. Merging would rebuild the whole closure on a shared box for no overlap. This PR's CI runs on the merge ref. - **The producer's doc comment is behind its type.** The header above `AdminAuditDecisions` in `plugin-auth/src/admin-user-endpoints.ts` lists the closed members (`event`, `passwordGenerated`, `placeholderEmail`, `membershipCreated`, `organizationId`) and says each is "a decision that no field of the user stores, or a reference to another record". It names none of the four keys PR #23440 added, and the three counts are neither a decision nor a reference: they are counts of what the revocation landed. The type itself declares them, and no user-field value reaches the row. Noted, not filed (`carrier:` none); this PR does not touch product source. --- _Generated by [Claude Code](https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent daafae6 commit 3f7cb1b

2 files changed

Lines changed: 26 additions & 7 deletions

File tree

‎packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,25 @@ const FIELD: Record<ClassName, string> = { masked: 'role', gated: 'must_change_p
8787
const SUBJECT = { email: 'ald.subject.kq@example.com', name: 'Ald Subject', role: 'aldrolekq' };
8888
const VALUE: Record<ClassName, string | null> = { masked: SUBJECT.role, gated: null, withheld: SUBJECT.email };
8989

90-
/** The explicit rows' events, and the decision keys each may carry. */
90+
/**
91+
* The explicit rows' events, and the decision keys each may carry: `required` is
92+
* written on every row of the event, `optional` only on some (the producer's
93+
* `AdminAuditDecisions` type, `plugin-auth/src/admin-user-endpoints.ts`).
94+
*
95+
* The password set (#23287, ruling C′ item 2, with the revocation ON by default):
96+
* `revokeSessionsAndKeys` is the administrator's switch, written on every forced
97+
* reset. `revokedSessions`, `revokedApiKeys` and `revocationComplete` are what the
98+
* revocation landed, counts only, written only when the switch was on. This
99+
* fixture's reset leaves the switch at its default, so its row carries all three.
100+
*/
91101
const CREATE_EVENT = 'user.admin_created';
92102
const PASSWORD_SET_EVENT = 'user.admin_password_set';
93103
const DECISIONS: Record<string, { required: string[]; optional: string[] }> = {
94104
[CREATE_EVENT]: { required: ['event', 'membershipCreated', 'passwordGenerated', 'placeholderEmail'], optional: ['organizationId'] },
95-
[PASSWORD_SET_EVENT]: { required: ['event', 'passwordGenerated'], optional: [] },
105+
[PASSWORD_SET_EVENT]: {
106+
required: ['event', 'passwordGenerated', 'revokeSessionsAndKeys'],
107+
optional: ['revokedSessions', 'revokedApiKeys', 'revocationComplete'],
108+
},
96109
};
97110

98111
const Anchor = ObjectSchema.create({

‎packages/qa/dogfood/test/bearer-lane-password-change.dogfood.test.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
* ## The defect this pins
77
*
88
* An admin-provisioned user (`mustChangePassword` defaults to true) is gated out
9-
* of every protected route with `403 PASSWORD_EXPIRED` until they rotate their
10-
* password. On the COOKIE lane the escape hatch worked. On the BEARER lane —
9+
* of every protected route with `403 PASSWORD_RESET_REQUIRED` until they rotate
10+
* their password. (#23287, ruling C′ item 2: the administrator-forced change has
11+
* its own code, scope `credential`; it reused `PASSWORD_EXPIRED`, which stays
12+
* natural expiry under `passwordExpiryDays`, until then.) On the COOKIE lane the
13+
* escape hatch worked. On the BEARER lane —
1114
* the documented API/agent/CLI lane — `/auth/change-password` answered **200**,
1215
* the password really rotated, and nothing else happened: `must_change_password`
1316
* stayed `true`, `password_changed_at` stayed `null`, and the caller stayed
@@ -225,7 +228,7 @@ describe('#8049: /auth/change-password clears the force-change flag and enforces
225228
const email = `bearer.lane.8049.${lane.name.replace(/[^a-z]/gi, '').toLowerCase()}@example.com`;
226229
let credential = '';
227230

228-
it('an admin-provisioned user is gated out with 403 PASSWORD_EXPIRED', async () => {
231+
it('an admin-provisioned user is gated out with 403 PASSWORD_RESET_REQUIRED', async () => {
229232
const created = await stack.apiAs(adminToken, 'POST', '/auth/admin/create-user', {
230233
email,
231234
name: `Bearer Lane ${lane.name}`,
@@ -241,9 +244,12 @@ describe('#8049: /auth/change-password clears the force-change flag and enforces
241244

242245
const read = await stack.api('/data/showcase_task?$top=1', { headers: lane.headers(credential) });
243246
// The gate refusal — asserted as code AND status, and distinctly from
244-
// the reuse refusal below (different envelope, different outcome).
247+
// the reuse refusal below (different envelope, different outcome). The
248+
// flag is the administrator's forced change, not expiry: no
249+
// `passwordExpiryDays` is armed here, so the one ruled answer is
250+
// `PASSWORD_RESET_REQUIRED` (ruling C′ item 2).
245251
expect(read.status).toBe(403);
246-
expect((await read.json())?.error?.code).toBe('PASSWORD_EXPIRED');
252+
expect((await read.json())?.error?.code).toBe('PASSWORD_RESET_REQUIRED');
247253
}, 120_000);
248254

249255
it('POST /auth/change-password rotates the password AND clears the force-change flag', async () => {

0 commit comments

Comments
 (0)