@vellumai/assistant 0.10.9-staging.1 → 0.10.9-staging.2

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.
Files changed (100) hide show
  1. package/AGENTS.md +4 -0
  2. package/knip.json +1 -4
  3. package/openapi.yaml +28 -18
  4. package/package.json +1 -1
  5. package/src/__tests__/always-loaded-tools-guard.test.ts +4 -6
  6. package/src/__tests__/app-routes-csp.test.ts +13 -4
  7. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +4 -8
  8. package/src/__tests__/disk-pressure-tools.test.ts +4 -4
  9. package/src/__tests__/file-list-tool.test.ts +22 -12
  10. package/src/__tests__/list-all-apps.test.ts +204 -0
  11. package/src/__tests__/messaging-send-tool.test.ts +67 -0
  12. package/src/__tests__/mtime-cache.test.ts +42 -0
  13. package/src/__tests__/oauth-provider-profiles.test.ts +3 -2
  14. package/src/__tests__/outlook-messaging-provider.test.ts +71 -0
  15. package/src/__tests__/plugin-app-serve-routes.test.ts +163 -0
  16. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  17. package/src/__tests__/scaffold-managed-skill-tool.test.ts +47 -0
  18. package/src/__tests__/subagent-notify-parent.test.ts +4 -3
  19. package/src/__tests__/subagent-tool-filtering.test.ts +21 -21
  20. package/src/__tests__/subagent-tool-gate-mode.test.ts +6 -6
  21. package/src/apps/app-store.ts +302 -2
  22. package/src/cli/AGENTS.md +17 -0
  23. package/src/cli/commands/__tests__/gateway.test.ts +55 -0
  24. package/src/cli/commands/apps.help.ts +17 -11
  25. package/src/cli/commands/apps.ts +5 -15
  26. package/src/cli/commands/gateway.help.ts +30 -0
  27. package/src/cli/commands/gateway.ts +40 -1
  28. package/src/cli/commands/platform/__tests__/status.test.ts +0 -90
  29. package/src/cli/commands/platform/index.help.ts +3 -2
  30. package/src/cli/commands/platform/index.ts +0 -9
  31. package/src/cli/commands/routes.help.ts +13 -7
  32. package/src/cli/commands/routes.ts +5 -3
  33. package/src/cli/commands/status.help.ts +24 -0
  34. package/src/cli/commands/status.ts +29 -0
  35. package/src/cli/lib/__tests__/global-json-option.test.ts +97 -0
  36. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +1 -0
  37. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +31 -0
  38. package/src/cli/lib/bundled-marketplace.json +187 -173
  39. package/src/cli/lib/global-json-option.ts +41 -0
  40. package/src/cli/lib/plugin-fingerprint.ts +18 -7
  41. package/src/cli/program.ts +5 -0
  42. package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
  43. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -19
  44. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  45. package/src/config/feature-flag-registry.json +1 -9
  46. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +3 -6
  47. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +3 -6
  48. package/src/daemon/__tests__/conversation-tool-setup.test.ts +8 -5
  49. package/src/daemon/conversation-tool-setup.ts +5 -90
  50. package/src/daemon/conversation.ts +2 -2
  51. package/src/messaging/provider-types.ts +9 -0
  52. package/src/messaging/providers/outlook/adapter.ts +17 -2
  53. package/src/messaging/providers/outlook/client.ts +11 -2
  54. package/src/messaging/providers/outlook/types.ts +9 -0
  55. package/src/monitoring/__tests__/plugin-source-watch.test.ts +37 -0
  56. package/src/monitoring/plugin-source-watch.ts +53 -0
  57. package/src/oauth/seed-providers.ts +6 -0
  58. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +3 -0
  59. package/src/persistence/embeddings/messages-lexical-index.ts +11 -0
  60. package/src/plugin-api/conversation-turn.ts +234 -0
  61. package/src/plugin-api/index.ts +12 -0
  62. package/src/plugins/__tests__/source-fingerprint.test.ts +41 -0
  63. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +48 -0
  64. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +46 -5
  65. package/src/plugins/defaults/memory/graph/tool-handlers.ts +2 -2
  66. package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +81 -2
  67. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +80 -33
  68. package/src/plugins/defaults/memory/memory-retrospective-job.ts +40 -1
  69. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +28 -2
  70. package/src/plugins/mtime-cache.ts +26 -13
  71. package/src/plugins/plugin-tree-walk.ts +30 -0
  72. package/src/plugins/source-fingerprint.ts +9 -2
  73. package/src/plugins/surface-import.ts +28 -0
  74. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +1 -1
  75. package/src/providers/speech-to-text/__tests__/vellum-speech-relay-connection.test.ts +4 -1
  76. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +5 -0
  77. package/src/runtime/routes/__tests__/gateway-status-routes.test.ts +84 -0
  78. package/src/runtime/routes/__tests__/plugins-routes.test.ts +47 -3
  79. package/src/runtime/routes/__tests__/user-routes-cli.test.ts +154 -0
  80. package/src/runtime/routes/app-management-routes.ts +96 -32
  81. package/src/runtime/routes/app-routes.ts +33 -17
  82. package/src/runtime/routes/gateway-status-routes.ts +69 -0
  83. package/src/runtime/routes/index.ts +2 -0
  84. package/src/runtime/routes/platform-routes.ts +4 -14
  85. package/src/runtime/routes/plugins-routes.ts +58 -1
  86. package/src/runtime/routes/settings-routes.ts +8 -6
  87. package/src/runtime/routes/user-route-dispatcher.ts +6 -86
  88. package/src/runtime/routes/user-route-resolution.ts +141 -0
  89. package/src/runtime/routes/user-routes-cli.ts +56 -35
  90. package/src/telemetry/AGENTS.md +15 -0
  91. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +258 -0
  92. package/src/telemetry/telemetry-wire-source.json +1 -1
  93. package/src/telemetry/telemetry-wire-validation.test.ts +157 -0
  94. package/src/telemetry/telemetry-wire-validation.ts +125 -0
  95. package/src/telemetry/telemetry-wire.generated.ts +60 -9
  96. package/src/telemetry/types.test.ts +38 -0
  97. package/src/telemetry/types.ts +183 -91
  98. package/src/telemetry/usage-telemetry-reporter.ts +5 -0
  99. package/src/telemetry/watchdog-direct-emit.ts +6 -0
  100. package/src/tools/skills/scaffold-managed.ts +43 -11
