@company-semantics/contracts 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/__tests__/resource-keys.test.ts +30 -23
- package/src/admin/authz-simulate.ts +4 -4
- package/src/admin/direct-grants.ts +2 -2
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +97 -0
- package/src/api/http/routes/ai-chat.ts +3 -3
- package/src/api/http/utils/resource-response.ts +5 -2
- package/src/api/index.ts +4 -4
- package/src/api/primitives.ts +6 -2
- package/src/auth/README.md +1 -0
- package/src/auth/index.ts +12 -5
- package/src/autotune.ts +5 -1
- package/src/billing/index.ts +1 -1
- package/src/billing/types.ts +1 -1
- package/src/chat/README.md +3 -0
- package/src/chat/__tests__/runtime-profile.test.ts +68 -48
- package/src/chat/index.ts +10 -4
- package/src/chat/runtime-profile.ts +25 -10
- package/src/chat/schemas.ts +49 -41
- package/src/chat/types.ts +48 -42
- package/src/ci-envelope/README.md +2 -0
- package/src/ci-envelope/__tests__/transitions.test.ts +56 -56
- package/src/ci-envelope/index.ts +2 -2
- package/src/ci-envelope/types.ts +20 -20
- package/src/ci-results/index.ts +2 -2
- package/src/ci-results/repo-ci-result.ts +15 -12
- package/src/compatibility.ts +6 -6
- package/src/content/index.ts +10 -4
- package/src/content/schemas.ts +42 -24
- package/src/dispatch/index.ts +18 -15
- package/src/email/__tests__/registry.test.ts +81 -77
- package/src/email/index.ts +3 -3
- package/src/email/registry.ts +25 -25
- package/src/email/types.ts +43 -43
- package/src/errors/index.ts +8 -8
- package/src/execution/__tests__/events.test.ts +42 -42
- package/src/execution/__tests__/lifecycle.test.ts +192 -190
- package/src/execution/__tests__/registry.test.ts +114 -114
- package/src/execution/audit-export.ts +4 -4
- package/src/execution/errors.ts +7 -7
- package/src/execution/event-metadata.ts +4 -4
- package/src/execution/events.ts +23 -21
- package/src/execution/expiry.ts +5 -5
- package/src/execution/hash-chain.ts +2 -2
- package/src/execution/index.ts +19 -28
- package/src/execution/kinds.ts +7 -7
- package/src/execution/lifecycle.ts +33 -33
- package/src/execution/registry.ts +63 -63
- package/src/execution/schemas.ts +31 -23
- package/src/execution/status.ts +45 -26
- package/src/execution/summary.ts +16 -17
- package/src/execution/timeline-ui.ts +9 -9
- package/src/execution/types.ts +31 -25
- package/src/generated/openapi-routes.ts +2 -0
- package/src/guards/config.ts +22 -18
- package/src/guards/index.ts +4 -4
- package/src/guards/types.ts +32 -24
- package/src/identity/__tests__/avatar.test.ts +68 -59
- package/src/identity/avatar.ts +8 -8
- package/src/identity/display-name.ts +3 -3
- package/src/identity/index.ts +8 -8
- package/src/identity/people-org-chart.ts +8 -4
- package/src/identity/schemas.ts +28 -18
- package/src/identity/types.ts +5 -5
- package/src/impersonation/index.ts +5 -5
- package/src/impersonation/schemas.ts +15 -9
- package/src/impersonation-events.ts +21 -21
- package/src/impersonation.ts +25 -24
- package/src/index.ts +118 -90
- package/src/interfaces/mcp/tools/help.ts +19 -19
- package/src/internal-admin.ts +6 -6
- package/src/mcp/README.md +2 -0
- package/src/mcp/__tests__/capability-graph.test.ts +290 -290
- package/src/mcp/capability-graph.ts +42 -40
- package/src/mcp/failure-context.ts +1 -3
- package/src/mcp/index.ts +69 -56
- package/src/mcp/resources.ts +9 -9
- package/src/meetings/index.ts +2 -2
- package/src/meetings/schemas.ts +51 -34
- package/src/message-parts/README.md +2 -0
- package/src/message-parts/__tests__/builder.test.ts +142 -142
- package/src/message-parts/__tests__/confirmation.test.ts +100 -86
- package/src/message-parts/__tests__/preview.test.ts +63 -63
- package/src/message-parts/__tests__/wire.test.ts +130 -124
- package/src/message-parts/builder.ts +23 -23
- package/src/message-parts/confirmation.ts +17 -14
- package/src/message-parts/execution.ts +7 -7
- package/src/message-parts/index.ts +10 -10
- package/src/message-parts/lifecycle.ts +25 -25
- package/src/message-parts/preview.ts +30 -30
- package/src/message-parts/types.ts +27 -27
- package/src/message-parts/wire.ts +24 -24
- package/src/mutations.ts +2 -2
- package/src/observability.ts +23 -11
- package/src/org/__tests__/org-units.test.ts +131 -96
- package/src/org/__tests__/tree-ordering.test.ts +57 -37
- package/src/org/__tests__/view-scopes.test.ts +40 -40
- package/src/org/domain.ts +9 -9
- package/src/org/index.ts +31 -21
- package/src/org/org-units.ts +34 -20
- package/src/org/schemas.ts +261 -124
- package/src/org/sharing.ts +17 -13
- package/src/org/tree-ordering.ts +3 -1
- package/src/org/types.ts +54 -47
- package/src/org/view-scopes.ts +9 -9
- package/src/permissions/access-levels.ts +7 -2
- package/src/permissions/access-source.ts +6 -6
- package/src/permissions/index.ts +5 -5
- package/src/permissions/orgchart-roles.ts +7 -7
- package/src/permissions/permission-introspection.ts +7 -5
- package/src/permissions/share-api.ts +19 -9
- package/src/pressure.ts +4 -4
- package/src/queryIntent.ts +21 -21
- package/src/ralph/__tests__/prd-groups.test.ts +159 -159
- package/src/ralph/__tests__/prd.test.ts +30 -30
- package/src/ralph/index.ts +3 -8
- package/src/ralph/prd.ts +33 -33
- package/src/ralph/progress.ts +1 -1
- package/src/rate-limit/README.md +4 -4
- package/src/rate-limit/index.ts +3 -3
- package/src/requests.ts +36 -8
- package/src/resource-keys.ts +207 -124
- package/src/resource-registry.ts +5 -5
- package/src/route-builder.ts +3 -3
- package/src/safe-mode.ts +2 -2
- package/src/security/index.ts +4 -4
- package/src/security/org-secrets.ts +13 -9
- package/src/security/secret.ts +3 -3
- package/src/sse.ts +3 -1
- package/src/system/README.md +3 -0
- package/src/system/capabilities.ts +22 -23
- package/src/system/diagram.ts +45 -45
- package/src/system/index.ts +14 -14
- package/src/tiers.ts +1 -1
- package/src/timeouts.ts +1 -1
- package/src/tracing.ts +30 -30
- package/src/types/analytics.ts +2 -2
- package/src/usage/README.md +3 -0
- package/src/usage/execution-types.ts +69 -69
- package/src/usage/types.ts +7 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
OrgUnitSchema,
|
|
4
4
|
OrgUnitTreeNodeSchema,
|
|
@@ -14,70 +14,79 @@ import {
|
|
|
14
14
|
OrgUnitMembershipSourceSchema,
|
|
15
15
|
OrgUnitPermissionsEntrySchema,
|
|
16
16
|
ListOrgUnitPermissionsResponseSchema,
|
|
17
|
-
} from
|
|
17
|
+
} from "../schemas.js";
|
|
18
18
|
|
|
19
|
-
const UUID_A =
|
|
20
|
-
const UUID_B =
|
|
21
|
-
const UUID_C =
|
|
19
|
+
const UUID_A = "11111111-1111-4111-8111-111111111111";
|
|
20
|
+
const UUID_B = "22222222-2222-4222-8222-222222222222";
|
|
21
|
+
const UUID_C = "33333333-3333-4333-8333-333333333333";
|
|
22
22
|
|
|
23
23
|
const makeUnit = (overrides: Record<string, unknown> = {}) => ({
|
|
24
24
|
id: UUID_A,
|
|
25
25
|
orgId: UUID_B,
|
|
26
26
|
parentId: UUID_C,
|
|
27
|
-
slug:
|
|
28
|
-
name:
|
|
27
|
+
slug: "engineering",
|
|
28
|
+
name: "Engineering",
|
|
29
29
|
description: null,
|
|
30
|
-
typeTag:
|
|
31
|
-
classification:
|
|
32
|
-
orderKey:
|
|
33
|
-
path: `${UUID_B.replace(/-/g,
|
|
34
|
-
syncMode:
|
|
30
|
+
typeTag: "department",
|
|
31
|
+
classification: "org_container",
|
|
32
|
+
orderKey: "a0",
|
|
33
|
+
path: `${UUID_B.replace(/-/g, "_")}.${UUID_A.replace(/-/g, "_")}`,
|
|
34
|
+
syncMode: "manual_only",
|
|
35
35
|
visibilityDefault: null,
|
|
36
36
|
metadata: {},
|
|
37
37
|
archivedAt: null,
|
|
38
|
-
createdAt:
|
|
39
|
-
updatedAt:
|
|
38
|
+
createdAt: "2026-04-17T00:00:00Z",
|
|
39
|
+
updatedAt: "2026-04-17T00:00:00Z",
|
|
40
40
|
...overrides,
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
describe(
|
|
44
|
-
it(
|
|
43
|
+
describe("OrgUnitSchema", () => {
|
|
44
|
+
it("accepts a complete unit", () => {
|
|
45
45
|
const parsed = OrgUnitSchema.parse(makeUnit());
|
|
46
46
|
expect(parsed.id).toBe(UUID_A);
|
|
47
|
-
expect(parsed.typeTag).toBe(
|
|
47
|
+
expect(parsed.typeTag).toBe("department");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it(
|
|
51
|
-
const root = makeUnit({
|
|
50
|
+
it("accepts a root unit with null parentId", () => {
|
|
51
|
+
const root = makeUnit({
|
|
52
|
+
parentId: null,
|
|
53
|
+
slug: "company",
|
|
54
|
+
name: "Company",
|
|
55
|
+
typeTag: "company",
|
|
56
|
+
});
|
|
52
57
|
expect(() => OrgUnitSchema.parse(root)).not.toThrow();
|
|
53
58
|
});
|
|
54
59
|
|
|
55
|
-
it(
|
|
56
|
-
const archived = makeUnit({ archivedAt:
|
|
60
|
+
it("accepts archived unit (archivedAt non-null)", () => {
|
|
61
|
+
const archived = makeUnit({ archivedAt: "2026-04-17T12:00:00Z" });
|
|
57
62
|
expect(() => OrgUnitSchema.parse(archived)).not.toThrow();
|
|
58
63
|
});
|
|
59
64
|
|
|
60
|
-
it(
|
|
61
|
-
expect(() => OrgUnitSchema.parse(makeUnit({ id:
|
|
65
|
+
it("rejects invalid UUID for id", () => {
|
|
66
|
+
expect(() => OrgUnitSchema.parse(makeUnit({ id: "not-a-uuid" }))).toThrow();
|
|
62
67
|
});
|
|
63
68
|
|
|
64
|
-
it(
|
|
65
|
-
expect(() =>
|
|
69
|
+
it("rejects invalid classification", () => {
|
|
70
|
+
expect(() =>
|
|
71
|
+
OrgUnitSchema.parse(makeUnit({ classification: "bogus" })),
|
|
72
|
+
).toThrow();
|
|
66
73
|
});
|
|
67
74
|
|
|
68
|
-
it(
|
|
75
|
+
it("rejects missing required fields", () => {
|
|
69
76
|
const { name: _name, ...missingName } = makeUnit();
|
|
70
77
|
expect(() => OrgUnitSchema.parse(missingName)).toThrow();
|
|
71
78
|
});
|
|
72
79
|
|
|
73
|
-
it(
|
|
74
|
-
const parsed = OrgUnitSchema.parse(
|
|
80
|
+
it("round-trips unknown metadata fields verbatim", () => {
|
|
81
|
+
const parsed = OrgUnitSchema.parse(
|
|
82
|
+
makeUnit({ metadata: { legacy_team_id: UUID_C } }),
|
|
83
|
+
);
|
|
75
84
|
expect(parsed.metadata).toEqual({ legacy_team_id: UUID_C });
|
|
76
85
|
});
|
|
77
86
|
});
|
|
78
87
|
|
|
79
|
-
describe(
|
|
80
|
-
it(
|
|
88
|
+
describe("OrgUnitTreeNodeSchema", () => {
|
|
89
|
+
it("requires depth 1..5", () => {
|
|
81
90
|
const base = {
|
|
82
91
|
...makeUnit(),
|
|
83
92
|
depth: 3,
|
|
@@ -91,175 +100,201 @@ describe('OrgUnitTreeNodeSchema', () => {
|
|
|
91
100
|
});
|
|
92
101
|
});
|
|
93
102
|
|
|
94
|
-
describe(
|
|
95
|
-
it(
|
|
103
|
+
describe("OrgUnitMembershipSchema", () => {
|
|
104
|
+
it("enforces role + status + source enums", () => {
|
|
96
105
|
const membership = {
|
|
97
106
|
id: UUID_A,
|
|
98
107
|
orgId: UUID_B,
|
|
99
108
|
unitId: UUID_C,
|
|
100
109
|
userId: UUID_A,
|
|
101
|
-
membershipRole:
|
|
102
|
-
status:
|
|
103
|
-
source:
|
|
104
|
-
sourceRef:
|
|
105
|
-
createdAt:
|
|
106
|
-
updatedAt:
|
|
110
|
+
membershipRole: "l1_unit_owner",
|
|
111
|
+
status: "active",
|
|
112
|
+
source: "google_groups",
|
|
113
|
+
sourceRef: "eng@example.com",
|
|
114
|
+
createdAt: "2026-04-17T00:00:00Z",
|
|
115
|
+
updatedAt: "2026-04-17T00:00:00Z",
|
|
107
116
|
};
|
|
108
117
|
expect(() => OrgUnitMembershipSchema.parse(membership)).not.toThrow();
|
|
109
118
|
expect(() =>
|
|
110
|
-
OrgUnitMembershipSchema.parse({ ...membership, membershipRole:
|
|
119
|
+
OrgUnitMembershipSchema.parse({ ...membership, membershipRole: "root" }),
|
|
120
|
+
).toThrow();
|
|
121
|
+
expect(() =>
|
|
122
|
+
OrgUnitMembershipSchema.parse({ ...membership, status: "terminated" }),
|
|
111
123
|
).toThrow();
|
|
112
|
-
expect(() => OrgUnitMembershipSchema.parse({ ...membership, status: 'terminated' })).toThrow();
|
|
113
124
|
});
|
|
114
125
|
});
|
|
115
126
|
|
|
116
|
-
describe(
|
|
117
|
-
it(
|
|
127
|
+
describe("OrgUnitRelationshipSchema", () => {
|
|
128
|
+
it("accepts graph edge with nullable contextKey", () => {
|
|
118
129
|
const edge = {
|
|
119
130
|
id: UUID_A,
|
|
120
131
|
orgId: UUID_B,
|
|
121
132
|
fromUnit: UUID_C,
|
|
122
133
|
toUnit: UUID_A,
|
|
123
|
-
type:
|
|
124
|
-
role:
|
|
134
|
+
type: "collaborates_with" as const,
|
|
135
|
+
role: "participant" as const,
|
|
125
136
|
contextKey: null,
|
|
126
137
|
metadata: {},
|
|
127
|
-
createdAt:
|
|
138
|
+
createdAt: "2026-04-17T00:00:00Z",
|
|
128
139
|
};
|
|
129
140
|
expect(() => OrgUnitRelationshipSchema.parse(edge)).not.toThrow();
|
|
130
141
|
expect(() =>
|
|
131
|
-
OrgUnitRelationshipSchema.parse({ ...edge, contextKey:
|
|
142
|
+
OrgUnitRelationshipSchema.parse({ ...edge, contextKey: "infra-context" }),
|
|
132
143
|
).not.toThrow();
|
|
133
144
|
});
|
|
134
145
|
});
|
|
135
146
|
|
|
136
|
-
describe(
|
|
137
|
-
it(
|
|
147
|
+
describe("OrgLevelConfigSchema", () => {
|
|
148
|
+
it("enforces depth 1..5, non-empty-when-present label/labelPlural, and icon enum", () => {
|
|
138
149
|
const entry = {
|
|
139
150
|
orgId: UUID_B,
|
|
140
151
|
depth: 2,
|
|
141
|
-
label:
|
|
142
|
-
labelPlural:
|
|
143
|
-
icon:
|
|
144
|
-
createdAt:
|
|
145
|
-
updatedAt:
|
|
152
|
+
label: "Department",
|
|
153
|
+
labelPlural: "Departments",
|
|
154
|
+
icon: "users-four" as const,
|
|
155
|
+
createdAt: "2026-04-17T00:00:00Z",
|
|
156
|
+
updatedAt: "2026-04-17T00:00:00Z",
|
|
146
157
|
};
|
|
147
158
|
expect(() => OrgLevelConfigSchema.parse(entry)).not.toThrow();
|
|
148
159
|
expect(() =>
|
|
149
|
-
OrgLevelConfigSchema.parse({
|
|
160
|
+
OrgLevelConfigSchema.parse({
|
|
161
|
+
...entry,
|
|
162
|
+
label: null,
|
|
163
|
+
labelPlural: null,
|
|
164
|
+
icon: null,
|
|
165
|
+
}),
|
|
150
166
|
).not.toThrow();
|
|
151
167
|
expect(() => OrgLevelConfigSchema.parse({ ...entry, depth: 6 })).toThrow();
|
|
152
|
-
expect(() => OrgLevelConfigSchema.parse({ ...entry, label:
|
|
153
|
-
expect(() =>
|
|
168
|
+
expect(() => OrgLevelConfigSchema.parse({ ...entry, label: "" })).toThrow();
|
|
169
|
+
expect(() =>
|
|
170
|
+
OrgLevelConfigSchema.parse({ ...entry, labelPlural: "" }),
|
|
171
|
+
).toThrow();
|
|
154
172
|
expect(() =>
|
|
155
|
-
OrgLevelConfigSchema.parse({ ...entry, icon:
|
|
173
|
+
OrgLevelConfigSchema.parse({ ...entry, icon: "not-an-icon" }),
|
|
156
174
|
).toThrow();
|
|
157
175
|
});
|
|
158
176
|
});
|
|
159
177
|
|
|
160
|
-
describe(
|
|
161
|
-
it(
|
|
178
|
+
describe("Response schemas", () => {
|
|
179
|
+
it("OrgUnitTreeResponseSchema accepts empty tree", () => {
|
|
162
180
|
expect(() =>
|
|
163
181
|
OrgUnitTreeResponseSchema.parse({
|
|
164
182
|
nodes: [],
|
|
165
183
|
levelConfig: [],
|
|
166
|
-
})
|
|
184
|
+
}),
|
|
167
185
|
).not.toThrow();
|
|
168
186
|
});
|
|
169
187
|
|
|
170
|
-
it(
|
|
188
|
+
it("OrgUnitChildrenResponseSchema wraps parentId + children array", () => {
|
|
171
189
|
expect(() =>
|
|
172
|
-
OrgUnitChildrenResponseSchema.parse({ parentId: UUID_A, children: [] })
|
|
190
|
+
OrgUnitChildrenResponseSchema.parse({ parentId: UUID_A, children: [] }),
|
|
173
191
|
).not.toThrow();
|
|
174
192
|
});
|
|
175
193
|
|
|
176
|
-
it(
|
|
194
|
+
it("OrgUnitRelationshipsResponseSchema has incoming + outgoing arrays", () => {
|
|
177
195
|
expect(() =>
|
|
178
196
|
OrgUnitRelationshipsResponseSchema.parse({
|
|
179
197
|
unitId: UUID_A,
|
|
180
198
|
incoming: [],
|
|
181
199
|
outgoing: [],
|
|
182
|
-
})
|
|
200
|
+
}),
|
|
183
201
|
).not.toThrow();
|
|
184
202
|
});
|
|
185
203
|
});
|
|
186
204
|
|
|
187
|
-
describe(
|
|
188
|
-
it(
|
|
205
|
+
describe("Enum exhaustiveness", () => {
|
|
206
|
+
it("OrgUnitErrorCodeSchema enumerates all 10 known codes", () => {
|
|
189
207
|
const codes = [
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
"CYCLE_BLOCKED",
|
|
209
|
+
"DEPTH_EXCEEDED",
|
|
210
|
+
"TARGET_ARCHIVED",
|
|
211
|
+
"ROOT_HAS_NO_PARENT",
|
|
212
|
+
"PARENT_ARCHIVED",
|
|
213
|
+
"SLUG_TAKEN",
|
|
214
|
+
"CROSS_ORG_REPARENT",
|
|
215
|
+
"ORDERKEY_CONFLICT",
|
|
216
|
+
"SIBLING_NOT_FOUND",
|
|
217
|
+
"SIBLINGS_DIFFERENT_PARENT",
|
|
200
218
|
];
|
|
201
219
|
for (const code of codes) {
|
|
202
220
|
expect(() => OrgUnitErrorCodeSchema.parse(code)).not.toThrow();
|
|
203
221
|
}
|
|
204
|
-
expect(() => OrgUnitErrorCodeSchema.parse(
|
|
222
|
+
expect(() => OrgUnitErrorCodeSchema.parse("OTHER")).toThrow();
|
|
205
223
|
});
|
|
206
224
|
|
|
207
|
-
it(
|
|
208
|
-
for (const v of [
|
|
225
|
+
it("classification enum matches backend _enums.ts", () => {
|
|
226
|
+
for (const v of ["execution_unit", "org_container", "custom"]) {
|
|
209
227
|
expect(() => OrgUnitClassificationSchema.parse(v)).not.toThrow();
|
|
210
228
|
}
|
|
211
229
|
});
|
|
212
230
|
|
|
213
|
-
it(
|
|
214
|
-
for (const v of [
|
|
231
|
+
it("relationship type enum matches backend", () => {
|
|
232
|
+
for (const v of [
|
|
233
|
+
"collaborates_with",
|
|
234
|
+
"reports_to",
|
|
235
|
+
"depends_on",
|
|
236
|
+
"custom",
|
|
237
|
+
]) {
|
|
215
238
|
expect(() => OrgUnitRelationshipTypeSchema.parse(v)).not.toThrow();
|
|
216
239
|
}
|
|
217
240
|
});
|
|
218
241
|
|
|
219
|
-
it(
|
|
220
|
-
for (const v of [
|
|
242
|
+
it("membership source enum matches backend", () => {
|
|
243
|
+
for (const v of ["manual", "google_groups", "scim", "hris"]) {
|
|
221
244
|
expect(() => OrgUnitMembershipSourceSchema.parse(v)).not.toThrow();
|
|
222
245
|
}
|
|
223
246
|
});
|
|
224
247
|
});
|
|
225
248
|
|
|
226
|
-
describe(
|
|
249
|
+
describe("OrgUnitPermissionsEntrySchema", () => {
|
|
227
250
|
const entry = {
|
|
228
251
|
userId: UUID_A,
|
|
229
|
-
membershipRole:
|
|
252
|
+
membershipRole: "l1_unit_owner" as const,
|
|
230
253
|
inheritedFromUnitId: UUID_C,
|
|
231
|
-
inheritedFromUnitName:
|
|
254
|
+
inheritedFromUnitName: "Engineering",
|
|
232
255
|
};
|
|
233
256
|
|
|
234
|
-
it(
|
|
257
|
+
it("accepts a well-formed entry", () => {
|
|
235
258
|
expect(() => OrgUnitPermissionsEntrySchema.parse(entry)).not.toThrow();
|
|
236
259
|
});
|
|
237
260
|
|
|
238
|
-
it(
|
|
261
|
+
it("rejects invalid membershipRole", () => {
|
|
239
262
|
expect(() =>
|
|
240
|
-
OrgUnitPermissionsEntrySchema.parse({
|
|
263
|
+
OrgUnitPermissionsEntrySchema.parse({
|
|
264
|
+
...entry,
|
|
265
|
+
membershipRole: "member",
|
|
266
|
+
}),
|
|
241
267
|
).not.toThrow();
|
|
242
268
|
expect(() =>
|
|
243
|
-
OrgUnitPermissionsEntrySchema.parse({
|
|
269
|
+
OrgUnitPermissionsEntrySchema.parse({
|
|
270
|
+
...entry,
|
|
271
|
+
membershipRole: "bogus",
|
|
272
|
+
}),
|
|
244
273
|
).toThrow();
|
|
245
274
|
});
|
|
246
275
|
|
|
247
|
-
it(
|
|
276
|
+
it("requires UUIDs for user and inherited unit", () => {
|
|
248
277
|
expect(() =>
|
|
249
|
-
OrgUnitPermissionsEntrySchema.parse({ ...entry, userId:
|
|
278
|
+
OrgUnitPermissionsEntrySchema.parse({ ...entry, userId: "nope" }),
|
|
250
279
|
).toThrow();
|
|
251
280
|
expect(() =>
|
|
252
|
-
OrgUnitPermissionsEntrySchema.parse({
|
|
281
|
+
OrgUnitPermissionsEntrySchema.parse({
|
|
282
|
+
...entry,
|
|
283
|
+
inheritedFromUnitId: "nope",
|
|
284
|
+
}),
|
|
253
285
|
).toThrow();
|
|
254
286
|
});
|
|
255
287
|
|
|
256
|
-
it(
|
|
288
|
+
it("rejects empty inheritedFromUnitName", () => {
|
|
257
289
|
expect(() =>
|
|
258
|
-
OrgUnitPermissionsEntrySchema.parse({
|
|
290
|
+
OrgUnitPermissionsEntrySchema.parse({
|
|
291
|
+
...entry,
|
|
292
|
+
inheritedFromUnitName: "",
|
|
293
|
+
}),
|
|
259
294
|
).toThrow();
|
|
260
295
|
});
|
|
261
296
|
|
|
262
|
-
it(
|
|
297
|
+
it("list response wraps entries with unitId", () => {
|
|
263
298
|
const parsed = ListOrgUnitPermissionsResponseSchema.parse({
|
|
264
299
|
unitId: UUID_B,
|
|
265
300
|
entries: [entry],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
2
|
-
import { orderTreeNodes, type TreeOrderableNode } from
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { orderTreeNodes, type TreeOrderableNode } from "../tree-ordering.js";
|
|
3
3
|
|
|
4
4
|
interface TestNode extends TreeOrderableNode {
|
|
5
5
|
label?: string;
|
|
@@ -19,31 +19,42 @@ function shuffle<T>(input: readonly T[], seed: number): T[] {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const FIXTURE: TestNode[] = [
|
|
22
|
-
{ id:
|
|
23
|
-
{ id:
|
|
24
|
-
{ id:
|
|
25
|
-
{ id:
|
|
26
|
-
{ id:
|
|
27
|
-
{ id:
|
|
28
|
-
{ id:
|
|
29
|
-
{ id:
|
|
30
|
-
{ id:
|
|
31
|
-
{ id:
|
|
22
|
+
{ id: "root-1", parentId: null, orderKey: "10" },
|
|
23
|
+
{ id: "root-2", parentId: null, orderKey: "20" },
|
|
24
|
+
{ id: "a", parentId: "root-1", orderKey: "20" },
|
|
25
|
+
{ id: "b", parentId: "root-1", orderKey: "10" },
|
|
26
|
+
{ id: "c", parentId: "root-1", orderKey: "30" },
|
|
27
|
+
{ id: "aa", parentId: "a", orderKey: "10" },
|
|
28
|
+
{ id: "ab", parentId: "a", orderKey: "20" },
|
|
29
|
+
{ id: "ba", parentId: "b", orderKey: "10" },
|
|
30
|
+
{ id: "x", parentId: "root-2", orderKey: "10" },
|
|
31
|
+
{ id: "y", parentId: "root-2", orderKey: "10" },
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
-
describe(
|
|
35
|
-
it(
|
|
34
|
+
describe("orderTreeNodes", () => {
|
|
35
|
+
it("emits a parent-before-children flat order sorted per parent", () => {
|
|
36
36
|
const ordered = orderTreeNodes(FIXTURE).map((n) => n.id);
|
|
37
|
-
expect(ordered).toEqual([
|
|
37
|
+
expect(ordered).toEqual([
|
|
38
|
+
"root-1",
|
|
39
|
+
"b",
|
|
40
|
+
"ba",
|
|
41
|
+
"a",
|
|
42
|
+
"aa",
|
|
43
|
+
"ab",
|
|
44
|
+
"c",
|
|
45
|
+
"root-2",
|
|
46
|
+
"x",
|
|
47
|
+
"y",
|
|
48
|
+
]);
|
|
38
49
|
});
|
|
39
50
|
|
|
40
|
-
it(
|
|
51
|
+
it("is idempotent — running twice produces the same result", () => {
|
|
41
52
|
const once = orderTreeNodes(FIXTURE);
|
|
42
53
|
const twice = orderTreeNodes(once);
|
|
43
54
|
expect(twice).toEqual(once);
|
|
44
55
|
});
|
|
45
56
|
|
|
46
|
-
it(
|
|
57
|
+
it("is stable under input shuffle — order depends only on (orderKey, id), not insertion order", () => {
|
|
47
58
|
const baseline = orderTreeNodes(FIXTURE).map((n) => n.id);
|
|
48
59
|
for (const seed of [1, 2, 3, 42, 1337, 999999]) {
|
|
49
60
|
const shuffled = shuffle(FIXTURE, seed);
|
|
@@ -52,51 +63,60 @@ describe('orderTreeNodes', () => {
|
|
|
52
63
|
}
|
|
53
64
|
});
|
|
54
65
|
|
|
55
|
-
it(
|
|
66
|
+
it("uses id as deterministic tiebreaker when orderKey is identical", () => {
|
|
56
67
|
const ties: TestNode[] = [
|
|
57
|
-
{ id:
|
|
58
|
-
{ id:
|
|
59
|
-
{ id:
|
|
68
|
+
{ id: "zeta", parentId: null, orderKey: "50" },
|
|
69
|
+
{ id: "alpha", parentId: null, orderKey: "50" },
|
|
70
|
+
{ id: "mike", parentId: null, orderKey: "50" },
|
|
60
71
|
];
|
|
61
72
|
const order = orderTreeNodes(ties).map((n) => n.id);
|
|
62
|
-
expect(order).toEqual([
|
|
73
|
+
expect(order).toEqual(["alpha", "mike", "zeta"]);
|
|
63
74
|
|
|
64
75
|
const shuffled = orderTreeNodes(shuffle(ties, 7)).map((n) => n.id);
|
|
65
|
-
expect(shuffled).toEqual([
|
|
76
|
+
expect(shuffled).toEqual(["alpha", "mike", "zeta"]);
|
|
66
77
|
});
|
|
67
78
|
|
|
68
|
-
it(
|
|
79
|
+
it("places nodes with null parentId in the root group, before any descendants", () => {
|
|
69
80
|
const nodes: TestNode[] = [
|
|
70
|
-
{ id:
|
|
71
|
-
{ id:
|
|
72
|
-
{ id:
|
|
73
|
-
{ id:
|
|
81
|
+
{ id: "child-of-r2", parentId: "r2", orderKey: "10" },
|
|
82
|
+
{ id: "r1", parentId: null, orderKey: "20" },
|
|
83
|
+
{ id: "r2", parentId: null, orderKey: "10" },
|
|
84
|
+
{ id: "child-of-r1", parentId: "r1", orderKey: "10" },
|
|
74
85
|
];
|
|
75
86
|
const ordered = orderTreeNodes(nodes).map((n) => n.id);
|
|
76
|
-
expect(ordered).toEqual([
|
|
87
|
+
expect(ordered).toEqual(["r2", "child-of-r2", "r1", "child-of-r1"]);
|
|
77
88
|
|
|
78
|
-
const roots = orderTreeNodes(nodes)
|
|
79
|
-
|
|
89
|
+
const roots = orderTreeNodes(nodes)
|
|
90
|
+
.filter((n) => n.parentId === null)
|
|
91
|
+
.map((n) => n.id);
|
|
92
|
+
expect(roots).toEqual(["r2", "r1"]);
|
|
80
93
|
});
|
|
81
94
|
|
|
82
|
-
it(
|
|
95
|
+
it("preserves all input fields on the output nodes", () => {
|
|
83
96
|
const rich: TestNode[] = [
|
|
84
|
-
{ id:
|
|
97
|
+
{ id: "only", parentId: null, orderKey: "1", label: "hello" },
|
|
85
98
|
];
|
|
86
99
|
const [out] = orderTreeNodes(rich);
|
|
87
|
-
expect(out).toEqual({
|
|
100
|
+
expect(out).toEqual({
|
|
101
|
+
id: "only",
|
|
102
|
+
parentId: null,
|
|
103
|
+
orderKey: "1",
|
|
104
|
+
label: "hello",
|
|
105
|
+
});
|
|
88
106
|
});
|
|
89
107
|
|
|
90
|
-
it(
|
|
108
|
+
it("returns a new array without mutating the input list", () => {
|
|
91
109
|
const input = FIXTURE.slice();
|
|
92
110
|
const snapshotIds = input.map((n) => n.id);
|
|
93
111
|
orderTreeNodes(input);
|
|
94
112
|
expect(input.map((n) => n.id)).toEqual(snapshotIds);
|
|
95
113
|
});
|
|
96
114
|
|
|
97
|
-
it(
|
|
115
|
+
it("is strictly synchronous (no Promise return)", () => {
|
|
98
116
|
const result = orderTreeNodes(FIXTURE);
|
|
99
117
|
expect(Array.isArray(result)).toBe(true);
|
|
100
|
-
expect(typeof (result as unknown as { then?: unknown }).then).toBe(
|
|
118
|
+
expect(typeof (result as unknown as { then?: unknown }).then).toBe(
|
|
119
|
+
"undefined",
|
|
120
|
+
);
|
|
101
121
|
});
|
|
102
122
|
});
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
2
|
-
import { VIEW_SCOPE_MAP, getViewScope } from
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { VIEW_SCOPE_MAP, getViewScope } from "../view-scopes.js";
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
4
|
+
describe("VIEW_SCOPE_MAP golden snapshot", () => {
|
|
5
|
+
it("exact values are frozen", () => {
|
|
6
6
|
expect(VIEW_SCOPE_MAP).toStrictEqual({
|
|
7
|
-
workspace:
|
|
8
|
-
timeline:
|
|
9
|
-
teamwork:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
teams:
|
|
13
|
-
|
|
7
|
+
workspace: "org.view_workspace",
|
|
8
|
+
timeline: "org.view_timeline",
|
|
9
|
+
teamwork: "org.view_teamwork",
|
|
10
|
+
"teamwork-member": "org.view_teamwork",
|
|
11
|
+
"company-md": "org.view_company_md",
|
|
12
|
+
teams: "org.view_teams",
|
|
13
|
+
"internal-admin": "internal.view_admin",
|
|
14
14
|
chat: null,
|
|
15
15
|
settings: null,
|
|
16
16
|
chats: null,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
"my-work": null,
|
|
18
|
+
"user-md": null,
|
|
19
19
|
upgrade: null,
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
})
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
23
|
|
|
24
|
-
describe(
|
|
25
|
-
it(
|
|
26
|
-
expect(getViewScope(
|
|
27
|
-
expect(getViewScope(
|
|
28
|
-
expect(getViewScope(
|
|
29
|
-
expect(getViewScope(
|
|
30
|
-
expect(getViewScope(
|
|
31
|
-
})
|
|
24
|
+
describe("getViewScope", () => {
|
|
25
|
+
it("returns the required scope for protected views", () => {
|
|
26
|
+
expect(getViewScope("workspace")).toBe("org.view_workspace");
|
|
27
|
+
expect(getViewScope("timeline")).toBe("org.view_timeline");
|
|
28
|
+
expect(getViewScope("teamwork")).toBe("org.view_teamwork");
|
|
29
|
+
expect(getViewScope("company-md")).toBe("org.view_company_md");
|
|
30
|
+
expect(getViewScope("internal-admin")).toBe("internal.view_admin");
|
|
31
|
+
});
|
|
32
32
|
|
|
33
|
-
it(
|
|
34
|
-
expect(getViewScope(
|
|
35
|
-
expect(getViewScope(
|
|
36
|
-
expect(getViewScope(
|
|
37
|
-
expect(getViewScope(
|
|
38
|
-
})
|
|
33
|
+
it("returns null for public views", () => {
|
|
34
|
+
expect(getViewScope("chat")).toBeNull();
|
|
35
|
+
expect(getViewScope("settings")).toBeNull();
|
|
36
|
+
expect(getViewScope("chats")).toBeNull();
|
|
37
|
+
expect(getViewScope("upgrade")).toBeNull();
|
|
38
|
+
});
|
|
39
39
|
|
|
40
|
-
it(
|
|
41
|
-
expect(getViewScope(
|
|
42
|
-
})
|
|
40
|
+
it("returns null for unknown view string", () => {
|
|
41
|
+
expect(getViewScope("nonexistent-view")).toBeNull();
|
|
42
|
+
});
|
|
43
43
|
|
|
44
|
-
it(
|
|
45
|
-
expect(getViewScope(
|
|
46
|
-
})
|
|
44
|
+
it("returns null for empty string", () => {
|
|
45
|
+
expect(getViewScope("")).toBeNull();
|
|
46
|
+
});
|
|
47
47
|
|
|
48
|
-
it(
|
|
49
|
-
expect(getViewScope(
|
|
50
|
-
})
|
|
51
|
-
})
|
|
48
|
+
it("returns null for view name that is a substring of a real view", () => {
|
|
49
|
+
expect(getViewScope("work")).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|