@jskit-ai/assistant 0.1.40 → 0.1.42

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,114 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import {
4
+ resolvePageLinkTargetDetails,
5
+ resolvePageTargetDetails
6
+ } from "@jskit-ai/kernel/server/support";
7
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
8
+
9
+ const PAGE_TEMPLATE_FILE = "../../templates/src/pages/assistant/index.vue";
10
+ const SETTINGS_PAGE_TEMPLATE_FILE = "../../templates/src/pages/settings/assistant/index.vue";
11
+
12
+ function resolveLinkToPropLine(linkTo = "") {
13
+ if (!linkTo) {
14
+ return "";
15
+ }
16
+ return ` to: ${JSON.stringify(linkTo)},\n`;
17
+ }
18
+
19
+ function resolveTemplateFilePath(relativePath = "") {
20
+ return fileURLToPath(new URL(relativePath, import.meta.url));
21
+ }
22
+
23
+ async function readAssistantPageTemplateSource(kind = "page") {
24
+ const templateFilePath =
25
+ kind === "settings-page"
26
+ ? resolveTemplateFilePath(SETTINGS_PAGE_TEMPLATE_FILE)
27
+ : resolveTemplateFilePath(PAGE_TEMPLATE_FILE);
28
+ return readFile(templateFilePath, "utf8");
29
+ }
30
+
31
+ function renderAssistantPageSource(templateSource = "", surfaceId = "") {
32
+ return String(templateSource || "").replaceAll("__ASSISTANT_SURFACE_ID__", String(surfaceId || ""));
33
+ }
34
+
35
+ async function resolveAssistantPageGenerationContext({
36
+ appRoot,
37
+ targetFile = "",
38
+ options = {},
39
+ context = "assistant page"
40
+ } = {}) {
41
+ const pageTarget = await resolvePageTargetDetails({
42
+ appRoot,
43
+ targetFile,
44
+ context
45
+ });
46
+ const linkTarget = await resolvePageLinkTargetDetails({
47
+ appRoot: pageTarget.appRoot,
48
+ pageTarget,
49
+ targetFile,
50
+ context,
51
+ placement: options?.["link-placement"],
52
+ componentToken: options?.["link-component-token"],
53
+ linkTo: options?.["link-to"]
54
+ });
55
+
56
+ return Object.freeze({
57
+ pageTarget,
58
+ pageLabel: normalizeText(options?.name) || pageTarget.defaultName,
59
+ linkPlacementHost: String(linkTarget.placementTarget?.host || ""),
60
+ linkPlacementPosition: String(linkTarget.placementTarget?.position || ""),
61
+ linkComponentToken: String(linkTarget.componentToken || ""),
62
+ linkWorkspaceSuffix: pageTarget.routeUrlSuffix,
63
+ linkNonWorkspaceSuffix: pageTarget.routeUrlSuffix,
64
+ linkToPropLine: resolveLinkToPropLine(linkTarget.linkTo)
65
+ });
66
+ }
67
+
68
+ function renderAssistantPageLinkPlacementBlock({
69
+ marker = "",
70
+ pageTarget = {},
71
+ generationContext = {}
72
+ } = {}) {
73
+ return (
74
+ `// ${marker}\n` +
75
+ "{\n" +
76
+ " addPlacement({\n" +
77
+ ` id: "${String(pageTarget?.placementId || "")}",\n` +
78
+ ` host: "${String(generationContext?.linkPlacementHost || "")}",\n` +
79
+ ` position: "${String(generationContext?.linkPlacementPosition || "")}",\n` +
80
+ ` surfaces: ["${String(pageTarget?.surfaceId || "")}"],\n` +
81
+ " order: 155,\n" +
82
+ ` componentToken: "${String(generationContext?.linkComponentToken || "")}",\n` +
83
+ " props: {\n" +
84
+ ` label: "${String(generationContext?.pageLabel || "")}",\n` +
85
+ ` surface: "${String(pageTarget?.surfaceId || "")}",\n` +
86
+ ` workspaceSuffix: "${String(generationContext?.linkWorkspaceSuffix || "")}",\n` +
87
+ ` nonWorkspaceSuffix: "${String(generationContext?.linkNonWorkspaceSuffix || "")}",\n` +
88
+ `${String(generationContext?.linkToPropLine || "")} },\n` +
89
+ " when: ({ auth }) => Boolean(auth?.authenticated)\n" +
90
+ " });\n" +
91
+ "}\n"
92
+ );
93
+ }
94
+
95
+ function renderAssistantPageSummary(
96
+ pageTarget = {},
97
+ { pageAlreadyExisted = false, pageOverwritten = false } = {}
98
+ ) {
99
+ if (!pageAlreadyExisted) {
100
+ return `Generated assistant page "${String(pageTarget?.routeUrlSuffix || "")}".`;
101
+ }
102
+ if (pageOverwritten) {
103
+ return `Regenerated assistant page "${String(pageTarget?.routeUrlSuffix || "")}".`;
104
+ }
105
+ return `Generated assistant page "${String(pageTarget?.routeUrlSuffix || "")}".`;
106
+ }
107
+
108
+ export {
109
+ readAssistantPageTemplateSource,
110
+ renderAssistantPageSource,
111
+ resolveAssistantPageGenerationContext,
112
+ renderAssistantPageLinkPlacementBlock,
113
+ renderAssistantPageSummary
114
+ };
@@ -0,0 +1,104 @@
1
+ import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import {
5
+ readAssistantPageTemplateSource,
6
+ renderAssistantPageLinkPlacementBlock,
7
+ renderAssistantPageSource,
8
+ renderAssistantPageSummary,
9
+ resolveAssistantPageGenerationContext
10
+ } from "../pageSupport.js";
11
+ import {
12
+ PLACEMENT_FILE,
13
+ appendBlockIfMarkerMissing,
14
+ rejectUnexpectedOptions,
15
+ requireEmptyPageSource,
16
+ requireSinglePositionalTargetFile,
17
+ resolvePathWithinApp
18
+ } from "./support.js";
19
+
20
+ async function runGeneratorSubcommand({
21
+ appRoot,
22
+ subcommand = "",
23
+ args = [],
24
+ options = {},
25
+ dryRun = false
26
+ } = {}) {
27
+ const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
28
+ if (normalizedSubcommand !== "page") {
29
+ throw new Error(`Unsupported assistant subcommand: ${normalizedSubcommand || "<empty>"}.`);
30
+ }
31
+
32
+ const targetFile = requireSinglePositionalTargetFile(args, { context: "assistant page" });
33
+ rejectUnexpectedOptions(options, ["name", "link-placement", "link-component-token", "link-to", "force"], {
34
+ context: "assistant page"
35
+ });
36
+ const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
37
+ ? normalizeBoolean(options.force)
38
+ : false;
39
+
40
+ const generationContext = await resolveAssistantPageGenerationContext({
41
+ appRoot,
42
+ targetFile,
43
+ options,
44
+ context: "assistant page"
45
+ });
46
+ const pageTarget = generationContext.pageTarget;
47
+ const pageFilePath = pageTarget.targetFilePath.absolutePath;
48
+ const pageRelativePath = pageTarget.targetFilePath.relativePath;
49
+ const templateSource = await readAssistantPageTemplateSource("page");
50
+ const desiredPageSource = renderAssistantPageSource(templateSource, pageTarget.surfaceId);
51
+
52
+ let existingPageSource = "";
53
+ let pageAlreadyExisted = true;
54
+ try {
55
+ existingPageSource = await readFile(pageFilePath, "utf8");
56
+ } catch {
57
+ pageAlreadyExisted = false;
58
+ }
59
+
60
+ requireEmptyPageSource(existingPageSource, pageRelativePath, {
61
+ context: "assistant page",
62
+ forceOverwrite
63
+ });
64
+
65
+ const touchedFiles = new Set();
66
+ if (!pageAlreadyExisted || forceOverwrite) {
67
+ if (dryRun !== true) {
68
+ await mkdir(path.dirname(pageFilePath), { recursive: true });
69
+ await writeFile(pageFilePath, desiredPageSource, "utf8");
70
+ }
71
+ touchedFiles.add(pageRelativePath);
72
+ }
73
+
74
+ const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
75
+ context: "assistant page"
76
+ });
77
+ const placementSource = await readFile(placementPath.absolutePath, "utf8");
78
+ const placementMarker = `jskit:assistant.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`;
79
+ const placementApplied = appendBlockIfMarkerMissing(
80
+ placementSource,
81
+ placementMarker,
82
+ renderAssistantPageLinkPlacementBlock({
83
+ marker: placementMarker,
84
+ pageTarget,
85
+ generationContext
86
+ })
87
+ );
88
+ if (placementApplied.changed) {
89
+ if (dryRun !== true) {
90
+ await writeFile(placementPath.absolutePath, placementApplied.content, "utf8");
91
+ }
92
+ touchedFiles.add(placementPath.relativePath);
93
+ }
94
+
95
+ return {
96
+ touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
97
+ summary: renderAssistantPageSummary(pageTarget, {
98
+ pageAlreadyExisted,
99
+ pageOverwritten: pageAlreadyExisted && forceOverwrite
100
+ })
101
+ };
102
+ }
103
+
104
+ export { runGeneratorSubcommand };
@@ -0,0 +1,108 @@
1
+ import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import {
5
+ readAssistantPageTemplateSource,
6
+ renderAssistantPageLinkPlacementBlock,
7
+ renderAssistantPageSource,
8
+ renderAssistantPageSummary,
9
+ resolveAssistantPageGenerationContext
10
+ } from "../pageSupport.js";
11
+ import { loadAppConfig, resolveSurfaceDefinition } from "../support.js";
12
+ import {
13
+ PLACEMENT_FILE,
14
+ appendBlockIfMarkerMissing,
15
+ rejectUnexpectedOptions,
16
+ requireEmptyPageSource,
17
+ requireSinglePositionalTargetFile,
18
+ resolvePathWithinApp
19
+ } from "./support.js";
20
+
21
+ async function runGeneratorSubcommand({
22
+ appRoot,
23
+ subcommand = "",
24
+ args = [],
25
+ options = {},
26
+ dryRun = false
27
+ } = {}) {
28
+ const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
29
+ if (normalizedSubcommand !== "settings-page") {
30
+ throw new Error(`Unsupported assistant subcommand: ${normalizedSubcommand || "<empty>"}.`);
31
+ }
32
+
33
+ const targetFile = requireSinglePositionalTargetFile(args, { context: "assistant settings-page" });
34
+ rejectUnexpectedOptions(options, ["surface", "name", "link-placement", "link-component-token", "link-to", "force"], {
35
+ context: "assistant settings-page"
36
+ });
37
+ const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
38
+ ? normalizeBoolean(options.force)
39
+ : false;
40
+
41
+ const appConfig = await loadAppConfig(appRoot);
42
+ const targetSurface = resolveSurfaceDefinition(appConfig, options?.surface, "surface");
43
+ const generationContext = await resolveAssistantPageGenerationContext({
44
+ appRoot,
45
+ targetFile,
46
+ options,
47
+ context: "assistant settings-page"
48
+ });
49
+ const pageTarget = generationContext.pageTarget;
50
+ const pageFilePath = pageTarget.targetFilePath.absolutePath;
51
+ const pageRelativePath = pageTarget.targetFilePath.relativePath;
52
+ const templateSource = await readAssistantPageTemplateSource("settings-page");
53
+ const desiredPageSource = renderAssistantPageSource(templateSource, targetSurface.id);
54
+
55
+ let existingPageSource = "";
56
+ let pageAlreadyExisted = true;
57
+ try {
58
+ existingPageSource = await readFile(pageFilePath, "utf8");
59
+ } catch {
60
+ pageAlreadyExisted = false;
61
+ }
62
+
63
+ requireEmptyPageSource(existingPageSource, pageRelativePath, {
64
+ context: "assistant settings-page",
65
+ forceOverwrite
66
+ });
67
+
68
+ const touchedFiles = new Set();
69
+ if (!pageAlreadyExisted || forceOverwrite) {
70
+ if (dryRun !== true) {
71
+ await mkdir(path.dirname(pageFilePath), { recursive: true });
72
+ await writeFile(pageFilePath, desiredPageSource, "utf8");
73
+ }
74
+ touchedFiles.add(pageRelativePath);
75
+ }
76
+
77
+ const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
78
+ context: "assistant settings-page"
79
+ });
80
+ const placementSource = await readFile(placementPath.absolutePath, "utf8");
81
+ const placementMarker =
82
+ `jskit:assistant.settings-page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}:${targetSurface.id}`;
83
+ const placementApplied = appendBlockIfMarkerMissing(
84
+ placementSource,
85
+ placementMarker,
86
+ renderAssistantPageLinkPlacementBlock({
87
+ marker: placementMarker,
88
+ pageTarget,
89
+ generationContext
90
+ })
91
+ );
92
+ if (placementApplied.changed) {
93
+ if (dryRun !== true) {
94
+ await writeFile(placementPath.absolutePath, placementApplied.content, "utf8");
95
+ }
96
+ touchedFiles.add(placementPath.relativePath);
97
+ }
98
+
99
+ return {
100
+ touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
101
+ summary: renderAssistantPageSummary(pageTarget, {
102
+ pageAlreadyExisted,
103
+ pageOverwritten: pageAlreadyExisted && forceOverwrite
104
+ })
105
+ };
106
+ }
107
+
108
+ export { runGeneratorSubcommand };
@@ -0,0 +1,108 @@
1
+ import path from "node:path";
2
+ import { resolveRequiredAppRoot, toPosixPath } from "@jskit-ai/kernel/server/support";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+
5
+ const PLACEMENT_FILE = "src/placement.js";
6
+
7
+ function requireSinglePositionalTargetFile(args = [], { context = "assistant" } = {}) {
8
+ const positionalArgs = Array.isArray(args) ? args.map((value) => normalizeText(value)).filter(Boolean) : [];
9
+ if (positionalArgs.length !== 1) {
10
+ throw new Error(`${context} requires exactly one <target-file> positional argument.`);
11
+ }
12
+
13
+ return positionalArgs[0];
14
+ }
15
+
16
+ function rejectUnexpectedOptions(options = {}, allowedOptionNames = [], { context = "assistant" } = {}) {
17
+ const allowedOptionNameSet = new Set(
18
+ (Array.isArray(allowedOptionNames) ? allowedOptionNames : [])
19
+ .map((optionName) => normalizeText(optionName))
20
+ .filter(Boolean)
21
+ );
22
+
23
+ const unexpectedOptions = Object.keys(options || {})
24
+ .map((optionName) => normalizeText(optionName))
25
+ .filter(Boolean)
26
+ .filter((optionName) => !allowedOptionNameSet.has(optionName))
27
+ .sort((left, right) => left.localeCompare(right));
28
+
29
+ if (unexpectedOptions.length < 1) {
30
+ return;
31
+ }
32
+
33
+ throw new Error(
34
+ `${context} received unsupported option${unexpectedOptions.length > 1 ? "s" : ""}: ${unexpectedOptions.map((optionName) => `--${optionName}`).join(", ")}.`
35
+ );
36
+ }
37
+
38
+ function resolvePathWithinApp(appRoot, targetPath, { context = "assistant" } = {}) {
39
+ const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
40
+ context
41
+ });
42
+
43
+ const normalizedTargetPath = normalizeText(targetPath);
44
+ if (!normalizedTargetPath) {
45
+ throw new Error(`${context} requires target path.`);
46
+ }
47
+
48
+ const absolutePath = path.resolve(resolvedAppRoot, normalizedTargetPath);
49
+ const relativePath = path.relative(resolvedAppRoot, absolutePath);
50
+ if (
51
+ !relativePath ||
52
+ relativePath === ".." ||
53
+ relativePath.startsWith(`..${path.sep}`) ||
54
+ path.isAbsolute(relativePath)
55
+ ) {
56
+ throw new Error(`${context} target path must stay within app root: ${normalizedTargetPath}`);
57
+ }
58
+
59
+ return Object.freeze({
60
+ absolutePath,
61
+ relativePath: toPosixPath(relativePath)
62
+ });
63
+ }
64
+
65
+ function ensureTrailingNewline(value = "") {
66
+ const source = String(value || "");
67
+ return source.endsWith("\n") ? source : `${source}\n`;
68
+ }
69
+
70
+ function appendBlockIfMarkerMissing(source = "", marker = "", block = "") {
71
+ const normalizedMarker = String(marker || "").trim();
72
+ const normalizedBlock = String(block || "").trim();
73
+ const sourceText = String(source || "");
74
+ if (!normalizedMarker || !normalizedBlock || sourceText.includes(normalizedMarker)) {
75
+ return {
76
+ changed: false,
77
+ content: sourceText
78
+ };
79
+ }
80
+
81
+ return {
82
+ changed: true,
83
+ content: `${ensureTrailingNewline(sourceText)}${normalizedBlock}\n`
84
+ };
85
+ }
86
+
87
+ function requireEmptyPageSource(existingSource = "", targetRelativePath = "", { context = "assistant", forceOverwrite = false } = {}) {
88
+ const sourceText = String(existingSource || "");
89
+ if (!sourceText) {
90
+ return;
91
+ }
92
+ if (forceOverwrite) {
93
+ return;
94
+ }
95
+
96
+ throw new Error(
97
+ `${context} will not overwrite existing page ${targetRelativePath}. Re-run with --force to overwrite it.`
98
+ );
99
+ }
100
+
101
+ export {
102
+ PLACEMENT_FILE,
103
+ requireSinglePositionalTargetFile,
104
+ rejectUnexpectedOptions,
105
+ resolvePathWithinApp,
106
+ appendBlockIfMarkerMissing,
107
+ requireEmptyPageSource
108
+ };
@@ -0,0 +1,78 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { loadAppConfigFromModuleUrl, resolveRequiredAppRoot } from "@jskit-ai/kernel/server/support";
4
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
5
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
6
+ import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
7
+
8
+ function normalizeConfigScope(value = "") {
9
+ const normalized = normalizeText(value).toLowerCase();
10
+ if (normalized === "global" || normalized === "workspace") {
11
+ return normalized;
12
+ }
13
+
14
+ throw new Error('assistant generator option "config-scope" must be "global" or "workspace".');
15
+ }
16
+
17
+ async function loadAppConfig(appRoot = "") {
18
+ const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
19
+ context: "assistant generator"
20
+ });
21
+ const publicConfigUrl = pathToFileURL(path.join(resolvedAppRoot, "config", "public.js")).href;
22
+ return loadAppConfigFromModuleUrl({
23
+ moduleUrl: publicConfigUrl
24
+ });
25
+ }
26
+
27
+ function resolveSurfaceDefinition(appConfig = {}, surfaceId = "", optionName = "surface") {
28
+ const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
29
+ if (!normalizedSurfaceId) {
30
+ throw new Error(`assistant generator requires --${optionName}.`);
31
+ }
32
+
33
+ const sourceDefinitions =
34
+ appConfig && typeof appConfig.surfaceDefinitions === "object" && !Array.isArray(appConfig.surfaceDefinitions)
35
+ ? appConfig.surfaceDefinitions
36
+ : {};
37
+ const rawDefinition = sourceDefinitions[normalizedSurfaceId];
38
+ if (!rawDefinition || typeof rawDefinition !== "object" || Array.isArray(rawDefinition)) {
39
+ throw new Error(`assistant generator surface "${normalizedSurfaceId}" is not defined in config/public.js.`);
40
+ }
41
+ if (rawDefinition.enabled === false) {
42
+ throw new Error(`assistant generator surface "${normalizedSurfaceId}" is disabled in config/public.js.`);
43
+ }
44
+
45
+ return Object.freeze({
46
+ id: normalizedSurfaceId,
47
+ requiresWorkspace: rawDefinition.requiresWorkspace === true,
48
+ accessPolicyId: normalizeText(rawDefinition.accessPolicyId).toLowerCase()
49
+ });
50
+ }
51
+
52
+ function assertAssistantSurfaceIsAvailable(appConfig = {}, surfaceId = "") {
53
+ const assistantSurfaces =
54
+ appConfig && typeof appConfig.assistantSurfaces === "object" && !Array.isArray(appConfig.assistantSurfaces)
55
+ ? appConfig.assistantSurfaces
56
+ : {};
57
+ if (assistantSurfaces[surfaceId]) {
58
+ throw new Error(`assistant generator surface "${surfaceId}" already has an assistant configured in config/public.js.`);
59
+ }
60
+ }
61
+
62
+ function resolveAiConfigPrefix(surfaceId = "", explicitPrefix = "") {
63
+ const normalizedExplicitPrefix = normalizeText(explicitPrefix);
64
+ if (normalizedExplicitPrefix) {
65
+ return normalizedExplicitPrefix;
66
+ }
67
+
68
+ const surfacePrefix = toSnakeCase(surfaceId).toUpperCase();
69
+ return surfacePrefix ? `${surfacePrefix}_ASSISTANT` : "ASSISTANT";
70
+ }
71
+
72
+ export {
73
+ normalizeConfigScope,
74
+ loadAppConfig,
75
+ resolveSurfaceDefinition,
76
+ assertAssistantSurfaceIsAvailable,
77
+ resolveAiConfigPrefix
78
+ };
@@ -9,7 +9,6 @@ async function withTempApp(run) {
9
9
  const appRoot = await mkdtemp(path.join(tmpdir(), "assistant-generator-"));
10
10
  try {
11
11
  await mkdir(path.join(appRoot, "config"), { recursive: true });
12
- await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
13
12
  await writeFile(
14
13
  path.join(appRoot, "package.json"),
15
14
  `${JSON.stringify({ name: "assistant-generator-test-app", private: true, type: "module" }, null, 2)}\n`,
@@ -25,17 +24,6 @@ async function withTempApp(run) {
25
24
  },
26
25
  assistantSurfaces: {}
27
26
  };
28
- `,
29
- "utf8"
30
- );
31
- await writeFile(
32
- path.join(appRoot, "src", "components", "ShellLayout.vue"),
33
- `<template>
34
- <div>
35
- <ShellOutlet host="shell-layout" position="primary-menu" />
36
- <ShellOutlet host="shell-layout" position="top-right" default />
37
- </div>
38
- </template>
39
27
  `,
40
28
  "utf8"
41
29
  );
@@ -46,29 +34,36 @@ async function withTempApp(run) {
46
34
  }
47
35
  }
48
36
 
49
- test("buildTemplateContext derives per-surface placeholders from explicit surfaces", async () => {
37
+ test("buildTemplateContext derives assistant setup placeholders from explicit setup surfaces", async () => {
50
38
  await withTempApp(async (appRoot) => {
51
39
  const context = await buildTemplateContext({
52
40
  appRoot,
53
41
  options: {
54
42
  surface: "app",
55
43
  "settings-surface": "console",
56
- "config-scope": "global",
57
- placement: "shell-layout:primary-menu",
58
- "menu-label": "Copilot"
44
+ "config-scope": "global"
59
45
  }
60
46
  });
61
47
 
62
- assert.equal(context.__ASSISTANT_SURFACE_ID__, "app");
63
48
  assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_ID__, "console");
64
49
  assert.equal(context.__ASSISTANT_CONFIG_SCOPE__, "global");
65
- assert.equal(context.__ASSISTANT_SETTINGS_HOST__, "console-settings");
66
50
  assert.equal(context.__ASSISTANT_AI_CONFIG_PREFIX__, "APP_ASSISTANT");
67
- assert.equal(context.__ASSISTANT_MENU_PLACEMENT_HOST__, "shell-layout");
68
- assert.equal(context.__ASSISTANT_MENU_PLACEMENT_POSITION__, "primary-menu");
69
- assert.equal(context.__ASSISTANT_MENU_LABEL__, "Copilot");
70
- assert.equal(context.__ASSISTANT_SETTINGS_MENU_WORKSPACE_SUFFIX__, "/settings/assistant");
71
- assert.equal(context.__ASSISTANT_SETTINGS_MENU_NON_WORKSPACE_SUFFIX__, "/settings/assistant");
51
+ });
52
+ });
53
+
54
+ test("buildTemplateContext honors explicit AI config prefix overrides", async () => {
55
+ await withTempApp(async (appRoot) => {
56
+ const context = await buildTemplateContext({
57
+ appRoot,
58
+ options: {
59
+ surface: "app",
60
+ "settings-surface": "console",
61
+ "config-scope": "global",
62
+ "ai-config-prefix": "CUSTOM_ASSISTANT"
63
+ }
64
+ });
65
+
66
+ assert.equal(context.__ASSISTANT_AI_CONFIG_PREFIX__, "CUSTOM_ASSISTANT");
72
67
  });
73
68
  });
74
69
 
@@ -81,8 +76,7 @@ test("buildTemplateContext rejects workspace config scope for a non-workspace as
81
76
  options: {
82
77
  surface: "app",
83
78
  "settings-surface": "console",
84
- "config-scope": "workspace",
85
- placement: "shell-layout:primary-menu"
79
+ "config-scope": "workspace"
86
80
  }
87
81
  }),
88
82
  /config-scope "workspace" requires surface "app" with requiresWorkspace=true/
@@ -117,8 +111,7 @@ test("buildTemplateContext rejects duplicate assistant surfaces already configur
117
111
  options: {
118
112
  surface: "app",
119
113
  "settings-surface": "console",
120
- "config-scope": "global",
121
- placement: "shell-layout:primary-menu"
114
+ "config-scope": "global"
122
115
  }
123
116
  }),
124
117
  /already has an assistant configured/