@nebutra/contracts 0.1.2 → 2.0.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.
@@ -0,0 +1,61 @@
1
+ import {
2
+ DEFAULT_RESUME_MARGINS,
3
+ ExperienceKindSchema,
4
+ RESUME_CONTENT_VERSION,
5
+ RESUME_SKILL_CATEGORIES,
6
+ ResumeAchievementSchema,
7
+ ResumeBasicSchema,
8
+ ResumeCertificateSchema,
9
+ ResumeContentV1Schema,
10
+ ResumeDerivedSchema,
11
+ ResumeEducationSchema,
12
+ ResumeExperienceSchema,
13
+ ResumeFundingSchema,
14
+ ResumeLangSchema,
15
+ ResumeLinksSchema,
16
+ ResumeMarginsSchema,
17
+ ResumeObjectiveSchema,
18
+ ResumeOpenSourceSchema,
19
+ ResumePatentSchema,
20
+ ResumePeriodSchema,
21
+ ResumePreferencesSchema,
22
+ ResumePressSchema,
23
+ ResumeProjectSchema,
24
+ ResumePublicationSchema,
25
+ ResumeSkillsSchema,
26
+ ResumeVentureSchema,
27
+ ResumeVolunteeringSchema,
28
+ ResumeWriteSchema,
29
+ VentureStageSchema
30
+ } from "./chunk-PU65L2HQ.js";
31
+ export {
32
+ DEFAULT_RESUME_MARGINS,
33
+ ExperienceKindSchema,
34
+ RESUME_CONTENT_VERSION,
35
+ RESUME_SKILL_CATEGORIES,
36
+ ResumeAchievementSchema,
37
+ ResumeBasicSchema,
38
+ ResumeCertificateSchema,
39
+ ResumeContentV1Schema,
40
+ ResumeDerivedSchema,
41
+ ResumeEducationSchema,
42
+ ResumeExperienceSchema,
43
+ ResumeFundingSchema,
44
+ ResumeLangSchema,
45
+ ResumeLinksSchema,
46
+ ResumeMarginsSchema,
47
+ ResumeObjectiveSchema,
48
+ ResumeOpenSourceSchema,
49
+ ResumePatentSchema,
50
+ ResumePeriodSchema,
51
+ ResumePreferencesSchema,
52
+ ResumePressSchema,
53
+ ResumeProjectSchema,
54
+ ResumePublicationSchema,
55
+ ResumeSkillsSchema,
56
+ ResumeVentureSchema,
57
+ ResumeVolunteeringSchema,
58
+ ResumeWriteSchema,
59
+ VentureStageSchema
60
+ };
61
+ //# sourceMappingURL=sleptons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@nebutra/contracts",
3
- "version": "0.1.2",
3
+ "version": "2.0.0",
4
4
  "description": "Canonical API/event/domain contracts for Nebutra superstarter",
5
5
  "private": false,
6
+ "nebutra": {
7
+ "status": "foundation",
8
+ "graph": "core",
9
+ "productionReady": false
10
+ },
6
11
  "license": "MIT",
7
12
  "type": "module",
8
13
  "main": "./dist/index.js",
@@ -27,6 +32,14 @@
27
32
  "./notifications": {
28
33
  "import": "./dist/notifications.js",
29
34
  "types": "./dist/notifications.d.ts"
35
+ },
36
+ "./admin": {
37
+ "import": "./dist/admin.js",
38
+ "types": "./dist/admin.d.ts"
39
+ },
40
+ "./sleptons": {
41
+ "import": "./dist/sleptons.js",
42
+ "types": "./dist/sleptons.d.ts"
30
43
  }
31
44
  },
