@jskit-ai/assistant 0.1.40 → 0.1.41

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.
@@ -2,51 +2,47 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import descriptor from "../package.descriptor.mjs";
4
4
 
5
- function findFileMutation(id) {
6
- const mutations = Array.isArray(descriptor?.mutations?.files) ? descriptor.mutations.files : [];
7
- return mutations.find((entry) => String(entry?.id || "") === id) || null;
8
- }
9
-
10
5
  function findTextMutation(id) {
11
6
  const mutations = Array.isArray(descriptor?.mutations?.text) ? descriptor.mutations.text : [];
12
7
  return mutations.find((entry) => String(entry?.id || "") === id) || null;
13
8
  }
14
9
 
15
- test("assistant descriptor exposes per-surface generation options and depends on assistant-runtime", () => {
10
+ test("assistant descriptor exposes setup as the primary command and still depends on assistant-runtime", () => {
16
11
  assert.equal(descriptor.kind, "generator");
12
+ assert.equal(descriptor.metadata?.generatorPrimarySubcommand, "setup");
17
13
  assert.equal(descriptor.options?.surface?.required, true);
14
+ assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
18
15
  assert.equal(descriptor.options?.["settings-surface"]?.required, true);
16
+ assert.equal(descriptor.options?.["settings-surface"]?.validationType, "enabled-surface-id");
19
17
  assert.equal(descriptor.options?.["config-scope"]?.defaultValue, "global");
20
- assert.equal(descriptor.options?.["ai-config-prefix"]?.required, false);
18
+ assert.equal(descriptor.options?.name?.required, false);
19
+ assert.equal(descriptor.options?.["link-placement"]?.required, false);
21
20
  assert.deepEqual(descriptor.dependsOn, ["@jskit-ai/assistant-runtime"]);
22
21
  });
23
22
 
24
- test("assistant descriptor generates only assistant runtime/settings pages and no local runtime package", () => {
25
- const runtimePage = findFileMutation("assistant-page-runtime");
26
- const settingsPageStandard = findFileMutation("assistant-page-settings-standard");
27
- const settingsPageAdmin = findFileMutation("assistant-page-settings-admin");
23
+ test("assistant descriptor defines explicit page subcommands and setup-only mutations", () => {
24
+ const subcommands = descriptor.metadata?.generatorSubcommands || {};
28
25
  const fileMutations = Array.isArray(descriptor?.mutations?.files) ? descriptor.mutations.files : [];
29
26
 
30
- assert.equal(runtimePage?.toSurface, "${option:surface|lower}");
31
- assert.equal(runtimePage?.toSurfacePath, "assistant/index.vue");
32
- assert.equal(settingsPageStandard?.toSurfacePath, "settings/${option:settings-route-path|path}/index.vue");
33
- assert.equal(settingsPageAdmin?.toSurfacePath, "workspace/settings/${option:settings-route-path|path}/index.vue");
34
- assert.equal(fileMutations.length, 3);
27
+ assert.equal(subcommands.setup?.description?.length > 0, true);
28
+ assert.equal(subcommands.page?.entrypoint, "src/server/subcommands/page.js");
29
+ assert.equal(subcommands["settings-page"]?.entrypoint, "src/server/subcommands/settingsPage.js");
30
+ assert.equal(subcommands.page?.positionalArgs?.[0]?.name, "target-file");
31
+ assert.equal(subcommands["settings-page"]?.requiredOptionNames?.[0], "surface");
32
+ assert.equal(subcommands.page?.optionNames?.includes("force"), true);
33
+ assert.equal(subcommands["settings-page"]?.optionNames?.includes("force"), true);
34
+ assert.equal(fileMutations.length, 0);
35
35
  });
36
36
 
37
- test("assistant descriptor appends menu placement, settings menu placement, and per-surface config entries", () => {
38
- const menuPlacement = findTextMutation("assistant-placement-menu");
39
- const settingsPlacement = findTextMutation("assistant-settings-menu-placement");
37
+ test("assistant descriptor appends only setup config and env entries", () => {
40
38
  const publicConfig = findTextMutation("assistant-public-surface-config");
41
39
  const serverConfig = findTextMutation("assistant-server-surface-config");
42
40
  const envBlock = findTextMutation("assistant-ai-prefixed-env");
41
+ const textMutations = Array.isArray(descriptor?.mutations?.text) ? descriptor.mutations.text : [];
43
42
 
44
- assert.match(String(menuPlacement?.value || ""), /assistant\.generated\.menu:\$\{option:surface\|lower\}/);
45
- assert.match(String(menuPlacement?.value || ""), /surfaces: \["\$\{option:surface\|lower\}"\]/);
46
- assert.match(String(settingsPlacement?.value || ""), /assistant\.generated\.settings\.menu:\$\{option:surface\|lower\}/);
47
- assert.match(String(settingsPlacement?.value || ""), /host: "__ASSISTANT_SETTINGS_HOST__"/);
48
- assert.match(String(settingsPlacement?.value || ""), /position: "primary-menu"/);
43
+ assert.equal(textMutations.length, 3);
49
44
  assert.match(String(publicConfig?.value || ""), /config\.assistantSurfaces\.\$\{option:surface\|lower\} = \{/);
45
+ assert.match(String(publicConfig?.value || ""), /settingsSurfaceId: "__ASSISTANT_SETTINGS_SURFACE_ID__"/);
50
46
  assert.match(String(serverConfig?.value || ""), /config\.assistantServer\.\$\{option:surface\|lower\} = \{/);
51
47
  assert.match(String(serverConfig?.value || ""), /aiConfigPrefix: "__ASSISTANT_AI_CONFIG_PREFIX__"/);
52
48
  assert.match(String(envBlock?.value || ""), /__ASSISTANT_AI_CONFIG_PREFIX___AI_PROVIDER=\$\{option:ai-provider\}/);
@@ -0,0 +1,283 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+ import { runGeneratorSubcommand as runPageSubcommand } from "../src/server/subcommands/page.js";
7
+ import { runGeneratorSubcommand as runSettingsPageSubcommand } from "../src/server/subcommands/settingsPage.js";
8
+
9
+ async function withTempApp(run) {
10
+ const appRoot = await mkdtemp(path.join(tmpdir(), "assistant-subcommands-"));
11
+ try {
12
+ return await run(appRoot);
13
+ } finally {
14
+ await rm(appRoot, { recursive: true, force: true });
15
+ }
16
+ }
17
+
18
+ async function writeFileInApp(appRoot, relativePath, source) {
19
+ const absoluteFile = path.join(appRoot, relativePath);
20
+ await mkdir(path.dirname(absoluteFile), { recursive: true });
21
+ await writeFile(absoluteFile, source, "utf8");
22
+ }
23
+
24
+ function toPagePath(targetFile = "") {
25
+ return path.join("src/pages", targetFile);
26
+ }
27
+
28
+ async function writeAppFixture(appRoot) {
29
+ await writeFileInApp(
30
+ appRoot,
31
+ "config/public.js",
32
+ `export const config = {
33
+ surfaceDefinitions: {
34
+ admin: {
35
+ id: "admin",
36
+ pagesRoot: "w/[workspaceSlug]/admin",
37
+ enabled: true,
38
+ requiresWorkspace: true,
39
+ accessPolicyId: "workspace_member"
40
+ },
41
+ console: {
42
+ id: "console",
43
+ pagesRoot: "console",
44
+ enabled: true,
45
+ requiresWorkspace: false,
46
+ accessPolicyId: "console_owner"
47
+ }
48
+ },
49
+ assistantSurfaces: {}
50
+ };
51
+ `
52
+ );
53
+ await writeFileInApp(
54
+ appRoot,
55
+ "src/components/ShellLayout.vue",
56
+ `<template>
57
+ <div>
58
+ <ShellOutlet host="shell-layout" position="primary-menu" default />
59
+ <ShellOutlet host="shell-layout" position="top-right" />
60
+ </div>
61
+ </template>
62
+ `
63
+ );
64
+ await writeFileInApp(
65
+ appRoot,
66
+ "src/placement.js",
67
+ `function addPlacement() {}
68
+
69
+ export { addPlacement };
70
+ export default function getPlacements() {
71
+ return [];
72
+ }
73
+ `
74
+ );
75
+ }
76
+
77
+ test("assistant page subcommand creates a runtime page at an explicit target file", async () => {
78
+ await withTempApp(async (appRoot) => {
79
+ await writeAppFixture(appRoot);
80
+
81
+ const targetFile = "w/[workspaceSlug]/admin/ops/copilot/index.vue";
82
+ const result = await runPageSubcommand({
83
+ appRoot,
84
+ subcommand: "page",
85
+ args: [targetFile],
86
+ options: {}
87
+ });
88
+
89
+ assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
90
+
91
+ const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
92
+ assert.match(pageSource, /<AssistantSurfaceClientElement surface-id="admin" \/>/);
93
+
94
+ const placementSource = await readFile(path.join(appRoot, "src/placement.js"), "utf8");
95
+ assert.match(placementSource, /jskit:assistant\.page\.link:admin:\/ops\/copilot/);
96
+ assert.match(placementSource, /id: "ui-generator\.page\.admin\.ops\.copilot\.link"/);
97
+ assert.match(placementSource, /host: "shell-layout"/);
98
+ assert.match(placementSource, /position: "primary-menu"/);
99
+ assert.match(placementSource, /label: "Copilot"/);
100
+ assert.match(placementSource, /workspaceSuffix: "\/ops\/copilot"/);
101
+ });
102
+ });
103
+
104
+ test("assistant settings-page subcommand uses the target assistant surface and infers parent subpage placement", async () => {
105
+ await withTempApp(async (appRoot) => {
106
+ await writeAppFixture(appRoot);
107
+ await writeFileInApp(
108
+ appRoot,
109
+ "src/pages/w/[workspaceSlug]/admin/settings/index.vue",
110
+ `<template>
111
+ <SectionContainerShell>
112
+ <template #tabs>
113
+ <ShellOutlet host="admin-settings" position="sub-pages" />
114
+ </template>
115
+ <RouterView />
116
+ </SectionContainerShell>
117
+ </template>
118
+ `
119
+ );
120
+
121
+ const targetFile = "w/[workspaceSlug]/admin/settings/index/assistant/index.vue";
122
+ const result = await runSettingsPageSubcommand({
123
+ appRoot,
124
+ subcommand: "settings-page",
125
+ args: [targetFile],
126
+ options: {
127
+ surface: "console"
128
+ }
129
+ });
130
+
131
+ assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
132
+
133
+ const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
134
+ assert.match(pageSource, /<AssistantSettingsClientElement target-surface-id="console" \/>/);
135
+
136
+ const placementSource = await readFile(path.join(appRoot, "src/placement.js"), "utf8");
137
+ assert.match(placementSource, /jskit:assistant\.settings-page\.link:admin:\/settings\/assistant:console/);
138
+ assert.match(placementSource, /host: "admin-settings"/);
139
+ assert.match(placementSource, /position: "sub-pages"/);
140
+ assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
141
+ assert.match(placementSource, /to: "\.\/assistant"/);
142
+ assert.match(placementSource, /label: "Assistant"/);
143
+ });
144
+ });
145
+
146
+ test("assistant page subcommand refuses to overwrite an existing user-owned page", async () => {
147
+ await withTempApp(async (appRoot) => {
148
+ await writeAppFixture(appRoot);
149
+
150
+ const targetFile = "w/[workspaceSlug]/admin/assistant/index.vue";
151
+ await writeFileInApp(
152
+ appRoot,
153
+ toPagePath(targetFile),
154
+ `<template>
155
+ <div>custom page</div>
156
+ </template>
157
+ `
158
+ );
159
+
160
+ await assert.rejects(
161
+ () =>
162
+ runPageSubcommand({
163
+ appRoot,
164
+ subcommand: "page",
165
+ args: [targetFile],
166
+ options: {}
167
+ }),
168
+ /will not overwrite existing page/
169
+ );
170
+ });
171
+ });
172
+
173
+ test("assistant page subcommand overwrites an existing page when --force is passed", async () => {
174
+ await withTempApp(async (appRoot) => {
175
+ await writeAppFixture(appRoot);
176
+
177
+ const targetFile = "w/[workspaceSlug]/admin/assistant/index.vue";
178
+ await writeFileInApp(
179
+ appRoot,
180
+ toPagePath(targetFile),
181
+ `<template>
182
+ <div>custom assistant page</div>
183
+ </template>
184
+ `
185
+ );
186
+
187
+ const result = await runPageSubcommand({
188
+ appRoot,
189
+ subcommand: "page",
190
+ args: [targetFile],
191
+ options: {
192
+ force: "true"
193
+ }
194
+ });
195
+
196
+ assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
197
+ assert.equal(result.summary, 'Regenerated assistant page "/assistant".');
198
+
199
+ const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
200
+ assert.match(pageSource, /<AssistantSurfaceClientElement surface-id="admin" \/>/);
201
+ assert.doesNotMatch(pageSource, /custom assistant page/);
202
+ });
203
+ });
204
+
205
+ test("assistant settings-page subcommand overwrites an existing page when --force is passed", async () => {
206
+ await withTempApp(async (appRoot) => {
207
+ await writeAppFixture(appRoot);
208
+ await writeFileInApp(
209
+ appRoot,
210
+ "src/pages/w/[workspaceSlug]/admin/settings/index.vue",
211
+ `<template>
212
+ <SectionContainerShell>
213
+ <template #tabs>
214
+ <ShellOutlet host="admin-settings" position="sub-pages" />
215
+ </template>
216
+ <RouterView />
217
+ </SectionContainerShell>
218
+ </template>
219
+ `
220
+ );
221
+
222
+ const targetFile = "w/[workspaceSlug]/admin/settings/index/assistant/index.vue";
223
+ await writeFileInApp(
224
+ appRoot,
225
+ toPagePath(targetFile),
226
+ `<template>
227
+ <div>custom settings page</div>
228
+ </template>
229
+ `
230
+ );
231
+
232
+ const result = await runSettingsPageSubcommand({
233
+ appRoot,
234
+ subcommand: "settings-page",
235
+ args: [targetFile],
236
+ options: {
237
+ surface: "console",
238
+ force: "true"
239
+ }
240
+ });
241
+
242
+ assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
243
+ assert.equal(result.summary, 'Regenerated assistant page "/settings/assistant".');
244
+
245
+ const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
246
+ assert.match(pageSource, /<AssistantSettingsClientElement target-surface-id="console" \/>/);
247
+ assert.doesNotMatch(pageSource, /custom settings page/);
248
+ });
249
+ });
250
+
251
+ test("assistant settings-page subcommand requires the target assistant surface option", async () => {
252
+ await withTempApp(async (appRoot) => {
253
+ await writeAppFixture(appRoot);
254
+
255
+ await assert.rejects(
256
+ () =>
257
+ runSettingsPageSubcommand({
258
+ appRoot,
259
+ subcommand: "settings-page",
260
+ args: ["w/[workspaceSlug]/admin/settings/assistant/index.vue"],
261
+ options: {}
262
+ }),
263
+ /assistant generator requires --surface/
264
+ );
265
+ });
266
+ });
267
+
268
+ test("assistant page subcommand rejects target files with a src/pages prefix", async () => {
269
+ await withTempApp(async (appRoot) => {
270
+ await writeAppFixture(appRoot);
271
+
272
+ await assert.rejects(
273
+ () =>
274
+ runPageSubcommand({
275
+ appRoot,
276
+ subcommand: "page",
277
+ args: ["src/pages/w/[workspaceSlug]/admin/assistant/index.vue"],
278
+ options: {}
279
+ }),
280
+ /must be relative to src\/pages\/, without the src\/pages\/ prefix/
281
+ );
282
+ });
283
+ });