@@ -18,7 +18,6 @@ let mockResponse: unknown = {
18
18
  available: false,
19
19
  organizationId: null,
20
20
  userId: null,
21
- velayTunnel: null,
22
21
  },
23
22
  };
24
23
 
@@ -63,7 +62,6 @@ describe("assistant platform status", () => {
63
62
  available: false,
64
63
  organizationId: null,
65
64
  userId: null,
66
- velayTunnel: null,
67
65
  },
68
66
  };
69
67
  process.exitCode = 0;
@@ -81,7 +79,6 @@ describe("assistant platform status", () => {
81
79
  available: true,
82
80
  organizationId: "org-456",
83
81
  userId: "user-789",
84
- velayTunnel: null,
85
82
  },
86
83
  };
87
84
 
@@ -116,93 +113,6 @@ describe("assistant platform status", () => {
116
113
  expect(parsed.available).toBe(true);
117
114
  expect(parsed.organizationId).toBe("org-456");
118
115
  expect(parsed.userId).toBe("user-789");
119
- expect(parsed.velayTunnel).toBeNull();
120
- });
121
-
122
- test("velayTunnel connected with publicUrl is returned when gateway is live", async () => {
123
- mockResponse = {
124
- ok: true,
125
- result: {
126
- isPlatform: false,
127
- baseUrl: "",
128
- assistantId: "",
129
- hasAssistantApiKey: false,
130
- hasWebhookSecret: false,
131
- available: false,
132
- organizationId: null,
133
- userId: null,
134
- velayTunnel: {
135
- connected: true,
136
- publicUrl: "https://abc123.vellum.ai",
137
- },
138
- },
139
- };
140
-
141
- const program = buildProgram();
142
- const stdoutChunks: string[] = [];
143
- const origWrite = process.stdout.write.bind(process.stdout);
144
- process.stdout.write = ((chunk: unknown) => {
145
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
146
- return true;
147
- }) as typeof process.stdout.write;
148
-
149
- try {
150
- await program.parseAsync([
151
- "node",
152
- "assistant",
153
- "platform",
154
- "status",
155
- "--json",
156
- ]);
157
- } finally {
158
- process.stdout.write = origWrite;
159
- }
160
-
161
- const parsed = JSON.parse(stdoutChunks.join(""));
162
- expect(parsed.velayTunnel).toEqual({
163
- connected: true,
164
- publicUrl: "https://abc123.vellum.ai",
165
- });
166
- });
167
-
168
- test("velayTunnel disconnected when gateway reports no active connection", async () => {
169
- mockResponse = {
170
- ok: true,
171
- result: {
172
- isPlatform: false,
173
- baseUrl: "",
174
- assistantId: "",
175
- hasAssistantApiKey: false,
176
- hasWebhookSecret: false,
177
- available: false,
178
- organizationId: null,
179
- userId: null,
180
- velayTunnel: { connected: false, publicUrl: null },
181
- },
182
- };
183
-
184
- const program = buildProgram();
185
- const stdoutChunks: string[] = [];
186
- const origWrite = process.stdout.write.bind(process.stdout);
187
- process.stdout.write = ((chunk: unknown) => {
188
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
189
- return true;
190
- }) as typeof process.stdout.write;
191
-
192
- try {
193
- await program.parseAsync([
194
- "node",
195
- "assistant",
196
- "platform",
197
- "status",
198
- "--json",
199
- ]);
200
- } finally {
201
- process.stdout.write = origWrite;
202
- }
203
-
204
- const parsed = JSON.parse(stdoutChunks.join(""));
205
- expect(parsed.velayTunnel).toEqual({ connected: false, publicUrl: null });
206
116
  });
