Skip to content

Commit 0f39075

Browse files
committed
test: pin administrator wording and zh-CN text on Setup forms, queue pages and mail delivery
Red on main: developer terms in served help, no sys_queue translations, English mail status and test toast under zh-CN. Claude-Session: https://claude.ai/code/session_01Utp6CbDNK83SuSb8zsRt2s Co-authored-by: Claude <noreply@anthropic.com>
1 parent fa9c0aa commit 0f39075

6 files changed

Lines changed: 311 additions & 6 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The help on Setup's New business unit, Edit organization and user forms
5+
* speaks to the administrator filling them in.
6+
*
7+
* A field's help tooltip is the served translation of its `description`.
8+
* These used to describe the implementation: the org tree as a graph
9+
* ("graph semantics", "expanded by graph queries"), a null value
10+
* ("Null = root of tenant"), an acronym for the upstream system ("HRIS"), a
11+
* boolean literal ("When true", zh-CN 「为 true 时」) and an HTTP status code.
12+
* What the administrator needs is what turning the field on does, and what to
13+
* fill in. This pins the served zh-CN text and the English source.
14+
*/
15+
16+
import { describe, expect, it } from 'vitest';
17+
import { SetupAppTranslations } from '../apps/translations/setup.translation.js';
18+
import { SysBusinessUnit } from './sys-business-unit.object.js';
19+
import { SysOrganization } from './sys-organization.object.js';
20+
import { SysUser } from './sys-user.object.js';
21+
22+
/** Implementation vocabulary an administrator's field help must not carry. */
23+
const DEVELOPER_TERMS: ReadonlyArray<readonly [string, RegExp]> = [
24+
['graph vocabulary', /图谱|\bgraph\b/i],
25+
['a null literal', /\bNull\b|为空 =/],
26+
['an acronym for the upstream system', /\bHRIS\b/],
27+
['a boolean literal', /为 ?(true|false) ?时|\bWhen (true|false)\b/i],
28+
['an HTTP status code', /\b[45]\d\d [A-Z][A-Z_]{3,}\b/],
29+
];
30+
31+
const FORMS = {
32+
sys_business_unit: {
33+
schema: SysBusinessUnit,
34+
fields: ['kind', 'parent_business_unit_id', 'active', 'effective_from', 'effective_to', 'external_ref'],
35+
},
36+
sys_organization: { schema: SysOrganization, fields: ['require_mfa'] },
37+
sys_user: { schema: SysUser, fields: ['banned', 'must_change_password'] },
38+
} as const;
39+
40+
function termsIn(text: string): string[] {
41+
return DEVELOPER_TERMS.filter(([, re]) => re.test(text)).map(([name]) => name);
42+
}
43+
44+
type ObjectCopy = { fields?: Record<string, { help?: string }> };
45+
46+
describe('Setup forms: business unit, organization and user help reads as administrator wording', () => {
47+
const zhCN = (SetupAppTranslations['zh-CN'] as { objects: Record<string, ObjectCopy> }).objects;
48+
49+
for (const [object, { schema, fields }] of Object.entries(FORMS)) {
50+
for (const field of fields) {
51+
it(`${object}.${field}: the served zh-CN help is Chinese and names no implementation term`, () => {
52+
const help = zhCN[object]?.fields?.[field]?.help ?? '';
53+
expect(help).toMatch(/[一-鿿]/);
54+
expect(termsIn(help), help).toEqual([]);
55+
});
56+
57+
it(`${object}.${field}: the English source names no implementation term`, () => {
58+
const description = (schema.fields as Record<string, { description?: string }>)[field]?.description ?? '';
59+
expect(description.length).toBeGreaterThan(0);
60+
expect(termsIn(description), description).toEqual([]);
61+
});
62+
}
63+
}
64+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* An out-of-office rule's "Valid Until" help says when the rule stops, not
5+
* which interval notation the check uses.
6+
*
7+
* The help tooltip on the `sys_approval_delegation` form is the served
8+
* translation of the field's `description`. It used to read "half-open
9+
* [from, until)" (zh-CN 「半开区间 [from, until)」): the shape of the check,
10+
* which an administrator has no use for. This pins the served zh-CN text and
11+
* the English source.
12+
*/
13+
14+
import { describe, expect, it } from 'vitest';
15+
import { ApprovalsTranslations } from './index.js';
16+
import { SysApprovalDelegation } from '../sys-approval-delegation.object.js';
17+
18+
const INTERVAL_NOTATION = /半开区间|half-open|\[from, until\)/i;
19+
20+
type ObjectCopy = { fields?: Record<string, { help?: string }> };
21+
22+
describe('sys_approval_delegation.valid_until: help reads as administrator wording', () => {
23+
it('the served zh-CN help is Chinese and carries no interval notation', () => {
24+
const objects = (ApprovalsTranslations['zh-CN'] as { objects: Record<string, ObjectCopy> }).objects;
25+
const help = objects.sys_approval_delegation?.fields?.valid_until?.help ?? '';
26+
expect(help).toMatch(/[一-鿿]/);
27+
expect(help).not.toMatch(INTERVAL_NOTATION);
28+
});
29+
30+
it('the English source carries no interval notation', () => {
31+
const description = (SysApprovalDelegation.fields as Record<string, { description?: string }>).valid_until
32+
?.description ?? '';
33+
expect(description.length).toBeGreaterThan(0);
34+
expect(description).not.toMatch(INTERVAL_NOTATION);
35+
});
36+
});

‎packages/plugins/plugin-email/src/mail-log-provider.pin.test.ts‎

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,36 @@ interface Resolved { value: unknown; source?: string }
2727
function fakeSettings(values: Record<string, Resolved>) {
2828
const listeners: Array<() => void> = [];
2929
const actions = new Map<string, (a: any) => Promise<any>>();
30-
let liveStatus: (() => Promise<LiveMailTransportStatus | undefined>) | undefined;
30+
type LiveStatusFn = (input?: { locale?: string }) => Promise<LiveMailTransportStatus | undefined>;
31+
let liveStatus: LiveStatusFn | undefined;
3132
return {
3233
values,
3334
createClient: () => ({}),
3435
getNamespace: async () => ({ values }),
3536
subscribe: (_ns: string, cb: () => void) => { listeners.push(cb); },
3637
registerAction: (ns: string, id: string, fn: (a: any) => Promise<any>) => { actions.set(`${ns}/${id}`, fn); },
37-
registerLiveStatus: (ns: string, fn: () => Promise<LiveMailTransportStatus | undefined>) => {
38+
registerLiveStatus: (ns: string, fn: LiveStatusFn) => {
3839
if (ns === 'mail') liveStatus = fn;
3940
},
4041
/** Patch the snapshot and emit settings:changed, as a page save does. */
4142
save(patch: Record<string, Resolved>) {
4243
Object.assign(values, patch);
4344
for (const l of listeners) l();
4445
},
45-
/** What the page states, as the settings GET route asks for it (waits for a running re-apply). */
46-
statement: () => liveStatus!(),
46+
/**
47+
* What the page states, as the settings GET route asks for it (waits for a
48+
* running re-apply), in the caller's display `locale` when one is given.
49+
*/
50+
statement: (locale?: string) => liveStatus!(locale ? { locale } : undefined),
4751
/**
4852
* The "Send test email" answer for the stored values — with nothing edited
49-
* on the form, or with `form` as the unsaved form state the page posts.
53+
* on the form, or with `form` as the unsaved form state the page posts —
54+
* in the caller's display `locale` when one is given.
5055
*/
51-
sendTest: (form?: Record<string, unknown>) => actions.get('mail/test')!({
56+
sendTest: (form?: Record<string, unknown>, locale?: string) => actions.get('mail/test')!({
5257
values: Object.fromEntries(Object.entries(values).map(([k, v]) => [k, v.value])),
5358
payload: form ? { to: 'admin@example.test', values: form } : { to: 'admin@example.test' },
59+
...(locale ? { locale } : {}),
5460
}),
5561
};
5662
}
@@ -226,3 +232,61 @@ describe('"Send test email" with provider "log" on the form, not saved, while SM
226232
expect(answer.message).toMatch(/^Sent test email to admin@example\.test via smtp \(smtp\.form\.test:587\)/);
227233
});
228234
});
235+
236+
// Setup → Email in the caller's display language. The page's statement and the
237+
// "Send test email" answer are both composed here, per request, so both take
238+
// the locale the settings routes read from the request (`Accept-Language`,
239+
// else `?locale`). English stays the answer for no locale and for a language
240+
// this plugin ships no copy for.
241+
describe('Setup → Email answers in the caller\'s language (zh-CN)', () => {
242+
const HAN = /[一-鿿]/;
243+
const SMTP_SAVED = {
244+
provider: { value: 'smtp', source: 'global' },
245+
smtp_host: { value: 'smtp.saved.test', source: 'global' },
246+
};
247+
248+
it('the out-of-the-box statement says, in Chinese, that no mail is sent', async () => {
249+
const h = await boot({});
250+
const statement = await h.settings.statement('zh-CN');
251+
expect(statement?.label).toBe('当前不会发出任何邮件');
252+
expect(statement?.bannerSeverity).toBe('warning');
253+
expect(statement?.bannerText).toMatch(HAN);
254+
expect(statement?.bannerText).not.toMatch(/live transport|sys_email/);
255+
});
256+
257+
it('a delivering transport is named in a Chinese statement', async () => {
258+
const h = await boot(SMTP_SAVED);
259+
const statement = await h.settings.statement('zh-CN');
260+
expect(statement?.label).toBe('邮件通过 SMTP (smtp.saved.test:587) 投递');
261+
expect(statement?.bannerSeverity).toBe('info');
262+
expect(statement?.bannerText).toMatch(HAN);
263+
});
264+
265+
it('the test toast reports the send in Chinese', async () => {
266+
const h = await boot(SMTP_SAVED);
267+
nm.sendMail.mockClear();
268+
const answer = await h.settings.sendTest(
269+
{ provider: 'smtp', smtp_host: 'smtp.form.test', smtp_port: 587 },
270+
'zh-CN',
271+
);
272+
expect(nm.sendMail).toHaveBeenCalledTimes(1);
273+
expect(answer).toMatchObject({ ok: true, severity: 'info' });
274+
expect(answer.message).toMatch(/^测试邮件已发送至 admin@example\.test,经由 smtp \(smtp\.form\.test:587\)/);
275+
expect(answer.message).not.toMatch(/Sent test email/);
276+
});
277+
278+
it('"None (log only)" on the form: the warning is Chinese and names the live transport in Chinese', async () => {
279+
const h = await boot(SMTP_SAVED);
280+
nm.sendMail.mockClear();
281+
const answer = await h.settings.sendTest({ provider: 'log' }, 'zh-CN');
282+
expect(nm.sendMail).not.toHaveBeenCalled();
283+
expect(answer).toMatchObject({ ok: false, severity: 'warning' });
284+
expect(answer.message).toMatch(/^没有投递任何邮件/);
285+
expect(answer.message).toContain('邮件通过 SMTP (smtp.saved.test:587) 投递');
286+
});
287+
288+
it('CONTROL — a language with no copy answers in English', async () => {
289+
const h = await boot({});
290+
expect((await h.settings.statement('fr-FR'))?.label).toBe('No mail leaves this machine');
291+
});
292+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The validity-window and reason help on a permission set assignment and a
5+
* position assignment speaks to the administrator filling in the form.
6+
*
7+
* Setup opens `sys_user_permission_set` (New permission set assignment) and
8+
* `sys_user_position` from a user's record, and each field's help tooltip is
9+
* the served translation of its `description`. The help used to describe the
10+
* implementation: an enforcement posture ("fail-closed"), interval notation
11+
* ("half-open [from, until)") and an internal grant class ("agent grants").
12+
* What the administrator needs is when the assignment starts and stops, and
13+
* what to write down. This pins the served zh-CN text and the English source.
14+
*/
15+
16+
import { describe, expect, it } from 'vitest';
17+
import { SecurityTranslations } from './index.js';
18+
import { SysUserPermissionSet, SysUserPosition } from '../objects/index.js';
19+
20+
/** Implementation vocabulary an administrator's field help must not carry. */
21+
const DEVELOPER_TERMS: ReadonlyArray<readonly [string, RegExp]> = [
22+
['an enforcement posture', /fail-closed/i],
23+
['interval notation', /半开区间|half-open|\[from, until\)/i],
24+
['an internal grant class', /智能体授权|\bagent grants?\b|task\/run attribution/i],
25+
];
26+
27+
const FORMS = {
28+
sys_user_permission_set: SysUserPermissionSet,
29+
sys_user_position: SysUserPosition,
30+
} as const;
31+
const FIELDS = ['valid_from', 'valid_until', 'reason'] as const;
32+
33+
function termsIn(text: string): string[] {
34+
return DEVELOPER_TERMS.filter(([, re]) => re.test(text)).map(([name]) => name);
35+
}
36+
37+
type FieldCopy = { help?: string };
38+
type ObjectCopy = { fields?: Record<string, FieldCopy> };
39+
40+
describe('assignment forms: validity and reason help reads as administrator wording', () => {
41+
const zhCN = (SecurityTranslations['zh-CN'] as { objects: Record<string, ObjectCopy> }).objects;
42+
43+
for (const [object, schema] of Object.entries(FORMS)) {
44+
for (const field of FIELDS) {
45+
it(`${object}.${field}: the served zh-CN help is Chinese and names no implementation term`, () => {
46+
const help = zhCN[object]?.fields?.[field]?.help ?? '';
47+
expect(help).toMatch(/[一-鿿]/);
48+
expect(termsIn(help), help).toEqual([]);
49+
});
50+
51+
it(`${object}.${field}: the English source names no implementation term`, () => {
52+
const description = (schema.fields as Record<string, { description?: string }>)[field]?.description ?? '';
53+
expect(description.length).toBeGreaterThan(0);
54+
expect(termsIn(description), description).toEqual([]);
55+
});
56+
}
57+
}
58+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The 队列 and 队列成员 pages read in Chinese on a zh-CN console.
5+
*
6+
* Setup's Queues and Queue Members pages draw their title, the 新建 button
7+
* (「新建」 + the object label), the list columns and every form field's
8+
* label and help from the served translation of `sys_queue` and
9+
* `sys_queue_member`. This package's bundle carried its three sharing objects
10+
* only, so both pages fell back to the English object metadata and the button
11+
* read 「新建Queue」. This pins what the bundle serves for both objects.
12+
*/
13+
14+
import { describe, expect, it } from 'vitest';
15+
import { SharingTranslations } from './index.js';
16+
import { SysQueue, SysQueueMember } from '../objects/index.js';
17+
18+
type FieldCopy = { label?: string; help?: string; options?: Record<string, string> };
19+
type ObjectCopy = { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, FieldCopy> };
20+
21+
const HAN = /[一-鿿]/;
22+
23+
describe('queue pages: the served zh-CN bundle translates sys_queue and sys_queue_member', () => {
24+
const zhCN = (SharingTranslations['zh-CN'] as { objects: Record<string, ObjectCopy> }).objects;
25+
26+
it('names the two objects 队列 and 队列成员', () => {
27+
expect(zhCN.sys_queue?.label).toBe('队列');
28+
expect(zhCN.sys_queue?.pluralLabel).toBe('队列');
29+
expect(zhCN.sys_queue_member?.label).toBe('队列成员');
30+
expect(zhCN.sys_queue_member?.pluralLabel).toBe('队列成员');
31+
});
32+
33+
for (const schema of [SysQueue, SysQueueMember]) {
34+
it(`${schema.name}: the page description and every field label and help are Chinese`, () => {
35+
const copy = zhCN[schema.name];
36+
expect(copy?.description ?? '').toMatch(HAN);
37+
for (const [field, def] of Object.entries(schema.fields as Record<string, { description?: string }>)) {
38+
expect(copy?.fields?.[field]?.label ?? '', `${schema.name}.${field}.label`).toMatch(HAN);
39+
if (def.description) {
40+
expect(copy?.fields?.[field]?.help ?? '', `${schema.name}.${field}.help`).toMatch(HAN);
41+
}
42+
}
43+
});
44+
}
45+
46+
it('sys_queue_member: every member type option is Chinese', () => {
47+
const options = zhCN.sys_queue_member?.fields?.recipient_type?.options ?? {};
48+
expect(Object.keys(options).sort()).toEqual(
49+
['business_unit', 'position', 'team', 'unit_and_subordinates', 'user'],
50+
);
51+
for (const [value, label] of Object.entries(options)) expect(label, value).toMatch(HAN);
52+
});
53+
});

‎packages/services/service-settings/src/settings-routes.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,36 @@ describe('settings-routes', () => {
151151
expect(state.body.data.ok).toBe(true);
152152
});
153153

154+
// The answer an action composes (the "Send test email" toast) is shown to
155+
// the caller, so the handler is told the caller's display language: the top
156+
// Accept-Language tag, else `?locale`, the same reading the page's
157+
// live-status statement gets. It authorizes nothing and stays out of `ctx`.
158+
it('POST action hands the handler the caller\'s display locale, outside ctx', async () => {
159+
const http = new MockHttp();
160+
const svc = new SettingsService({ env: {} });
161+
svc.registerManifest(brandingSettingsManifest);
162+
const seen: Array<{ locale?: string; ctxKeys: string[] }> = [];
163+
svc.registerAction('branding', 'ping', ({ locale, ctx }) => {
164+
seen.push({ locale, ctxKeys: Object.keys(ctx) });
165+
return { ok: true, message: 'pong' };
166+
});
167+
registerSettingsRoutes(http, svc, { contextFromRequest: adminProvider });
168+
169+
const h = http.routes.get('POST /api/settings/:namespace/:actionId')!;
170+
const zh = makeReqRes({
171+
params: { namespace: 'branding', actionId: 'ping' },
172+
body: null,
173+
headers: { 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8' },
174+
});
175+
await h(zh.req, zh.res);
176+
const none = makeReqRes({ params: { namespace: 'branding', actionId: 'ping' }, body: null });
177+
await h(none.req, none.res);
178+
179+
expect(zh.state.status).toBe(200);
180+
expect(seen.map((s) => s.locale)).toEqual(['zh-CN', undefined]);
181+
expect(seen[0].ctxKeys).not.toContain('locale');
182+
});
183+
154184
// ── [Finding-1] the DEFAULT (no verified provider) is SECURE ──────────────
155185
// Header-trusted identity is gone; an unauthenticated request can neither
156186
// enumerate protected namespaces nor write them. Since the family took the

0 commit comments

Comments
 (0)