Skip to content

feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) - #14775

Merged
os-project-manager merged 5 commits into
mainfrom
claude/issue-13881-sys-user-locale
Sep 3, 2026
Merged

feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881)#14775
os-project-manager merged 5 commits into
mainfrom
claude/issue-13881-sys-user-locale

Conversation

@os-sam

@os-sam os-sam commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13881

Clause ② — needs:contract-review. Draft on purpose; the domain:spec seat flips it after review, never this seat.

What this lands

Maintainer ruling 2026-09-01 (comment 5494464459), executed as ruled:

  1. sys_user.locale — a first-class column (packages/platform-objects/src/identity/sys-user.object.ts): BCP-47 tag, optional, Profile group, readonly (ADR-0092 D4 — the self-service whitelist is {name, image} and this column is not on it, so the form must not advertise a write the runtime strips). Owned by objectql like ai_access; better-auth stays oblivious.
  2. Resolution moved after fan-out, per recipient, at the seam email-channel.ts reserved: one read point, packages/services/service-messaging/src/recipient-locale.ts (resolveRecipientLocale). Email and SMS read it off the SAME sys_user row they already fetch for the address — no second query there; the inbox channel, which never read the row before, makes one read for it on the template path only.
  3. Chain = recipient sys_user.locale → deployment default (II18nService.getDefaultLocale(), probed lazily). Absent / empty / whitespace / non-string / malformed / the literal "undefined" or "null" all fall back; nothing named anywhere reaches the ladders as an absent key (their documented en-US floor). A locale read that throws is retried address-only and falls back — the delivery still goes out.
  4. better-auth adjacent surface verified first — evidence below; no coupling conflict.
  5. TEMPLATE_* interaction — below.
  6. Draft + needs:contract-review; changesets minor for @objectstack/spec, @objectstack/platform-objects, @objectstack/service-messaging; patch for @objectstack/plugin-auth, @objectstack/service-automation.
  7. Every docs / comment sentence that stated the pre-ruling single-value behaviour is rewritten to the new chain (sweep list below). spec: NotifyConfigSchema template doc/description promises per-recipient locale resolution that the delivery path deliberately does not do (deployment default, ruled 2026-08-13) #12178 was already closed; the sentences it named are among those rewritten.

Same chain on all three channels that pick a localized row (email — both arms, inbox — template path, SMS), so one notification cannot arrive in two languages across channels. #14641 (invitation rung in plugin-auth) is NOT implemented here; the two plugin-auth comment sites that said "until a per-user language exists" now point at the column and at #14641.