207
117
 
208
118
  test("plain text mode does not emit JSON to stdout", async () => {
@@ -58,8 +58,9 @@ Fields:
58
58
  available Whether callback registration prerequisites are satisfied
59
59
  organizationId The platform organization ID (from stored credentials)
60
60
  userId The platform user ID (from stored credentials)
61
- velayTunnel Live Velay tunnel status from the gateway IPC socket
62
- (null when the gateway is not running)
61
+
62
+ For the Velay tunnel state (only relevant for Twilio webhooks and live
63
+ voice/audio), use 'assistant gateway status' instead.
63
64
 
64
65
  Examples:
65
66
  $ assistant platform status
@@ -24,7 +24,6 @@ interface PlatformStatusResult {
24
24
  available: boolean;
25
25
  organizationId: string | null;
26
26
  userId: string | null;
27
- velayTunnel: { connected: boolean; publicUrl: string | null } | null;
28
27
  }
29
28
 
30
29
  interface PlatformCreditsResult {
@@ -87,14 +86,6 @@ export function registerPlatformCommand(program: Command): void {
87
86
  `Organization ID: ${result.organizationId || "(not set)"}`,
88
87
  );
89
88
  log.info(`User ID: ${result.userId || "(not set)"}`);
90
- if (result.velayTunnel !== null) {
91
- const tunnelState = result.velayTunnel.connected
92
- ? `connected${result.velayTunnel.publicUrl ? ` (${result.velayTunnel.publicUrl})` : ""}`
93
- : "disconnected";
94
- log.info(`Velay tunnel: ${tunnelState}`);
95
- } else {
96
- log.info(`Velay tunnel: (gateway not running)`);
97
- }
98
89
  }
99
90
  },
100
91
  );
