Skip to content

Commit c71a201

Browse files
committed
test(setup-copy): pin each package's admin copy against design-record vocabulary
One unit test per package (approvals, security, sharing, automation), in the form the service-messaging pin landed: every object the package ships, the security permission sets, and every leaf of the package's objects bundles. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
1 parent 52f99e4 commit c71a201

4 files changed

Lines changed: 455 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Admin-facing copy of the approval objects carries no design-record
5+
* vocabulary.
6+
*
7+
* Setup opens three of these objects from its Approvals group (Requests,
8+
* Action History and Delegations); the sentence under each page title is the
9+
* object's `description`, a field's help tooltip is the field's
10+
* `description`, and the generated bundles carry both into every locale. An
11+
* ADR number, a decision id ("D2"), a review-finding id ("finding F2"), an
12+
* API call ("emit()") or a runtime internal ("kernel") belongs in the
13+
* docblock next to the string, never in the string. Every object this package
14+
* ships is judged, not only the three Setup opens: a nav entry added later
15+
* inherits the rule with nothing to update.
16+
*
17+
* The same rule over the other Setup producers lives in each of them
18+
* (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`,
19+
* `@objectstack/service-messaging`'s `src/objects/admin-copy-vocabulary.test.ts`);
20+
* the matcher is restated rather than imported, because a shared assertion
21+
* module would be one more export for a test to reach across a package for.
22+
*/
23+
24+
import { describe, expect, it } from 'vitest';
25+
import * as Approvals from './index.js';
26+
// Registered by the plugin, not re-exported by the package entry.
27+
import { SysApprovalToken } from './sys-approval-token.object.js';
28+
import { enObjects } from './translations/en.objects.generated.js';
29+
import { zhCNObjects } from './translations/zh-CN.objects.generated.js';
30+
import { jaJPObjects } from './translations/ja-JP.objects.generated.js';
31+
import { esESObjects } from './translations/es-ES.objects.generated.js';
32+
33+
/** The design-record vocabulary an admin-facing string must not carry. */
34+
const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
35+
['an ADR number', /ADR-\d/],
36+
['a decision id', /\bD\d+\b/],
37+
['a review-finding id', /finding F/],
38+
['an API call', /\bemit\(\)/],
39+
['a runtime internal', /\bkernel\b/i],
40+
];
41+
42+
function vocabularyIn(text: string): string[] {
43+
return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name);
44+
}
45+
46+
const COPY_KEYS = new Set(['label', 'pluralLabel', 'description']);
47+
48+
function copyStrings(node: unknown, path: string, out: Array<[string, string]>): void {
49+
if (Array.isArray(node)) {
50+
node.forEach((v, i) => copyStrings(v, `${path}[${i}]`, out));
51+
return;
52+
}
53+
if (!node || typeof node !== 'object') return;
54+
for (const [k, v] of Object.entries(node)) {
55+
const at = path ? `${path}.${k}` : k;
56+
if (typeof v === 'string') {
57+
if (COPY_KEYS.has(k)) out.push([at, v]);
58+
} else {
59+
copyStrings(v, at, out);
60+
}
61+
}
62+
}
63+
64+
function leafStrings(node: unknown, path: string, out: Array<[string, string]>): void {
65+
if (typeof node === 'string') {
66+
out.push([path, node]);
67+
return;
68+
}
69+
if (!node || typeof node !== 'object') return;
70+
for (const [k, v] of Object.entries(node)) leafStrings(v, path ? `${path}.${k}` : k, out);
71+
}
72+
73+
function offenders(strings: Array<[string, string]>): string[] {
74+
return strings
75+
.map(([at, text]) => [at, text, vocabularyIn(text)] as const)
76+
.filter(([, , hits]) => hits.length > 0)
77+
.map(([at, text, hits]) => `${at} (${hits.join(', ')}): ${text}`);
78+
}
79+
80+
type ObjectLike = { name: string; fields: Record<string, unknown> };
81+
function isObjectSchema(v: unknown): v is ObjectLike {
82+
return !!v && typeof v === 'object'
83+
&& typeof (v as ObjectLike).name === 'string'
84+
&& !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object';
85+
}
86+
87+
const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const;
88+
89+
describe('approval objects: admin-facing copy carries no design-record vocabulary', () => {
90+
it('the matcher recognises each kind of vocabulary it exists to keep out', () => {
91+
expect(vocabularyIn('Suspended run this request gates (ADR-0019).')).toEqual(['an ADR number']);
92+
expect(vocabularyIn('Enforced by the shared predicate, decision D2.')).toEqual(['a decision id']);
93+
expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']);
94+
expect(vocabularyIn('One row per emit().')).toEqual(['an API call']);
95+
expect(vocabularyIn('Resumed by a rebuilt kernel.')).toEqual(['a runtime internal']);
96+
expect(vocabularyIn('While someone is away, approvals go to their delegate.')).toEqual([]);
97+
});
98+
99+
it('object, field and view copy of every object this package ships', () => {
100+
const objects: ObjectLike[] = [...(Object.values(Approvals) as unknown[]), SysApprovalToken]
101+
.filter(isObjectSchema);
102+
const names = objects.map((o) => o.name);
103+
for (const name of [
104+
'sys_approval_request', 'sys_approval_action', 'sys_approval_approver',
105+
'sys_approval_token', 'sys_approval_delegation',
106+
]) {
107+
expect(names, `${name} is in the population`).toContain(name);
108+
}
109+
const strings: Array<[string, string]> = [];
110+
for (const def of objects) copyStrings(def, def.name, strings);
111+
expect(offenders(strings)).toEqual([]);
112+
});
113+
114+
it.each(Object.keys(OBJECT_BUNDLES))('every leaf of the %s objects bundle', (locale) => {
115+
const strings: Array<[string, string]> = [];
116+
leafStrings(OBJECT_BUNDLES[locale as keyof typeof OBJECT_BUNDLES], 'objects', strings);
117+
expect(strings.length).toBeGreaterThan(100);
118+
expect(offenders(strings)).toEqual([]);
119+
});
120+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Admin-facing copy of the security objects and the shipped permission sets
5+
* carries no design-record vocabulary.
6+
*
7+
* Setup reaches these through Access Control and the user record page: the
8+
* Permission Sets page lists each shipped set with its `description`, and a
9+
* user's Positions and Permission Sets lists open the `sys_user_position` and
10+
* `sys_user_permission_set` forms, where a field's help tooltip is the field's
11+
* `description`. The generated bundles carry the object copy into every
12+
* locale. An ADR number, a decision id ("D2"), a review-finding id
13+
* ("finding F2"), an API call ("emit()") or a runtime internal ("kernel")
14+
* belongs in the docblock next to the string, never in the string. Every
15+
* object and permission set this package ships is judged, not only the ones
16+
* Setup opens today.
17+
*
18+
* The same rule over the other Setup producers lives in each of them
19+
* (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`,
20+
* `@objectstack/service-messaging`'s `src/objects/admin-copy-vocabulary.test.ts`);
21+
* the matcher is restated rather than imported, because a shared assertion
22+
* module would be one more export for a test to reach across a package for.
23+
*/
24+
25+
import { describe, expect, it } from 'vitest';
26+
import * as SecurityObjects from './index.js';
27+
import { defaultPermissionSets } from './default-permission-sets.js';
28+
import { enObjects } from '../translations/en.objects.generated.js';
29+
import { zhCNObjects } from '../translations/zh-CN.objects.generated.js';
30+
import { jaJPObjects } from '../translations/ja-JP.objects.generated.js';
31+
import { esESObjects } from '../translations/es-ES.objects.generated.js';
32+
33+
/** The design-record vocabulary an admin-facing string must not carry. */
34+
const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
35+
['an ADR number', /ADR-\d/],
36+
['a decision id', /\bD\d+\b/],
37+
['a review-finding id', /finding F/],
38+
['an API call', /\bemit\(\)/],
39+
['a runtime internal', /\bkernel\b/i],
40+
];
41+
42+
function vocabularyIn(text: string): string[] {
43+
return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name);
44+
}
45+
46+
const COPY_KEYS = new Set(['label', 'pluralLabel', 'description']);
47+
48+
function copyStrings(node: unknown, path: string, out: Array<[string, string]>): void {
49+
if (Array.isArray(node)) {
50+
node.forEach((v, i) => copyStrings(v, `${path}[${i}]`, out));
51+
return;
52+
}
53+
if (!node || typeof node !== 'object') return;
54+
for (const [k, v] of Object.entries(node)) {
55+
const at = path ? `${path}.${k}` : k;
56+
if (typeof v === 'string') {
57+
if (COPY_KEYS.has(k)) out.push([at, v]);
58+
} else {
59+
copyStrings(v, at, out);
60+
}
61+
}
62+
}
63+
64+
function leafStrings(node: unknown, path: string, out: Array<[string, string]>): void {
65+
if (typeof node === 'string') {
66+
out.push([path, node]);
67+
return;
68+
}
69+
if (!node || typeof node !== 'object') return;
70+
for (const [k, v] of Object.entries(node)) leafStrings(v, path ? `${path}.${k}` : k, out);
71+
}
72+
73+
function offenders(strings: Array<[string, string]>): string[] {
74+
return strings
75+
.map(([at, text]) => [at, text, vocabularyIn(text)] as const)
76+
.filter(([, , hits]) => hits.length > 0)
77+
.map(([at, text, hits]) => `${at} (${hits.join(', ')}): ${text}`);
78+
}
79+
80+
type ObjectLike = { name: string; fields: Record<string, unknown> };
81+
function isObjectSchema(v: unknown): v is ObjectLike {
82+
return !!v && typeof v === 'object'
83+
&& typeof (v as ObjectLike).name === 'string'
84+
&& !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object';
85+
}
86+
87+
const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const;
88+
89+
describe('security objects and permission sets: admin-facing copy carries no design-record vocabulary', () => {
90+
it('the matcher recognises each kind of vocabulary it exists to keep out', () => {
91+
expect(vocabularyIn('Assigns a position (ADR-0057).')).toEqual(['an ADR number']);
92+
expect(vocabularyIn('Enforced fail-closed at resolution time (D2).')).toEqual(['a decision id']);
93+
expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']);
94+
expect(vocabularyIn('One row per emit().')).toEqual(['an API call']);
95+
expect(vocabularyIn('The Kernel API is additive.')).toEqual(['a runtime internal']);
96+
expect(vocabularyIn('Which positions each user holds, one row per assignment.')).toEqual([]);
97+
});
98+
99+
it('object, field and view copy of every object this package exports', () => {
100+
const objects: ObjectLike[] = (Object.values(SecurityObjects) as unknown[]).filter(isObjectSchema);
101+
const names = objects.map((o) => o.name);
102+
for (const name of ['sys_user_permission_set', 'sys_user_position', 'sys_audience_binding_suggestion']) {
103+
expect(names, `${name} is in the population`).toContain(name);
104+
}
105+
const strings: Array<[string, string]> = [];
106+
for (const def of objects) copyStrings(def, def.name, strings);
107+
expect(offenders(strings)).toEqual([]);
108+
});
109+
110+
it('the label and description of every permission set this package ships', () => {
111+
const names = defaultPermissionSets.map((ps) => ps.name);
112+
for (const name of ['organization_admin', 'organization_admin_no_bypass', 'mcp_agent_data_read', 'mcp_agent_data_write']) {
113+
expect(names, `${name} is in the population`).toContain(name);
114+
}
115+
const strings: Array<[string, string]> = [];
116+
for (const ps of defaultPermissionSets) {
117+
if (typeof ps.label === 'string') strings.push([`${ps.name}.label`, ps.label]);
118+
if (typeof ps.description === 'string') strings.push([`${ps.name}.description`, ps.description]);
119+
}
120+
expect(offenders(strings)).toEqual([]);
121+
});
122+
123+
it.each(Object.keys(OBJECT_BUNDLES))('every leaf of the %s objects bundle', (locale) => {
124+
const strings: Array<[string, string]> = [];
125+
leafStrings(OBJECT_BUNDLES[locale as keyof typeof OBJECT_BUNDLES], 'objects', strings);
126+
expect(strings.length).toBeGreaterThan(50);
127+
expect(offenders(strings)).toEqual([]);
128+
});
129+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Admin-facing copy of the sharing objects carries no design-record
5+
* vocabulary.
6+
*
7+
* Setup opens two of these objects from Access Control (Sharing Rules and
8+
* Record Shares); the sentence under each page title is the object's
9+
* `description`, a field's help tooltip is the field's `description`, and the
10+
* generated bundles carry both into every locale. An ADR number, a decision
11+
* id ("D2"), a review-finding id ("finding F2"), an API call ("emit()") or a
12+
* runtime internal ("kernel") belongs in the docblock next to the string,
13+
* never in the string. Every object this package ships is judged, not only
14+
* the two Setup opens: a nav entry added later inherits the rule with nothing
15+
* to update. (`sys_business_unit` and its member object, which this plugin
16+
* also registers, are `@objectstack/platform-objects`' and are judged there.)
17+
*
18+
* The same rule over the other Setup producers lives in each of them
19+
* (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`,
20+
* `@objectstack/service-messaging`'s `src/objects/admin-copy-vocabulary.test.ts`);
21+
* the matcher is restated rather than imported, because a shared assertion
22+
* module would be one more export for a test to reach across a package for.
23+
*/
24+
25+
import { describe, expect, it } from 'vitest';
26+
import * as SharingObjects from './index.js';
27+
import { enObjects } from '../translations/en.objects.generated.js';
28+
import { zhCNObjects } from '../translations/zh-CN.objects.generated.js';
29+
import { jaJPObjects } from '../translations/ja-JP.objects.generated.js';
30+
import { esESObjects } from '../translations/es-ES.objects.generated.js';
31+
32+
/** The design-record vocabulary an admin-facing string must not carry. */
33+
const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
34+
['an ADR number', /ADR-\d/],
35+
['a decision id', /\bD\d+\b/],
36+
['a review-finding id', /finding F/],
37+
['an API call', /\bemit\(\)/],
38+
['a runtime internal', /\bkernel\b/i],
39+
];
40+
41+
function vocabularyIn(text: string): string[] {
42+
return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name);
43+
}
44+
45+
const COPY_KEYS = new Set(['label', 'pluralLabel', 'description']);
46+
47+
function copyStrings(node: unknown, path: string, out: Array<[string, string]>): void {
48+
if (Array.isArray(node)) {
49+
node.forEach((v, i) => copyStrings(v, `${path}[${i}]`, out));
50+
return;
51+
}
52+
if (!node || typeof node !== 'object') return;
53+
for (const [k, v] of Object.entries(node)) {
54+
const at = path ? `${path}.${k}` : k;
55+
if (typeof v === 'string') {
56+
if (COPY_KEYS.has(k)) out.push([at, v]);
57+
} else {
58+
copyStrings(v, at, out);
59+
}
60+
}
61+
}
62+
63+
function leafStrings(node: unknown, path: string, out: Array<[string, string]>): void {
64+
if (typeof node === 'string') {
65+
out.push([path, node]);
66+
return;
67+
}
68+
if (!node || typeof node !== 'object') return;
69+
for (const [k, v] of Object.entries(node)) leafStrings(v, path ? `${path}.${k}` : k, out);
70+
}
71+
72+
function offenders(strings: Array<[string, string]>): string[] {
73+
return strings
74+
.map(([at, text]) => [at, text, vocabularyIn(text)] as const)
75+
.filter(([, , hits]) => hits.length > 0)
76+
.map(([at, text, hits]) => `${at} (${hits.join(', ')}): ${text}`);
77+
}
78+
79+
type ObjectLike = { name: string; fields: Record<string, unknown> };
80+
function isObjectSchema(v: unknown): v is ObjectLike {
81+
return !!v && typeof v === 'object'
82+
&& typeof (v as ObjectLike).name === 'string'
83+
&& !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object';
84+
}
85+
86+
const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const;
87+
88+
describe('sharing objects: admin-facing copy carries no design-record vocabulary', () => {
89+
it('the matcher recognises each kind of vocabulary it exists to keep out', () => {
90+
expect(vocabularyIn('Sharing grant (ADR-0052 §5).')).toEqual(['an ADR number']);
91+
expect(vocabularyIn('Placement intent, decision D12.')).toEqual(['a decision id']);
92+
expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']);
93+
expect(vocabularyIn('One row per emit().')).toEqual(['an API call']);
94+
expect(vocabularyIn('The Kernel API is additive.')).toEqual(['a runtime internal']);
95+
expect(vocabularyIn('Records shared beyond their owner, one row per grant.')).toEqual([]);
96+
});
97+
98+
it('object, field and view copy of every object this package exports', () => {
99+
const objects: ObjectLike[] = (Object.values(SharingObjects) as unknown[]).filter(isObjectSchema);
100+
const names = objects.map((o) => o.name);
101+
for (const name of ['sys_record_share', 'sys_sharing_rule', 'sys_share_link']) {
102+
expect(names, `${name} is in the population`).toContain(name);
103+
}
104+
const strings: Array<[string, string]> = [];
105+
for (const def of objects) copyStrings(def, def.name, strings);
106+
expect(offenders(strings)).toEqual([]);
107+
});
108+
109+
it.each(Object.keys(OBJECT_BUNDLES))('every leaf of the %s objects bundle', (locale) => {
110+
const strings: Array<[string, string]> = [];
111+
leafStrings(OBJECT_BUNDLES[locale as keyof typeof OBJECT_BUNDLES], 'objects', strings);
112+
expect(strings.length).toBeGreaterThan(100);
113+
expect(offenders(strings)).toEqual([]);
114+
});
115+
});

0 commit comments

Comments
 (0)