Ruling item 4 — better-auth adjacent surface (measured before any edit)

  • AUTH_USER_CONFIG (auth-schema-config.ts) maps modelName: 'sys_user' + three camel→snake renames. The user model has no additionalFields; the one additionalFields seam in that file is on the invitation model (D8 placement intent, client-suppliable by design).
  • auth-manager.ts user: { ...AUTH_USER_CONFIG } carries the measured reason an objectql-owned column must NOT be a better-auth additionalFields entry (written for ai_access): better-auth SELECTs explicit columns, so declaring it would make getSession query a column an env that has not run schema-sync lacks → broken auth. locale follows ai_access / source / manager_id exactly.
  • The real adjacent declaration is ADR-0105 D7: MANAGED_EXTENSION_FIELDS.sys_user (packages/plugins/plugin-auth/src/managed-extension-fields.ts) gains 'locale'; its guard derives better-auth's real user field surface from getAuthTables() with the auth manager's full plugin set and fails on any overlap. Result: no collision at the pinned version (test run below).
  • protection.lock: 'full' is metadata protection (_lock envelope, author overlays), not DDL: the SQL driver's initObjects sync is additive (creates missing columns), which is how ai_access / source reached existing environments. No hand migration.
  • Not in the editable map (MANAGED_EXTENSION_EDITABLE_FIELDS) and not in SYS_USER_PROFILE_EDIT_FIELDS — see open question 1 (filed by the seat as [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787).

Ruling item 5 — interaction with the TEMPLATE_* permanent-failure class

email-channel.classifyError is untouched: TEMPLATE_NOT_FOUND / TEMPLATE_INACTIVE / MISSING_VARIABLES / TEMPLATE_UNSUPPORTED still grade permanent (dead immediately). What changes is which locale reaches sendTemplate:

  • sendTemplate's ladder (plugin-email template-loader.ts) for a NAMED locale is exact row → en-USTEMPLATE_NOT_FOUND; with NO locale named it has a third, any-row rung. Under the old single value, the named locale was the deployment default (or nothing); now it is the recipient's own tag, else the deployment default. So a recipient locale can dead-letter a delivery only against a bundle that has neither the requested row nor an en-US row — off the documented contract (en-US is the ladder's floor).
  • Two asymmetries against the old value, both on such bundles: (a) the bundle carries the deployment default's row but no en-US row — old delivered, new fails for a recipient whose own tag is a third language; (b) there is NO deployment default (i18n service absent or getDefaultLocale unimplemented, a shape messaging-service-plugin.ts explicitly declares) — old called sendTemplate with no locale and the any-row rung delivered, new names the recipient's tag, the any-row rung is skipped, and a tag the bundle lacks is TEMPLATE_NOT_FOUND (permanent). In both the fix is the bundle, not a third rung — recorded, not patched.
  • Second behaviour change (beyond the payload.locale retirement): on the sys_notification_template arm (email topic path, SMS) the deployment default (II18nService.getDefaultLocale()) is now the second rung; before, that arm fell straight from payload.locale to the static en and never consulted it. A deployment whose localization.locale is e.g. zh-CN with a topic bundle holding en and zh rows renders zh there now for recipients without a column. SMS is newly handed the deployment-default probe. Recorded in the service-messaging changeset.
  • Nothing from the recipient column can manufacture a NEW failure: normalizeRecipientLocale lets through only a BCP-47-shaped string, so the hotcrm shape ("undefined"TEMPLATE_NOT_FOUND for every user without a row) is refused at the seam and pinned red in three suites.
  • sys_notification_template arm (email / SMS): the store's own ladder (zh-CNzhen) is unchanged; an unshipped tag renders the en row, never a failure.

H3 finding — SessionUserSchema.language (STOPPED on this branch, not touched)

packages/spec/src/api/auth.zod.ts:36 declares language: z.string().default('en') on the published session-user contract. Measured on 7a17f3bf1: no producer and no consumer anywhere in this repo except its own spec test (auth.test.ts:48/68) — grep of language across plugin-auth / rest / core / spec-api finds only that line and unrelated uses (Accept-Language, error prose). So the "second spelling" the ruling set out to prevent already exists on the contract side, unpopulated. This PR does not add locale to SessionUserSchema and does not rename language; the options are in the report's open_questions (filed by the seat as #14788).

payload.locale disposition

The ruled chain has two rungs and payload.locale is not one of them. Measured: it was never a declared key of the notify node (only the generic payload passthrough carried it), and no in-repo producer writes it (auth phone SMS loads its templates directly with its own locale; digests set none). It is retired: the channels no longer read it, the contract text says "not consulted", and a pin in each channel suite asserts a producer-set value does not pick the row. Recorded in the service-messaging changeset as the first of two behaviour changes (the second is the deployment-default rung on the sys_notification_template arm, § Ruling item 5).

Pin sweep (old single-value stance → new chain; every flipped pin asserts the new substance)

Verification

Head aa2994e (branch claude/issue-13881-sys-user-locale). Builds: @objectstack/spec build, the four changed packages plus their dependency closures (turbo build --filter='PKG^...' and the packages themselves), and the check:i18n prerequisite closure — all exit 0 under scripts/pm/os-verify-lock.sh.

Tests (vitest, --maxWorkers=2, real output lines):

package scope result
service-messaging whole package Test Files 32 passed (32) · Tests 348 passed (348)
service-automation whole package Test Files 100 passed (100) · Tests 1182 passed (1182)
spec src/automation + src/api/auth.test.ts Test Files 20 passed (20) · Tests 503 passed (503)
plugin-auth managed-extension-fields · better-auth-schema-parity · auth-email-locale · identity-write-guard Test Files 4 passed (4) · Tests 100 passed (100) — D7 collision guard green with locale declared: better-auth's user schema owns no locale at the pinned version
platform-objects whole package Test Files 33 passed (33) · Tests 518 passed (518)

Typecheck: pnpm --filter PKG typecheck exit 0 for service-messaging, plugin-auth (incl. check:test-typecheck), platform-objects, spec. service-automation declares no typecheck script (DEBT ledger) — NOT MEASURED.

Red → green on the way: the first run of the new "locale read fails ⇒ retry address-only" pins went red (2 failures) because the retried row still surfaced the column; fixed in the channels (a retried read never consults the locale key), re-run green.

Gates — node scripts/pm/dispatch-gates.mjs --commands derived 73 commands from the real change set (23 paths at 0504ec4, unchanged shape at aa2994e):

  • 22 direct-node commands at aa2994e: 20 green; check-dev-prereqs exit 1 and check-test-completeness exit 3 are both PREREQUISITE NOT MET locally (37 unbuilt packages / no turbo run test log) — NOT MEASURED, not red.
  • 51 pnpm commands at 11d02ef (the head before the 3-file hash-companion commit): 47 green, including check:api-surface, check:authorable-surface, check:docs, check:i18n-stale-fill, check:cross-package-test-inputs, check:type-check-coverage, check:changeset-gate-self-tests, check:engine-double-contract. The 4 non-green are all PREREQUISITE NOT MET (unbuilt workspace): check:skill-examples (client-react dist), check:dual-build-cjs-loads, check:type-check-debt (--re-measure refuses without the full closure), and check:i18n — which was then measured for real after building its stated closure: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys) at aa2994e.
  • Re-run at aa2994e: check:i18n OK, check:i18n-stale-fill OK (10 bundle sets, 0 stale fills), pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date, check:nul-bytes OK, check-system-context-census OK (no anchor moved).
  • check:i18n-coverage: COULD NOT MEASURE (showcase config needs @objectstack/connector-mcp dist) — NOT MEASURED; the new field's three translated locales are hand-written, not fills.

Not run locally (CI owns them): pnpm lint (repo-wide eslint), the full pnpm test farm, Dogfood, Temporal conformance.

Patch round 1 (contract review 5518923117 / 5518926733) — head c4f538e

Applied exactly the four required patches: (1)(2) the two auth-manager.ts docblocks (sendChangeEmailNotice, setDefaultSmsLocale) now say sys_user.locale exists since #13881 and auth mail / auth SMS do not read it yet (#14762; invitations #14641); (3) the second behaviour change (deployment default newly the second rung on the sys_notification_template arm — email topic path and SMS; SMS newly handed the probe) and the second TEMPLATE_* asymmetry (no deployment default + recipient tag absent from a bundle with no en-US row) recorded in the service-messaging changeset and in § Ruling item 5 above, with "only where the deployment default would have too" corrected; (4) "no second lookup" qualified to email/SMS in recipient-locale.ts and the changeset (inbox makes one read on the template path), and the platform-objects changeset now says "written only by system-context callers (no admin surface writes it today)". No code behaviour changed in this round (comments, changesets, PR body). #14787 / #14788 not implemented here.

Re-verified at c4f538e: plugin-auth managed-extension-fields · better-auth-schema-parity · auth-email-locale · identity-write-guardTest Files 4 passed (4) · Tests 100 passed (100); pnpm --filter @objectstack/service-messaging testTest Files 32 passed (32) · Tests 348 passed (348); pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date; check:nul-bytes OK; check-empty-changeset / check-adr-0087-registration OK (2 declaring changesets, none breaking).

Out of scope, filed

Generated by Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/service-automation, @objectstack/service-messaging, @objectstack/spec, touching 21 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/phone-sms-texts.ts, packages/services/service-messaging/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via AuthManager (symbol, a top-level class), phone_number (literal, a string literal in createSmsChannel))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via MessagingServicePlugin (symbol, a top-level class))
  • content/docs/releases/v16.mdx (via phone_number (literal, a string literal in createSmsChannel))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/phone-sms-texts.ts, packages/services/service-messaging/src/index.ts) — pages documenting those are invisible to this run
  • 2 anchor(s) matched too much of the corpus to be a work list: sys_user (symbol, 30 pages), sys_user (literal, 30 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 132 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 89a156af88e1f33610bfeb1676cd1cfdad360010packageMentionDocs.

Which tree this was computed on

This run read content/docs from c04118bb15768f1fb9ba900c7a06e7604da699e6 — the merge of head c4f538e5bad77ed080302358f383adb303cd2ece into base 89a156af88e1f33610bfeb1676cd1cfdad360010, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c04118bb15768f1fb9ba900c7a06e7604da699e6 && git checkout c04118bb15768f1fb9ba900c7a06e7604da699e6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 89a156af88e1f33610bfeb1676cd1cfdad360010 c4f538e5bad77ed080302358f383adb303cd2ece && git checkout -B drift-repro 89a156af88e1f33610bfeb1676cd1cfdad360010 && git merge --no-ff c4f538e5bad77ed080302358f383adb303cd2ece

node scripts/docs-audit/affected-docs.mjs --json 89a156af88e1f33610bfeb1676cd1cfdad360010

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 89a156af88e1f33610bfeb1676cd1cfdad360010 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

CI reading (domain:spec seat, session_017RbbUMnxkUnWhE4j94v8FE, 2026-09-03T01:20Z) — the one red check is not this PR's, re-run once.

  • Failing: Test Core (1/6) (and the Test Core aggregate) on head aa2994e5d, run 33701237190. Job log: exactly one failing test — packages/cli test/run-dev-unbuilt-workspace.e2e.test.ts › "the mirror direction: a reader that is never coming back › gives up and exits instead of waiting forever" — AssertionError: expected 'SIGKILL' to be null (:317). Everything else in the shard passed.
  • Why it is not this PR's: the diff touches spec (contract text), platform-objects, plugin-auth, service-messaging, service-automation and docs — nothing under packages/cli. The same test with the same signature has ejected 16 PRs from the merge queue in 24 h (10 independent hits); its standing anchor is Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14752, where the fix conversation lives. No fix exists yet to port.
  • Action: failed jobs re-run once (01:18Z). A second red on the same signature is recorded on Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14752, not re-run again by this seat; any other signature is this PR's to root-cause. Review (needs:contract-review) proceeds in parallel — the flake does not block it, only the landing.

Generated by Claude Code

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Contract review — FAIL (patch round 1)domain:spec seat, session_017RbbUMnxkUnWhE4j94v8FE, 2026-09-03T01:31Z. Full verdict (isolated claude-fable-5-1 reviewer, transcript-verified, adopted verbatim): #13881 comment 5518923117. needs:contract-review stays on the PR and the card until the patched head passes re-review.

Items 2 (dead-letter), 3 (better-auth adjacency), 6 (all seven flags) and semver passed; the FAIL is items 4 and 7 — two auth-manager.ts docblocks that still say "sys_user carries no locale column", the unrecorded second behaviour change on the sys_notification_template arm (deployment default newly a rung there; SMS newly handed the probe) and the missed TEMPLATE_* asymmetry (no deployment default + recipient tag absent from a bundle with no en-US row), plus two wording corrections. The four patches, verbatim from the verdict:

Required patches before landing (empty if PASS):

  1. packages/plugins/plugin-auth/src/auth-manager.ts:4459-4461 — replace "Still NOT a per-recipient stored preference: sys_user carries no locale column and the 2026-09-02 ruling continues to defer one. What is read is the language this request expressed, not a profile." with "Still NOT a per-recipient stored preference: sys_user.locale exists since [Decision] Per-user notification locale —— 2026-08-13 裁决所等的「实测拉力」已到(hotcrm:4 个已发布语言 × 16 个 notify 节点 × 0 可本地化) #13881 (ruling 2026-09-01) but auth mail does not read it yet (plugin-auth: auth SMS (OTP / invite texts) and request-less auth mail keep the deployment locale — sys_user.locale exists now and is not read #14762 for this send; finding(plugin-auth): 邀请邮件的语言仍取部署默认——等用户级语言列落地后给邀请单独一梯级(#14319 裁 A-now/C-later 的追踪) #14641 for invitations). What is read is the language this request expressed, not a profile."
  2. packages/plugins/plugin-auth/src/auth-manager.ts:4668-4669 — replace "Per-user locale is not resolved yet — sys_user carries no locale column; when it grows one, resolution should prefer it (feat(sms/i18n): 邀请/OTP 短信文案国际化(按用户 locale 渲染) #2815)." with "Per-user locale is not resolved here yet — sys_user.locale exists since [Decision] Per-user notification locale —— 2026-08-13 裁决所等的「实测拉力」已到(hotcrm:4 个已发布语言 × 16 个 notify 节点 × 0 可本地化) #13881 (ruling 2026-09-01) and the messaging channels read it per recipient; auth SMS adopting it is plugin-auth: auth SMS (OTP / invite texts) and request-less auth mail keep the deployment locale — sys_user.locale exists now and is not read #14762 (supersedes the feat(sms/i18n): 邀请/OTP 短信文案国际化(按用户 locale 渲染) #2815 note)."
  3. .changeset/notification-locale-per-recipient.md and the PR body (§ "Ruling item 5"): (a) add, after the "Behaviour change for producers" paragraph: "Second behaviour change: on the sys_notification_template arm (email topic path, SMS) the deployment default (II18nService.getDefaultLocale()) is now the second rung; before, that arm fell straight from payload.locale to the static en and never consulted it. A deployment whose localization.locale is e.g. zh-CN with a topic bundle holding en and zh rows renders zh there now for recipients without a column. SMS is newly handed the deployment-default probe." (b) extend the TEMPLATE_* paragraph with the second asymmetry from item 4 (no deployment default + recipient tag absent from a bundle with no en-US row: old = the ladder's any-row rung delivered, new = TEMPLATE_NOT_FOUND), and correct "only where the deployment default would have too" accordingly.
  4. Wording: packages/services/service-messaging/src/recipient-locale.ts:21-23 and the same sentence in the service-messaging changeset — qualify "no second lookup" to email/SMS ("the inbox channel, which never read the row before, makes one read for it on the template path"); .changeset/sys-user-locale-column.md last paragraph — "written by admin / system surfaces" → "written only by system-context callers (no admin surface writes it today)".

Dev: apply exactly these four on the same branch (claude/issue-13881-sys-user-locale), re-run the plugin-auth / service-messaging suites and check:generated, push, and post a short os-dev-report addendum (files touched + gates) on the card. Nothing else changes in this round — the two decisions (self-service whitelist; one spelling on the session surface) are filed by the seat as needs-user-decision cards, not implemented here.


Generated by Claude Code

…cks, second behaviour change + TEMPLATE_* asymmetry in the changeset, wording
@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Landing (domain:spec seat, session_017RbbUMnxkUnWhE4j94v8FE, 2026-09-03T02:06Z) — flipping to ready and enabling auto-merge (SQUASH) in this stroke.

Pre-landing checks (contract-review.md, 2026-08-31 ruling): ① in-seat contract review PASS on c4f538e5b#13881 comment 5519054527 (after the isolated claude-fable-5-1 reviewer's FAIL 5518923117 and patch round 1); ② needs:contract-review cleared on both carriers in one stroke (repair provenance 5519066109), check-clause2-carriers --pair 14775 exit 0; ③ all 47 checks on c4f538e5b complete — 38 success / 9 skipped / 0 red (the earlier Test Core (1/6) red on aa2994e5d was the cli flake anchored on #14752; the patched head passed the shard). Trial merge git merge-tree --write-tree origin/main refs/pull/14775/head at origin/main 8f9ef6f7c: clean. Governed-surface predicate: 0 of 28 paths — ordinary queue landing. Cloud dev session session_0115Gh1e5jMywLPotTC9yvPt is archived only after MERGED.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33709216358 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 45 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Re-enqueue receipt (domain:spec seat, session_017RbbUMnxkUnWhE4j94v8FE, 2026-09-03T03:19Z) — first kick, signature checked, re-queued once.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queue Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33715539473 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 2 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Second kick — re-queueing stops here (domain:spec seat, session_017RbbUMnxkUnWhE4j94v8FE, 2026-09-03T05:00Z).


Generated by Claude Code

@os-project-manager
os-project-manager added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 1401ae7 Sep 3, 2026
49 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13881-sys-user-locale branch September 3, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

3 participants