@@ -8,8 +8,10 @@ export const routesHelp: CliCommandHelp = {
8
8
  "Manage user-defined authenticated HTTP route handlers under /x/*",
9
9
  helpText: `
10
10
  User-defined routes let you expose custom HTTP endpoints by dropping handler
11
- files into /workspace/routes/. Each file exports named HTTP method functions
12
- (GET, POST, etc.) and becomes reachable at /x/<path>.
11
+ files into /workspace/routes/ (reachable at /x/<path>) or into a plugin's
12
+ /workspace/plugins/<name>/routes/ (reachable in that plugin's namespace at
13
+ /x/plugins/<name>/<path>). Each file exports named HTTP method functions
14
+ (GET, POST, etc.).
13
15
 
14
16
  These routes require edge authentication — they are intended for
15
17
  assistant-internal or user-facing endpoints, not for unauthenticated provider
@@ -21,7 +23,8 @@ needed.
21
23
  Examples:
22
24
  $ assistant routes list
23
25
  $ assistant routes list --json
24
- $ assistant routes inspect my-dashboard-api/submit`,
26
+ $ assistant routes inspect my-dashboard-api/submit
27
+ $ assistant routes inspect plugins/my-plugin/status`,
25
28
  subcommands: [
26
29
  {
27
30
  name: "list",
@@ -33,8 +36,9 @@ Examples:
33
36
  },
34
37
  ],
35
38
  helpText: `
36
- Scans /workspace/routes/ for handler files (.ts, .js) and displays the route
37
- path, exported HTTP methods, optional description, and file location.
39
+ Scans /workspace/routes/ and each enabled plugin's /workspace/plugins/<name>/routes/
40
+ for handler files (.ts, .js) and displays the route path, exported HTTP
41
+ methods, optional description, and file location.
38
42
 
39
43
  Examples:
40
44
  $ assistant routes list
@@ -52,14 +56,16 @@ Examples:
52
56
  ],
53
57
  helpText: `
54
58
  Arguments:
55
- path Route path relative to /x/ (e.g. "my-dashboard-api/submit").
56
- Do not include the /x/ prefix.
59
+ path Route path relative to /x/ (e.g. "my-dashboard-api/submit" or
60
+ "plugins/my-plugin/status"). The /x/ prefix is optional, so a path
61
+ copied straight from 'routes list' works too.
57
62
 
58
63
  Loads the handler file and displays exported methods, description, file path,
59
64
  public URL, file size, and last modified time.
60
65
 
61
66
  Examples:
62
67
  $ assistant routes inspect my-dashboard-api/submit
68
+ $ assistant routes inspect plugins/my-plugin/status
63
69
  $ assistant routes inspect items --json`,
64
70
  },
65
71
  ],
@@ -68,7 +68,9 @@ export function registerRoutesCommand(program: Command): void {
68
68
  }
69
69
 
70
70
  if (discovered.length === 0) {
71
- log.info("No route handlers found in /workspace/routes/.");
71
+ log.info(
72
+ "No route handlers found in /workspace/routes/ or /workspace/plugins/<name>/routes/.",
73
+ );
72
74
  log.info(
73
75
  "Create a .ts or .js file exporting named HTTP method functions (GET, POST, etc.).",
74
76
  );
@@ -108,7 +110,7 @@ export function registerRoutesCommand(program: Command): void {
108
110
  route.routePath.padEnd(routeWidth),
109
111
  formatMethods(route.methods).padEnd(methodsWidth),
110
112
  (route.description ?? "").padEnd(descWidth),
111
- `routes/${route.filePath}`,
113
+ route.filePath,
112
114
  ].join(" ");
113
115
  log.info(` ${cols}`);
114
116
  }