32
45
  "dependencies": {
@@ -35,7 +48,8 @@
35
48
  "devDependencies": {
36
49
  "@types/node": "^22.19.15",
37
50
  "tsup": "^8.5.1",
38
- "typescript": "^5.9.3"
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.4"
39
53
  },
40
54
  "homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/commerce/contracts#readme",
41
55
  "repository": {
@@ -52,6 +66,7 @@
52
66
  "scripts": {
53
67
  "build": "tsup",
54
68
  "typecheck": "tsc --noEmit",
55
- "clean": "rm -rf dist .turbo"
69
+ "clean": "rm -rf dist .turbo",
70
+ "test": "vitest run"
56
71
  }
57
72
  }
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ ActionRequestSchema,
4
+ AdminManifestSchema,
5
+ assertApplyAllowed,
6
+ resolveContractUrl,
7
+ roleAtLeast,
8
+ SignalReadingSchema,
9
+ } from "../admin";
10
+
11
+ const manifest = {
12
+ contract: "nebutra.admin/v1",
13
+ product: "router",
14
+ label: "Nebutra Router",
15
+ version: "0.1.1",
16
+ origin: "https://router.example.com",
17
+ graph: "core",
18
+ status: "wip",
19
+ health: "/api/health",
20
+ domains: [
21
+ {
22
+ id: "supply",
23
+ label: "Supply",
24
+ resources: [
25
+ {
26
+ id: "account",
27
+ label: "Accounts",
28
+ list: "/api/admin/v1/supply/accounts",
29
+ columns: [{ key: "provider", label: "Provider" }],
30
+ actions: ["account.relogin"],
31
+ },
32
+ ],
33
+ actions: [
34
+ {
35
+ id: "account.relogin",
36
+ verb: "Re-login",
37
+ resource: "account",
38
+ role: "platform_operator",
39
+ url: "/api/admin/v1/supply/actions/account.relogin",
40
+ },
41
+ {
42
+ id: "channel.sync",
43
+ verb: "Sync channel",
44
+ role: "platform_operator",
45
+ url: "/api/admin/v1/supply/actions/channel.sync",
46
+ },
47
+ ],
48
+ signals: [
49
+ {
50
+ id: "channel.drift",
51
+ label: "Channel out of sync",
52
+ severity: "warn",
53
+ probe: "/api/admin/v1/supply/signals/channel.drift",
54
+ action: "channel.sync",
55
+ },
56
+ ],
57
+ policies: [
58
+ {
59
+ id: "channel.autosync",
60
+ label: "Auto-sync",
61
+ on: "nebutra/supply.account.changed",
62
+ runs: "channel.sync",
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ };
68
+
69
+ describe("admin contract", () => {
70
+ it("accepts a well-formed manifest and applies defaults", () => {
71
+ const parsed = AdminManifestSchema.parse(manifest);
72
+ expect(parsed.domains[0]?.actions[0]?.plan).toBe(true);
73
+ expect(parsed.domains[0]?.resources[0]?.key).toBe("id");
74
+ });
75
+
76
+ it("rejects dangling references", () => {
77
+ const bad = structuredClone(manifest) as typeof manifest;
78
+ const signal = bad.domains[0]?.signals[0];
79
+ if (!signal) throw new Error("fixture");
80
+ signal.action = "does.not.exist";
81
+ const result = AdminManifestSchema.safeParse(bad);
82
+ expect(result.success).toBe(false);
83
+ expect(JSON.stringify(result.error?.issues)).toContain("unknown action does.not.exist");
84
+ });
85
+
86
+ it("requires a plan before applying a planned action", () => {
87
+ const action = AdminManifestSchema.parse(manifest).domains[0]?.actions[1];
88
+ if (!action) throw new Error("fixture");
89
+ expect(
90
+ assertApplyAllowed(action, ActionRequestSchema.parse({ mode: "apply" }))?.error.code,
91
+ ).toBe("plan_required");
92
+ expect(
93
+ assertApplyAllowed(action, ActionRequestSchema.parse({ mode: "apply", planId: "p1" })),
94
+ ).toBeNull();
95
+ expect(assertApplyAllowed(action, ActionRequestSchema.parse({ mode: "plan" }))).toBeNull();
96
+ });
97
+
98
+ it("signal readings must carry probedAt", () => {
99
+ expect(
100
+ SignalReadingSchema.safeParse({ id: "channel.drift", status: "raised", severity: "warn" })
101
+ .success,
102
+ ).toBe(false);
103
+ expect(
104
+ SignalReadingSchema.safeParse({
105
+ id: "channel.drift",
106
+ status: "ok",
107
+ severity: "warn",
108
+ probedAt: new Date().toISOString(),
109
+ }).success,
110
+ ).toBe(true);
111
+ });
112
+
113
+ it("role ladder and url resolution", () => {
114
+ expect(roleAtLeast("platform_owner", "platform_operator")).toBe(true);
115
+ expect(roleAtLeast("platform_readonly", "platform_support")).toBe(false);
116
+ expect(resolveContractUrl(manifest, "/api/health")).toBe(
117
+ "https://router.example.com/api/health",
118
+ );
119
+ });
120
+ });
package/src/admin.ts ADDED
@@ -0,0 +1,297 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Admin Contract — `nebutra.admin/v1`.
5
+ *
6
+ * Every Nebutra SaaS exposes one manifest describing the admin surface it
7
+ * owns: resources (nouns), actions (audited verbs, plan → apply), signals
8
+ * (probed facts) and policies (actions the system runs by itself). The
9
+ * platform admin renders any product from its manifest; an agent derives
10
+ * tools from it. Nothing product-specific lives in the renderer.
11
+ *
12
+ * See docs/plans/2026-09-08-admin-of-admins-model.md.
13
+ */
14
+
15
+ export const ADMIN_CONTRACT_VERSION = "nebutra.admin/v1" as const;
16
+
17
+ /** One status vocabulary. `unknown` is first-class and never renders green. */
18
+ export const AdminStatusSchema = z.enum(["healthy", "degraded", "down", "unknown"]);
19
+ export type AdminStatus = z.infer<typeof AdminStatusSchema>;
20
+
21
+ /** One role ladder; product roles map onto it, staff grants mean the same everywhere. */
22
+ export const StaffRoleSchema = z.enum([
23
+ "platform_readonly",
24
+ "platform_support",
25
+ "platform_operator",
26
+ "platform_owner",
27
+ ]);
28
+ export type StaffRole = z.infer<typeof StaffRoleSchema>;
29
+ export const STAFF_ROLE_RANK: Record<StaffRole, number> = {
30
+ platform_readonly: 0,
31
+ platform_support: 1,
32
+ platform_operator: 2,
33
+ platform_owner: 3,
34
+ };
35
+ export function roleAtLeast(role: StaffRole, required: StaffRole): boolean {
36
+ return STAFF_ROLE_RANK[role] >= STAFF_ROLE_RANK[required];
37
+ }
38
+
39
+ export const AdminSeveritySchema = z.enum(["info", "warn", "critical"]);
40
+ export type AdminSeverity = z.infer<typeof AdminSeveritySchema>;
41
+
42
+ /** The canonical eleven. A product declares the subset it has. */
43
+ export const AdminDomainIdSchema = z.enum([
44
+ "identity",
45
+ "access",
46
+ "commerce",
47
+ "usage",
48
+ "catalog",
49
+ "operations",
50
+ "integrations",
51
+ "trust",
52
+ "support",
53
+ "growth",
54
+ "settings",
55
+ // Platform-only cross-cutting domain: shared AI supply (router).
56
+ "supply",
57
+ ]);
58
+ export type AdminDomainId = z.infer<typeof AdminDomainIdSchema>;
59
+
60
+ const IdSchema = z
61
+ .string()
62
+ .min(1)
63
+ .max(64)
64
+ .regex(/^[a-z][a-z0-9._-]*$/, "ids are lowercase dotted identifiers");
65
+ const UrlPathSchema = z.string().min(1).regex(/^\//, "contract urls are origin-relative paths");
66
+
67
+ export const AdminColumnSchema = z.object({
68
+ key: z.string().min(1),
69
+ label: z.string().min(1),
70
+ kind: z.enum(["text", "mono", "status", "badge", "number", "time", "link"]).default("text"),
71
+ /** Right-align numbers; the renderer does not guess from values. */
72
+ align: z.enum(["start", "end"]).default("start"),
73
+ });
74
+ export type AdminColumn = z.infer<typeof AdminColumnSchema>;
75
+
76
+ export const AdminResourceSchema = z.object({
77
+ id: IdSchema,
78
+ label: z.string().min(1),
79
+ /** `GET list` → ResourceList */
80
+ list: UrlPathSchema,
81
+ /** `GET detail` with `{id}` placeholder → one item */
82
+ detail: UrlPathSchema.optional(),
83
+ columns: z.array(AdminColumnSchema).min(1),
84
+ /** Column key that identifies a row. */
85
+ key: z.string().min(1).default("id"),
86
+ search: z.boolean().default(false),
87
+ /** Action ids applicable per row. */
88
+ actions: z.array(IdSchema).default([]),
89
+ });
90
+ export type AdminResource = z.infer<typeof AdminResourceSchema>;
91
+
92
+ export const AdminActionSchema = z.object({
93
+ id: IdSchema,
94
+ verb: z.string().min(1),
95
+ /** Resource the action belongs to; omitted = domain-level action. */
96
+ resource: IdSchema.optional(),
97
+ /** Minimum staff role. Read-only tier never sees write controls. */
98
+ role: StaffRoleSchema,
99
+ /** `POST url` with ActionRequest (`mode: plan | apply`). */
100
+ url: UrlPathSchema,
101
+ /** Plan step is mandatory; `false` only for idempotent, non-destructive refreshes. */
102
+ plan: z.boolean().default(true),
103
+ destructive: z.boolean().default(false),
104
+ /** JSON Schema for `input`; doubles as the MCP tool inputSchema. */
105
+ input: z.record(z.string(), z.unknown()).optional(),
106
+ description: z.string().optional(),
107
+ });
108
+ export type AdminAction = z.infer<typeof AdminActionSchema>;
109
+
110
+ export const AdminSignalSchema = z.object({
111
+ id: IdSchema,
112
+ label: z.string().min(1),
113
+ severity: AdminSeveritySchema,
114
+ /** `GET probe` → SignalReading. Probed, never configured. */
115
+ probe: UrlPathSchema,
116
+ resource: IdSchema.optional(),
117
+ /** Action that resolves the signal when raised. */
118
+ action: IdSchema.optional(),
119
+ });
120
+ export type AdminSignal = z.infer<typeof AdminSignalSchema>;
121
+
122
+ export const AdminPolicySchema = z.object({
123
+ id: IdSchema,
124
+ label: z.string().min(1),
125
+ /** Event name (`nebutra/<domain>.<thing>.<verb>`) that triggers the policy. */
126
+ on: z.string().min(1),
127
+ runs: IdSchema,
128
+ enabled: z.boolean().default(true),
129
+ /** Natural-language intent the policy was compiled from, when it was. */
130
+ intent: z.string().optional(),
131
+ });
132
+ export type AdminPolicy = z.infer<typeof AdminPolicySchema>;
133
+
134
+ export const AdminDomainSchema = z.object({
135
+ id: AdminDomainIdSchema,
136
+ label: z.string().min(1),
137
+ resources: z.array(AdminResourceSchema).default([]),
138
+ actions: z.array(AdminActionSchema).default([]),
139
+ signals: z.array(AdminSignalSchema).default([]),
140
+ policies: z.array(AdminPolicySchema).default([]),
141
+ /** Product-owned browser surface for what the generic renderer cannot express. */
142
+ slot: UrlPathSchema.optional(),
143
+ });
144
+ export type AdminDomain = z.infer<typeof AdminDomainSchema>;
145
+
146
+ export const AdminManifestSchema = z
147
+ .object({
148
+ contract: z.literal(ADMIN_CONTRACT_VERSION),
149
+ product: IdSchema,
150
+ label: z.string().min(1),
151
+ version: z.string().min(1),
152
+ /** Absolute origin every relative url resolves against. */
153
+ origin: z.string().url(),
154
+ graph: z.enum(["core", "runtime", "labs"]),
155
+ status: z.enum(["stable", "foundation", "wip", "experimental"]),
156
+ /** `GET health` → @nebutra/health HealthCheckResult. */
157
+ health: UrlPathSchema,
158
+ /** `GET audit?…` → audit events (nebutra.audit/v1). */
159
+ audit: UrlPathSchema.optional(),
160
+ domains: z.array(AdminDomainSchema).min(1),
161
+ })
162
+ .superRefine((m, ctx) => {
163
+ for (const d of m.domains) {
164
+ const actionIds = new Set(d.actions.map((a) => a.id));
165
+ const resourceIds = new Set(d.resources.map((r) => r.id));
166
+ for (const r of d.resources) {
167
+ for (const a of r.actions) {
168
+ if (!actionIds.has(a)) {
169
+ ctx.addIssue({
170
+ code: "custom",
171
+ message: `resource ${r.id} references unknown action ${a}`,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ for (const s of d.signals) {
177
+ if (s.action && !actionIds.has(s.action)) {
178
+ ctx.addIssue({
179
+ code: "custom",
180
+ message: `signal ${s.id} references unknown action ${s.action}`,
181
+ });
182
+ }
183
+ if (s.resource && !resourceIds.has(s.resource)) {
184
+ ctx.addIssue({
185
+ code: "custom",
186
+ message: `signal ${s.id} references unknown resource ${s.resource}`,
187
+ });
188
+ }
189
+ }
190
+ for (const p of d.policies) {
191
+ if (!actionIds.has(p.runs)) {
192
+ ctx.addIssue({ code: "custom", message: `policy ${p.id} runs unknown action ${p.runs}` });
193
+ }
194
+ }
195
+ }
196
+ });
197
+ export type AdminManifest = z.infer<typeof AdminManifestSchema>;
198
+
199
+ // -----------------------------------------------------------------------------
200
+ // Wire envelopes
201
+ // -----------------------------------------------------------------------------
202
+
203
+ export const ResourceListSchema = z.object({
204
+ items: z.array(z.record(z.string(), z.unknown())),
205
+ total: z.number().int().min(0),
206
+ /** When the list was produced from live probes; omitted for pure reads. */
207
+ probedAt: z.string().datetime().optional(),
208
+ });
209
+ export type ResourceList = z.infer<typeof ResourceListSchema>;
210
+
211
+ export const ActionRequestSchema = z.discriminatedUnion("mode", [
212
+ z.object({ mode: z.literal("plan"), input: z.record(z.string(), z.unknown()).default({}) }),
213
+ z.object({
214
+ mode: z.literal("apply"),
215
+ input: z.record(z.string(), z.unknown()).default({}),
216
+ /** Required when the action is destructive; ties the apply to a reviewed plan. */
217
+ planId: z.string().min(1).optional(),
218
+ }),
219
+ ]);
220
+ export type ActionRequest = z.infer<typeof ActionRequestSchema>;
221
+
222
+ export const PlanDiffEntrySchema = z.object({
223
+ op: z.enum(["add", "remove", "change"]),
224
+ path: z.string().min(1),
225
+ from: z.unknown().optional(),
226
+ to: z.unknown().optional(),
227
+ });
228
+ export const ActionPlanSchema = z.object({
229
+ planId: z.string().min(1),
230
+ summary: z.string().min(1),
231
+ diff: z.array(PlanDiffEntrySchema),
232
+ affected: z
233
+ .array(z.object({ resource: IdSchema, id: z.string(), label: z.string().optional() }))
234
+ .default([]),
235
+ warnings: z.array(z.string()).default([]),
236
+ /** Plans expire; an apply after this is refused. */
237
+ expiresAt: z.string().datetime(),
238
+ });
239
+ export type ActionPlan = z.infer<typeof ActionPlanSchema>;
240
+
241
+ export const ActionResultSchema = z.object({
242
+ /** Audit event id written by the product for this apply. */
243
+ auditId: z.string().min(1),
244
+ summary: z.string().min(1),
245
+ result: z.record(z.string(), z.unknown()).optional(),
246
+ });
247
+ export type ActionResult = z.infer<typeof ActionResultSchema>;
248
+
249
+ export const SignalReadingSchema = z.object({
250
+ id: IdSchema,
251
+ status: z.enum(["ok", "raised", "unknown"]),
252
+ severity: AdminSeveritySchema,
253
+ probedAt: z.string().datetime(),
254
+ title: z.string().optional(),
255
+ detail: z.string().optional(),
256
+ resource: IdSchema.optional(),
257
+ action: IdSchema.optional(),
258
+ data: z.record(z.string(), z.unknown()).optional(),
259
+ });
260
+ export type SignalReading = z.infer<typeof SignalReadingSchema>;
261
+
262
+ /** Standard error body for contract endpoints. */
263
+ export const AdminErrorSchema = z.object({
264
+ error: z.object({
265
+ code: z.enum([
266
+ "unauthenticated",
267
+ "forbidden",
268
+ "not_found",
269
+ "invalid_input",
270
+ "plan_required",
271
+ "plan_expired",
272
+ "upstream_unavailable",
273
+ "internal",
274
+ ]),
275
+ message: z.string(),
276
+ }),
277
+ });
278
+ export type AdminError = z.infer<typeof AdminErrorSchema>;
279
+
280
+ /** Validate an apply against its action: destructive applies must reference a plan. */
281
+ export function assertApplyAllowed(action: AdminAction, request: ActionRequest): AdminError | null {
282
+ if (request.mode !== "apply") return null;
283
+ if ((action.destructive || action.plan) && !request.planId) {
284
+ return {
285
+ error: {
286
+ code: "plan_required",
287
+ message: `${action.id} requires a reviewed plan before apply`,
288
+ },
289
+ };
290
+ }
291
+ return null;
292
+ }
293
+
294
+ /** Resolve a contract path against the manifest origin. */
295
+ export function resolveContractUrl(manifest: Pick<AdminManifest, "origin">, path: string): string {
296
+ return new URL(path, manifest.origin).toString();
297
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
+ export * from "./admin";
1
2
  export * from "./billing";
2
3
  export * from "./events";
3
4
  export * from "./identity";
4
5
  export * from "./notifications";
6
+ export * from "./sleptons";
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { RESUME_CONTENT_VERSION, ResumeContentV1Schema, ResumeWriteSchema } from "./sleptons";
3
+
4
+ describe("ResumeContentV1Schema", () => {
5
+ it("accepts a minimal résumé and fills defaults", () => {
6
+ const parsed = ResumeContentV1Schema.parse({ basic: { name: "Zhang Wei" } });
7
+ expect(parsed.version).toBe(RESUME_CONTENT_VERSION);
8
+ expect(parsed.objective).toEqual({});
9
+ expect(parsed.preferences.paper).toBe("A4");
10
+ expect(parsed.preferences.margins.top).toBe("12mm");
11
+ expect(parsed.preferences.show_contact_public).toBe(false);
12
+ });
13
+
14
+ it("defaults experience kind to work", () => {
15
+ const parsed = ResumeContentV1Schema.parse({
16
+ basic: { name: "A" },
17
+ experiences: [{ period: "2024", org: "Acme", title: "Eng" }],
18
+ });
19
+ expect(parsed.experiences?.[0]?.kind).toBe("work");
20
+ });
21
+
22
+ it("rejects an empty name, bad email, and more than 8 advantage tags", () => {
23
+ expect(ResumeContentV1Schema.safeParse({ basic: { name: "" } }).success).toBe(false);
24
+ expect(ResumeContentV1Schema.safeParse({ basic: { name: "A", email: "nope" } }).success).toBe(
25
+ false,
26
+ );
27
+ expect(
28
+ ResumeContentV1Schema.safeParse({
29
+ basic: { name: "A" },
30
+ objective: { advantage_tags: Array.from({ length: 9 }, (_, i) => `t${i}`) },
31
+ }).success,
32
+ ).toBe(false);
33
+ });
34
+
35
+ it("rejects a wrong content version", () => {
36
+ expect(ResumeContentV1Schema.safeParse({ version: 2, basic: { name: "A" } }).success).toBe(
37
+ false,
38
+ );
39
+ });
40
+
41
+ it("only allows known skill categories", () => {
42
+ const res = ResumeContentV1Schema.safeParse({
43
+ basic: { name: "A" },
44
+ skills: { programming: ["TypeScript"], unknown_cat: ["x"] },
45
+ });
46
+ // unknown keys are stripped by default, never persisted
47
+ expect(res.success).toBe(true);
48
+ expect(res.success && "unknown_cat" in (res.data.skills ?? {})).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe("ResumeWriteSchema", () => {
53
+ it("wraps content with optional visibility and language", () => {
54
+ const parsed = ResumeWriteSchema.parse({
55
+ content: { basic: { name: "A" } },
56
+ is_public: true,
57
+ language: "EN",
58
+ });
59
+ expect(parsed.is_public).toBe(true);
60
+ expect(parsed.language).toBe("EN");
61
+ });
62
+ });