@@ -130,7 +132,7 @@ export function registerRoutesCommand(program: Command): void {
130
132
  async (routePath: string, opts: { json?: boolean }) => {
131
133
  const r = await cliIpcCall<{ route: InspectedRoute }>(
132
134
  "user_routes_inspect",
133
- { path: routePath },
135
+ { body: { path: routePath } },
134
136
  );
135
137
  if (!r.ok) {
136
138
  if (opts.json) {
@@ -5,4 +5,28 @@ import type { CliCommandHelp } from "../lib/cli-command-help.js";
5
5
  export const statusHelp: CliCommandHelp = {
6
6
  name: "status",
7
7
  description: "Show assistant version, workspace, and runtime health",
8
+ options: [
9
+ {
10
+ flags: "--json",
11
+ description: "Emit the status as machine-readable JSON",
12
+ },
13
+ ],
14
+ helpText: `
15
+ Behavior:
16
+ Reports the running assistant's version, workspace path, and memory/disk
17
+ usage. When the assistant is unreachable, falls back to the installed CLI
18
+ version and whether the assistant socket is present.
19
+
20
+ With --json, the same fields are emitted as a single JSON object:
21
+ - reachable: whether the assistant answered the health check
22
+ - cliVersion: the installed CLI version
23
+ - assistantVersion: the running assistant version (null when unreachable)
24
+ - versionStale: true when the CLI and assistant versions differ
25
+ - workspace: the workspace directory
26
+ - memory / disk: usage in MB (null when unreachable)
27
+
28
+ Examples:
29
+ $ assistant status
30
+ $ assistant status --json
31
+ $ assistant status --json | jq '.assistantVersion'`,
8
32
  };
@@ -8,6 +8,7 @@ import { getWorkspaceDirDisplay } from "../../util/platform.js";
8
8
  import { APP_VERSION } from "../../version.js";
9
9
  import { applyCommandHelp } from "../lib/cli-command-help.js";
10
10
  import { registerCommand } from "../lib/register-command.js";
11
+ import { shouldOutputJson, writeOutput } from "../output.js";
11
12
  import { statusHelp } from "./status.help.js";
12
13
 
13
14
  interface HealthResponse {
@@ -30,6 +31,7 @@ export function registerStatusCommand(program: Command): void {
30
31
  applyCommandHelp(cmd, statusHelp);
31
32
 
32
33
  cmd.action(async () => {
34
+ const json = shouldOutputJson(cmd);
33
35
  const result = await cliIpcCall<HealthResponse>("health");
34
36
 
35
37
  if (!result.ok) {
@@ -41,6 +43,19 @@ export function registerStatusCommand(program: Command): void {
41
43
  const socketPath = getAssistantSocketPath();
42
44
  const socketExists = existsSync(socketPath);
43
45
  const workspace = getWorkspaceDirDisplay();
46
+ if (json) {
47
+ writeOutput(cmd, {
48
+ reachable: false,
49
+ cliVersion: APP_VERSION,
50
+ assistantVersion: null,
51
+ versionStale: false,
52
+ running: socketExists,
53
+ workspace,
54
+ memory: null,
55
+ disk: null,
56
+ });
57
+ process.exit(0);
58
+ }
44
59
  // Daemon unreachable, so its runtime version is unknown; show the
45
60
  // installed CLI version so there's always one reliable number.
46
61
  process.stdout.write(`CLI Version: ${APP_VERSION}\n`);
@@ -62,6 +77,20 @@ export function registerStatusCommand(program: Command): void {
62
77
  const h = result.result;
63
78
  const workspace = getWorkspaceDirDisplay();
64
79
 
80
+ if (json) {
81
+ writeOutput(cmd, {
82
+ reachable: true,
83
+ cliVersion: APP_VERSION,
84
+ assistantVersion: h.version,
85
+ versionStale: h.version !== APP_VERSION,
86
+ running: true,
87
+ workspace,
88
+ memory: h.memory,
89
+ disk: h.disk,
90
+ });
91
+ return;
92
+ }
93
+
65
94
  // h.version is the running runtime; APP_VERSION is the installed CLI.
66
95
  // They drift mid-upgrade (CLI bumped, daemon not yet restarted).
67
96
  const runtimeVersion =
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { Command } from "commander";
4
+
5
+ import { registerGlobalJsonOption } from "../global-json-option.js";
6
+
7
+ function hasJsonOption(cmd: Command): boolean {
8
+ return cmd.options.some((opt) => opt.long === "--json");
9
+ }
10
+
11
+ describe("registerGlobalJsonOption", () => {
12
+ it("adds --json to leaf commands (including nested leaves)", () => {
13
+ const program = new Command();
14
+ program.name("assistant");
15
+ const memory = program.command("memory");
16
+ const items = memory.command("items");
17
+ const list = items.command("list");
18
+ const status = program.command("status");
19
+
20
+ registerGlobalJsonOption(program);
21
+
22
+ expect(hasJsonOption(list)).toBe(true);
23
+ expect(hasJsonOption(status)).toBe(true);
24
+ });
25
+
26
+ it("does NOT add --json to group commands (would steal the flag from subcommands)", () => {
27
+ const program = new Command();
28
+ program.name("assistant");
29
+ const clients = program.command("clients");
30
+ const list = clients.command("list");
31
+
32
+ registerGlobalJsonOption(program);
33
+
34
+ // Commander consumes a recognized option at the outermost declaring
35
+ // command, so a group-level --json would swallow `clients list --json`
36
+ // before the leaf action reads it.
37
+ expect(hasJsonOption(clients)).toBe(false);
38
+ expect(hasJsonOption(list)).toBe(true);
39
+ });
40
+
41
+ it("keeps `<group> <leaf> --json` bound to the leaf's own opts", () => {
42
+ const program = new Command();
43
+ program.name("assistant").exitOverride();
44
+ const clients = program.command("clients");
45
+ let leafJson: unknown;
46
+ clients.command("list").action((opts: { json?: boolean }) => {
47
+ leafJson = opts.json;
48
+ });
49
+
50
+ registerGlobalJsonOption(program);
51
+
52
+ program.parse(["node", "assistant", "clients", "list", "--json"]);
53
+ expect(leafJson).toBe(true);
54
+ });
55
+
56
+ it("does not duplicate --json on a command that already declares it", () => {
57
+ const program = new Command();
58
+ program.name("assistant");
59
+ const sub = program.command("nodes");
60
+ sub.option("--json", "Machine-readable compact JSON output");
61
+
62
+ registerGlobalJsonOption(program);
63
+
64
+ const jsonOptions = sub.options.filter((opt) => opt.long === "--json");
65
+ expect(jsonOptions).toHaveLength(1);
66
+ // The original description is preserved, not overwritten.
67
+ expect(jsonOptions[0]!.description).toBe(
68
+ "Machine-readable compact JSON output",
69
+ );
70
+ });
71
+
72
+ it("leaves the root program without a --json option", () => {
73
+ const program = new Command();
74
+ program.name("assistant");
75
+ program.command("status");
76
+
77
+ registerGlobalJsonOption(program);
78
+
79
+ expect(hasJsonOption(program)).toBe(false);
80
+ });
81
+
82
+ it("makes --json parse without error on a command that lacked it", () => {
83
+ const program = new Command();
84
+ program.name("assistant").exitOverride();
85
+ let seenJson: unknown;
86
+ program.command("status").action((opts: { json?: boolean }) => {
87
+ seenJson = opts.json;
88
+ });
89
+
90
+ registerGlobalJsonOption(program);
91
+
92
+ expect(() =>
93
+ program.parse(["node", "assistant", "status", "--json"]),
94
+ ).not.toThrow();
95
+ expect(seenJson).toBe(true);
96
+ });
97
+ });
@@ -37,6 +37,7 @@ describe("buildBundledPluginCatalog", () => {
37
37
  category: "productivity",
38
38
  homepage: "https://github.com/JuliusBrussee/caveman",
39
39
  license: "MIT",
40
+ icon: "🦴",
40
41
  source: {
41
42
  kind: "github",
42
43
  repo: "JuliusBrussee/caveman",
@@ -243,3 +243,34 @@ describe("computeContentHash", () => {
243
243
  expect(second).not.toBe(first);
244
244
  });
245
245
  });
246
+
247
+ describe("generated app build output is excluded", () => {
248
+ test("apps/<app>/dist is not fingerprinted, so a build is never drift", () => {
249
+ // Installed source: an app with a src/ entry point (no dist yet).
250
+ write("apps/dash/src/main.tsx", "export default 1;");
251
+ write("hooks/stop.ts", "export default () => 1;");
252
+ const baseline = computeFingerprint(root);
253
+ expect(Object.keys(baseline.files)).toContain("apps/dash/src/main.tsx");
254
+
255
+ // The watcher later compiles src -> apps/dash/dist. That generated output
256
+ // must not surface as drift (added files) against the pinned commit.
257
+ write("apps/dash/dist/main.js", "console.log(1)");
258
+ write("apps/dash/dist/index.html", "<html></html>");
259
+ const cmp = compareFingerprint(root, baseline);
260
+ expect(cmp.clean).toBe(true);
261
+ expect(cmp.added).toEqual([]);
262
+
263
+ // A plugin's own top-level dist/ is still tracked (not an app build dir).
264
+ write("dist/bundle.js", "x");
265
+ expect(compareFingerprint(root, baseline).added).toEqual([
266
+ "dist/bundle.js",
267
+ ]);
268
+ });
269
+
270
+ test("content hash ignores apps/<app>/dist", () => {
271
+ write("apps/dash/src/main.tsx", "export default 1;");
272
+ const before = computeContentHash(root);
273
+ write("apps/dash/dist/main.js", "console.log(1)");
274
+ expect(computeContentHash(root)).toBe(before);
275
+ });
276
+ });