@stigmer/runner 3.12.3 → 3.12.5

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 (227) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-http.js +12 -0
  3. package/dist/activities/call-http.js.map +1 -1
  4. package/dist/activities/call-llm.d.ts +18 -0
  5. package/dist/activities/call-llm.js +56 -2
  6. package/dist/activities/call-llm.js.map +1 -1
  7. package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
  8. package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
  9. package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
  10. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  11. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  12. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  13. package/dist/activities/execute-cursor/index.d.ts +10 -1
  14. package/dist/activities/execute-cursor/index.js +108 -57
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  17. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  18. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  19. package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
  20. package/dist/activities/execute-cursor/service-tier.js +5 -21
  21. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  23. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  24. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  25. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  27. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  28. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  29. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  30. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  31. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/index.js +57 -5
  33. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  34. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  35. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  37. package/dist/activities/execute-deep-agent/setup.js +31 -16
  38. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
  40. package/dist/activities/execute-deep-agent/shell-env.js +10 -7
  41. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  43. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  45. package/dist/config.js +10 -5
  46. package/dist/config.js.map +1 -1
  47. package/dist/encryption/config.js +7 -2
  48. package/dist/encryption/config.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/main.js +12 -6
  52. package/dist/main.js.map +1 -1
  53. package/dist/middleware/path-normalization.d.ts +49 -49
  54. package/dist/middleware/path-normalization.js +73 -84
  55. package/dist/middleware/path-normalization.js.map +1 -1
  56. package/dist/middleware/types.d.ts +3 -3
  57. package/dist/payload-codecs.js +2 -1
  58. package/dist/payload-codecs.js.map +1 -1
  59. package/dist/runner-manager.d.ts +1 -1
  60. package/dist/runner-manager.js +42 -22
  61. package/dist/runner-manager.js.map +1 -1
  62. package/dist/runner.js +30 -6
  63. package/dist/runner.js.map +1 -1
  64. package/dist/shared/args-preview.d.ts +8 -0
  65. package/dist/shared/args-preview.js +14 -3
  66. package/dist/shared/args-preview.js.map +1 -1
  67. package/dist/shared/artifact-storage.d.ts +11 -1
  68. package/dist/shared/artifact-storage.js +10 -1
  69. package/dist/shared/artifact-storage.js.map +1 -1
  70. package/dist/shared/declared-preferences.d.ts +47 -0
  71. package/dist/shared/declared-preferences.js +64 -0
  72. package/dist/shared/declared-preferences.js.map +1 -0
  73. package/dist/shared/fingerprint-secret.d.ts +3 -2
  74. package/dist/shared/fingerprint-secret.js +5 -3
  75. package/dist/shared/fingerprint-secret.js.map +1 -1
  76. package/dist/shared/llm-backend.js +8 -1
  77. package/dist/shared/llm-backend.js.map +1 -1
  78. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  79. package/dist/shared/mcp-enabled-tools.js +2 -3
  80. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  81. package/dist/shared/mcp-resolver.d.ts +20 -15
  82. package/dist/shared/mcp-resolver.js +11 -12
  83. package/dist/shared/mcp-resolver.js.map +1 -1
  84. package/dist/shared/model-client.d.ts +15 -0
  85. package/dist/shared/model-client.js +57 -13
  86. package/dist/shared/model-client.js.map +1 -1
  87. package/dist/shared/placeholder-resolver.d.ts +9 -2
  88. package/dist/shared/placeholder-resolver.js +9 -2
  89. package/dist/shared/placeholder-resolver.js.map +1 -1
  90. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  91. package/dist/shared/plan-mode-permissions.js +27 -55
  92. package/dist/shared/plan-mode-permissions.js.map +1 -1
  93. package/dist/shared/registry-endpoint.d.ts +5 -0
  94. package/dist/shared/registry-endpoint.js +7 -1
  95. package/dist/shared/registry-endpoint.js.map +1 -1
  96. package/dist/shared/runner-credential-keys.d.ts +26 -1
  97. package/dist/shared/runner-credential-keys.js +34 -1
  98. package/dist/shared/runner-credential-keys.js.map +1 -1
  99. package/dist/shared/runner-credential-store.d.ts +77 -0
  100. package/dist/shared/runner-credential-store.js +111 -0
  101. package/dist/shared/runner-credential-store.js.map +1 -0
  102. package/dist/shared/service-tier.d.ts +55 -0
  103. package/dist/shared/service-tier.js +67 -0
  104. package/dist/shared/service-tier.js.map +1 -0
  105. package/dist/shared/skill-mount.d.ts +89 -0
  106. package/dist/shared/skill-mount.js +142 -0
  107. package/dist/shared/skill-mount.js.map +1 -0
  108. package/dist/shared/skill-writer.d.ts +28 -26
  109. package/dist/shared/skill-writer.js +79 -102
  110. package/dist/shared/skill-writer.js.map +1 -1
  111. package/dist/shared/worker-shutdown.d.ts +67 -0
  112. package/dist/shared/worker-shutdown.js +79 -0
  113. package/dist/shared/worker-shutdown.js.map +1 -0
  114. package/dist/shared/workspace/types.d.ts +3 -2
  115. package/dist/shared/zip-extract.d.ts +10 -3
  116. package/dist/shared/zip-extract.js +10 -3
  117. package/dist/shared/zip-extract.js.map +1 -1
  118. package/dist/workflow-engine/loader.js +38 -1
  119. package/dist/workflow-engine/loader.js.map +1 -1
  120. package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
  121. package/dist/workflow-engine/tasks/call-function.js +49 -5
  122. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  123. package/dist/workflow-engine/tasks/human-input.js +23 -2
  124. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  125. package/dist/workflow-engine/types.d.ts +6 -0
  126. package/dist/workflow-engine/types.js.map +1 -1
  127. package/dist/workflows/engine-core.js +36 -8
  128. package/dist/workflows/engine-core.js.map +1 -1
  129. package/package.json +15 -11
  130. package/src/activities/__tests__/call-http.test.ts +36 -0
  131. package/src/activities/__tests__/call-llm.test.ts +77 -0
  132. package/src/activities/call-http.ts +17 -0
  133. package/src/activities/call-llm.ts +78 -2
  134. package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
  135. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  136. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  137. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
  138. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  139. package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
  140. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  141. package/src/activities/execute-cursor/index.ts +129 -55
  142. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  143. package/src/activities/execute-cursor/service-tier.ts +5 -29
  144. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  145. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  146. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  147. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  148. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  149. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  150. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  151. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  152. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  153. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  154. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  155. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  156. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  157. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  158. package/src/activities/execute-deep-agent/index.ts +57 -5
  159. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  160. package/src/activities/execute-deep-agent/setup.ts +36 -19
  161. package/src/activities/execute-deep-agent/shell-env.ts +10 -7
  162. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  163. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  164. package/src/config.ts +10 -5
  165. package/src/encryption/config.ts +8 -2
  166. package/src/index.ts +1 -1
  167. package/src/main.ts +16 -6
  168. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  169. package/src/middleware/path-normalization.ts +78 -90
  170. package/src/middleware/types.ts +3 -3
  171. package/src/payload-codecs.ts +2 -1
  172. package/src/runner-manager.ts +55 -22
  173. package/src/runner.ts +39 -6
  174. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  175. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  176. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  177. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  178. package/src/shared/__tests__/model-client.test.ts +99 -0
  179. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  180. package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
  181. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  182. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  183. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  184. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  185. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  186. package/src/shared/__tests__/zip-extract.test.ts +46 -11
  187. package/src/shared/args-preview.ts +17 -3
  188. package/src/shared/artifact-storage.ts +20 -1
  189. package/src/shared/declared-preferences.ts +84 -0
  190. package/src/shared/fingerprint-secret.ts +5 -3
  191. package/src/shared/llm-backend.ts +7 -1
  192. package/src/shared/mcp-enabled-tools.ts +2 -3
  193. package/src/shared/mcp-resolver.ts +20 -20
  194. package/src/shared/model-client.ts +76 -13
  195. package/src/shared/placeholder-resolver.ts +9 -2
  196. package/src/shared/plan-mode-permissions.ts +27 -58
  197. package/src/shared/registry-endpoint.ts +9 -1
  198. package/src/shared/runner-credential-keys.ts +36 -1
  199. package/src/shared/runner-credential-store.ts +115 -0
  200. package/src/shared/service-tier.ts +78 -0
  201. package/src/shared/skill-mount.ts +179 -0
  202. package/src/shared/skill-writer.ts +96 -130
  203. package/src/shared/worker-shutdown.ts +99 -0
  204. package/src/shared/workspace/types.ts +3 -2
  205. package/src/shared/zip-extract.ts +14 -7
  206. package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
  207. package/src/workflow-engine/__tests__/loader.test.ts +51 -0
  208. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
  209. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
  210. package/src/workflow-engine/loader.ts +46 -1
  211. package/src/workflow-engine/tasks/call-function.ts +74 -13
  212. package/src/workflow-engine/tasks/human-input.ts +33 -5
  213. package/src/workflow-engine/types.ts +6 -0
  214. package/src/workflows/engine-core.ts +39 -8
  215. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  216. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  217. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  218. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  219. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  220. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  221. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  222. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  223. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  224. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  225. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  226. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  227. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -0,0 +1,220 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ cacheSessionAgent,
4
+ closeAllCachedAgents,
5
+ computeAgentFingerprint,
6
+ evictSessionAgent,
7
+ takeCachedAgent,
8
+ _parkedAgentCountForTests,
9
+ _resetAgentSessionCacheForTests,
10
+ type CacheableAgent,
11
+ } from "../agent-session-cache.js";
12
+
13
+ function makeAgent(agentId = "agent-1"): CacheableAgent & { close: ReturnType<typeof vi.fn> } {
14
+ return { agentId, close: vi.fn() };
15
+ }
16
+
17
+ const FP = "fingerprint-a";
18
+ const OTHER_FP = "fingerprint-b";
19
+
20
+ describe("agent-session-cache", () => {
21
+ beforeEach(() => {
22
+ _resetAgentSessionCacheForTests();
23
+ delete process.env.STIGMER_CURSOR_AGENT_CACHE_TTL_MS;
24
+ });
25
+
26
+ afterEach(() => {
27
+ _resetAgentSessionCacheForTests();
28
+ vi.useRealTimers();
29
+ });
30
+
31
+ describe("checkout semantics", () => {
32
+ it("park then take returns the same agent without closing it", () => {
33
+ const agent = makeAgent();
34
+ cacheSessionAgent("ses-1", agent, FP);
35
+
36
+ const taken = takeCachedAgent("ses-1", FP, "agent-1");
37
+
38
+ expect(taken).toBe(agent);
39
+ expect(agent.close).not.toHaveBeenCalled();
40
+ });
41
+
42
+ it("checkout is exclusive — a second take misses", () => {
43
+ cacheSessionAgent("ses-1", makeAgent(), FP);
44
+
45
+ expect(takeCachedAgent("ses-1", FP, "agent-1")).toBeDefined();
46
+ expect(takeCachedAgent("ses-1", FP, "agent-1")).toBeUndefined();
47
+ });
48
+
49
+ it("an empty expectedAgentId (first-execution shape) still matches", () => {
50
+ const agent = makeAgent();
51
+ cacheSessionAgent("ses-1", agent, FP);
52
+
53
+ expect(takeCachedAgent("ses-1", FP, "")).toBe(agent);
54
+ });
55
+
56
+ it("misses for a different session", () => {
57
+ cacheSessionAgent("ses-1", makeAgent(), FP);
58
+
59
+ expect(takeCachedAgent("ses-2", FP, "agent-1")).toBeUndefined();
60
+ expect(_parkedAgentCountForTests()).toBe(1);
61
+ });
62
+ });
63
+
64
+ describe("reuse guards", () => {
65
+ it("fingerprint drift closes the parked agent and misses", () => {
66
+ const agent = makeAgent();
67
+ cacheSessionAgent("ses-1", agent, FP);
68
+
69
+ const taken = takeCachedAgent("ses-1", OTHER_FP, "agent-1");
70
+
71
+ expect(taken).toBeUndefined();
72
+ expect(agent.close).toHaveBeenCalledTimes(1);
73
+ expect(_parkedAgentCountForTests()).toBe(0);
74
+ });
75
+
76
+ it("agentId mismatch (harnessStateId replaced elsewhere) closes and misses", () => {
77
+ const agent = makeAgent("agent-old");
78
+ cacheSessionAgent("ses-1", agent, FP);
79
+
80
+ const taken = takeCachedAgent("ses-1", FP, "agent-new");
81
+
82
+ expect(taken).toBeUndefined();
83
+ expect(agent.close).toHaveBeenCalledTimes(1);
84
+ });
85
+ });
86
+
87
+ describe("lifetime bounds", () => {
88
+ it("re-parking a session closes the displaced agent", () => {
89
+ const first = makeAgent("agent-1");
90
+ const second = makeAgent("agent-2");
91
+ cacheSessionAgent("ses-1", first, FP);
92
+ cacheSessionAgent("ses-1", second, FP);
93
+
94
+ expect(first.close).toHaveBeenCalledTimes(1);
95
+ expect(takeCachedAgent("ses-1", FP, "agent-2")).toBe(second);
96
+ });
97
+
98
+ it("idle TTL evicts and closes", () => {
99
+ vi.useFakeTimers();
100
+ process.env.STIGMER_CURSOR_AGENT_CACHE_TTL_MS = "1000";
101
+ const agent = makeAgent();
102
+ cacheSessionAgent("ses-1", agent, FP);
103
+
104
+ vi.advanceTimersByTime(1001);
105
+
106
+ expect(agent.close).toHaveBeenCalledTimes(1);
107
+ expect(_parkedAgentCountForTests()).toBe(0);
108
+ });
109
+
110
+ it("checkout before the TTL fires disarms the eviction timer", () => {
111
+ vi.useFakeTimers();
112
+ process.env.STIGMER_CURSOR_AGENT_CACHE_TTL_MS = "1000";
113
+ const agent = makeAgent();
114
+ cacheSessionAgent("ses-1", agent, FP);
115
+
116
+ const taken = takeCachedAgent("ses-1", FP, "agent-1");
117
+ vi.advanceTimersByTime(5000);
118
+
119
+ expect(taken).toBe(agent);
120
+ expect(agent.close).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it("the LRU cap closes the oldest parked agent", () => {
124
+ vi.useFakeTimers();
125
+ const first = makeAgent("agent-0");
126
+ cacheSessionAgent("ses-0", first, FP);
127
+ for (let i = 1; i < 32; i++) {
128
+ vi.advanceTimersByTime(1);
129
+ cacheSessionAgent(`ses-${i}`, makeAgent(`agent-${i}`), FP);
130
+ }
131
+
132
+ vi.advanceTimersByTime(1);
133
+ cacheSessionAgent("ses-32", makeAgent("agent-32"), FP);
134
+
135
+ expect(first.close).toHaveBeenCalledTimes(1);
136
+ expect(_parkedAgentCountForTests()).toBe(32);
137
+ });
138
+
139
+ it("an empty sessionId is never parked — the lease releases immediately", () => {
140
+ const agent = makeAgent();
141
+ cacheSessionAgent("", agent, FP);
142
+
143
+ expect(agent.close).toHaveBeenCalledTimes(1);
144
+ expect(_parkedAgentCountForTests()).toBe(0);
145
+ });
146
+ });
147
+
148
+ describe("explicit release", () => {
149
+ it("evictSessionAgent closes the parked agent", () => {
150
+ const agent = makeAgent();
151
+ cacheSessionAgent("ses-1", agent, FP);
152
+
153
+ evictSessionAgent("ses-1");
154
+
155
+ expect(agent.close).toHaveBeenCalledTimes(1);
156
+ expect(_parkedAgentCountForTests()).toBe(0);
157
+ });
158
+
159
+ it("closeAllCachedAgents closes everything (worker shutdown)", () => {
160
+ const a = makeAgent("agent-a");
161
+ const b = makeAgent("agent-b");
162
+ cacheSessionAgent("ses-a", a, FP);
163
+ cacheSessionAgent("ses-b", b, FP);
164
+
165
+ closeAllCachedAgents();
166
+
167
+ expect(a.close).toHaveBeenCalledTimes(1);
168
+ expect(b.close).toHaveBeenCalledTimes(1);
169
+ expect(_parkedAgentCountForTests()).toBe(0);
170
+ });
171
+
172
+ it("a close() that throws never breaks eviction", () => {
173
+ const agent = makeAgent();
174
+ agent.close.mockImplementation(() => {
175
+ throw new Error("already disposed");
176
+ });
177
+ cacheSessionAgent("ses-1", agent, FP);
178
+
179
+ expect(() => evictSessionAgent("ses-1")).not.toThrow();
180
+ expect(_parkedAgentCountForTests()).toBe(0);
181
+ });
182
+ });
183
+
184
+ describe("computeAgentFingerprint", () => {
185
+ const baseOptions = {
186
+ apiKey: "sk-secret-key",
187
+ model: "gpt-5",
188
+ modelParams: [{ key: "tier", value: "standard" }],
189
+ workspaceDirs: ["/workspace/app"],
190
+ mcpServers: { github: { command: "npx", env: { TOKEN: "t-1" } } },
191
+ };
192
+
193
+ it("is stable across key ordering", () => {
194
+ const reordered = {
195
+ mcpServers: { github: { env: { TOKEN: "t-1" }, command: "npx" } },
196
+ workspaceDirs: ["/workspace/app"],
197
+ modelParams: [{ key: "tier", value: "standard" }],
198
+ model: "gpt-5",
199
+ apiKey: "sk-secret-key",
200
+ };
201
+ expect(computeAgentFingerprint(baseOptions)).toBe(computeAgentFingerprint(reordered));
202
+ });
203
+
204
+ it("changes when any acquisition input changes", () => {
205
+ const base = computeAgentFingerprint(baseOptions);
206
+ expect(computeAgentFingerprint({ ...baseOptions, apiKey: "sk-rotated" })).not.toBe(base);
207
+ expect(computeAgentFingerprint({ ...baseOptions, model: "gpt-5-mini" })).not.toBe(base);
208
+ expect(
209
+ computeAgentFingerprint({
210
+ ...baseOptions,
211
+ mcpServers: { github: { command: "npx", env: { TOKEN: "t-2" } } },
212
+ }),
213
+ ).not.toBe(base);
214
+ });
215
+
216
+ it("never embeds the raw api key", () => {
217
+ expect(computeAgentFingerprint(baseOptions)).not.toContain("sk-secret-key");
218
+ });
219
+ });
220
+ });
@@ -201,6 +201,56 @@ describe("buildPrompt", () => {
201
201
  expect(prompt).not.toContain("<session_context>");
202
202
  });
203
203
 
204
+ it("carries the declared preferences on the first execution, attributed per scope", () => {
205
+ const prompt = buildPrompt(
206
+ input({
207
+ resolution: resolution("local", "created_first_execution"),
208
+ declaredPreferences: {
209
+ orgContext: "We deploy to us-east-1.",
210
+ userContext: "Keep answers terse.",
211
+ },
212
+ }),
213
+ );
214
+ expect(prompt).toContain("<declared_preferences>");
215
+ expect(prompt).toContain("Declared by the organization:\nWe deploy to us-east-1.");
216
+ expect(prompt).toContain("Declared by the user:\nKeep answers terse.");
217
+ // Preferences are CONTEXT like the bridge; the approval protocol keeps
218
+ // its pinned last-before-task slot.
219
+ expect(prompt.indexOf("<declared_preferences>"))
220
+ .toBeLessThan(prompt.indexOf("<tool_approval_protocol>"));
221
+ });
222
+
223
+ it("orders platform-declared preferences before the embedder's session context (DD-002 D3)", () => {
224
+ const prompt = buildPrompt(
225
+ input({
226
+ resolution: resolution("local", "created_first_execution"),
227
+ declaredPreferences: { orgContext: "We deploy to us-east-1." },
228
+ sessionContext: "Role: platform admin",
229
+ }),
230
+ );
231
+ const preferences = prompt.indexOf("<declared_preferences>");
232
+ const context = prompt.indexOf("<session_context>");
233
+ expect(preferences).toBeGreaterThan(-1);
234
+ expect(context).toBeGreaterThan(preferences);
235
+ });
236
+
237
+ it("never re-sends the preferences to a successfully resumed agent — frozen per session by design (DD-002 D3)", () => {
238
+ const prompt = buildPrompt(
239
+ input({
240
+ resolution: resolution("local", "resumed_successfully"),
241
+ declaredPreferences: { orgContext: "We deploy to us-east-1." },
242
+ }),
243
+ );
244
+ expect(prompt).toBe(USER_MESSAGE);
245
+ });
246
+
247
+ it("omits the preferences section when the execution carries none", () => {
248
+ const prompt = buildPrompt(
249
+ input({ resolution: resolution("local", "created_first_execution") }),
250
+ );
251
+ expect(prompt).not.toContain("<declared_preferences>");
252
+ });
253
+
204
254
  it("uses the reinvocation prompt for a HITL reinvocation (human-meaningful, no opaque ids)", () => {
205
255
  const approvalDecisions = new Map<string, ApprovalAction>([
206
256
  ["tool-call-1", ApprovalAction.APPROVE],
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type { ResolvedMcpServer } from "../../../shared/mcp-resolver.js";
3
+ import { toCursorMcpConfig, validateMcpServerEnv } from "../cursor-mcp-config.js";
4
+
5
+ // Resolution behavior (enabled_tools threading, overrides scoping, the
6
+ // transport guard) is pinned once on the single shared resolver
7
+ // (shared/__tests__/mcp-resolver.test.ts — oss#387). These tests pin what is
8
+ // genuinely Cursor-specific: the SDK config projection and the env pre-flight.
9
+
10
+ function server(overrides: Partial<ResolvedMcpServer>): ResolvedMcpServer {
11
+ return {
12
+ slug: "github",
13
+ connectionType: "http",
14
+ url: "https://mcp.example.com/mcp",
15
+ toolApprovals: [],
16
+ pinnedToolApprovals: [],
17
+ discoveredCapabilitiesEmpty: false,
18
+ toolApprovalOverrides: [],
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ function usage(slug: string) {
24
+ return { mcpServerRef: { slug, org: "test-org", kind: 0 } } as any;
25
+ }
26
+
27
+ describe("toCursorMcpConfig", () => {
28
+ it("projects an http server to a url config", () => {
29
+ const config = toCursorMcpConfig([
30
+ server({ headers: { Authorization: "Bearer x" } }),
31
+ ]);
32
+
33
+ expect(config.github).toEqual({
34
+ type: "http",
35
+ url: "https://mcp.example.com/mcp",
36
+ headers: { Authorization: "Bearer x" },
37
+ });
38
+ });
39
+
40
+ it("projects a stdio server to a command config", () => {
41
+ const config = toCursorMcpConfig([
42
+ server({
43
+ slug: "local-tool",
44
+ connectionType: "stdio",
45
+ url: undefined,
46
+ command: "npx",
47
+ args: ["-y", "tool"],
48
+ env: { API_KEY: "k" },
49
+ cwd: "/work",
50
+ }),
51
+ ]);
52
+
53
+ expect(config["local-tool"]).toEqual({
54
+ type: "stdio",
55
+ command: "npx",
56
+ args: ["-y", "tool"],
57
+ env: { API_KEY: "k" },
58
+ cwd: "/work",
59
+ });
60
+ });
61
+
62
+ it("skips servers missing their transport's required field", () => {
63
+ const config = toCursorMcpConfig([
64
+ server({ url: undefined }),
65
+ server({ slug: "no-cmd", connectionType: "stdio", url: undefined, command: undefined }),
66
+ ]);
67
+
68
+ expect(config).toEqual({});
69
+ });
70
+
71
+ it("never narrows the config for enabledTools — the SDK has no allow-list field; enforcement is the hook's disabled arm", () => {
72
+ const config = toCursorMcpConfig([server({ enabledTools: ["create_pr"] })]);
73
+
74
+ expect(config.github).toEqual({
75
+ type: "http",
76
+ url: "https://mcp.example.com/mcp",
77
+ headers: undefined,
78
+ });
79
+ });
80
+ });
81
+
82
+ describe("validateMcpServerEnv", () => {
83
+ it("warns for a usage whose server failed to resolve", () => {
84
+ const warnings = validateMcpServerEnv([], [usage("github")]);
85
+
86
+ expect(warnings).toHaveLength(1);
87
+ expect(warnings[0]).toContain("'github'");
88
+ expect(warnings[0]).toContain("failed to resolve");
89
+ });
90
+
91
+ it("warns for a stdio server carrying empty env values", () => {
92
+ const warnings = validateMcpServerEnv(
93
+ [server({
94
+ connectionType: "stdio",
95
+ url: undefined,
96
+ command: "npx",
97
+ env: { API_KEY: "", OTHER: "set" },
98
+ })],
99
+ [usage("github")],
100
+ );
101
+
102
+ expect(warnings).toHaveLength(1);
103
+ expect(warnings[0]).toContain("[API_KEY]");
104
+ });
105
+
106
+ it("stays quiet for healthy servers", () => {
107
+ const warnings = validateMcpServerEnv([server({})], [usage("github")]);
108
+
109
+ expect(warnings).toEqual([]);
110
+ });
111
+ });
@@ -19,10 +19,10 @@ vi.mock("@cursor/sdk", () => ({
19
19
  }));
20
20
 
21
21
  import {
22
- resolveEffectiveServiceTier,
23
22
  resolveServiceTierParams,
24
23
  resetCatalogCacheForTests,
25
24
  } from "../service-tier.js";
25
+ import { resolveEffectiveServiceTier } from "../../../shared/service-tier.js";
26
26
 
27
27
  const CATALOG = [
28
28
  {
@@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync } from "no
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { ConnectError, Code } from "@connectrpc/connect";
6
- import { resolveSkills, downloadArtifact } from "../skill-resolver.js";
6
+ import { resolveSkills } from "../skill-resolver.js";
7
7
  import { buildZip } from "../../../__test-utils__/zip-fixtures.js";
8
8
 
9
9
  /** A server that predates the transfer lane (#675) answers the mint RPC
@@ -390,96 +390,5 @@ describe("resolveSkills — artifact extraction", () => {
390
390
  });
391
391
  });
392
392
 
393
- // ─── downloadArtifact transfer lane routing (#675) ─────────────────────
394
-
395
- describe("downloadArtifact — transfer lane routing", () => {
396
- afterEach(() => {
397
- vi.unstubAllGlobals();
398
- });
399
-
400
- function makeClient(overrides: Record<string, any> = {}) {
401
- return {
402
- getSkillArtifact: vi.fn().mockResolvedValue({ artifact: new Uint8Array(0) }),
403
- getSkillArtifactDownloadUrl: unimplementedMint(),
404
- ...overrides,
405
- } as any;
406
- }
407
-
408
- it("fetches bytes over HTTP when the server mints a download URL", async () => {
409
- const bytes = new Uint8Array([1, 2, 3, 4, 5]);
410
- const client = makeClient({
411
- getSkillArtifactDownloadUrl: vi.fn().mockResolvedValue({
412
- url: "http://localhost:7234/v1/skill-artifacts/skills/abc.zip",
413
- sizeBytes: 5n,
414
- ttlSeconds: 0,
415
- }),
416
- });
417
- const fetchMock = vi.fn().mockResolvedValue({
418
- ok: true,
419
- arrayBuffer: async () => bytes.buffer,
420
- });
421
- vi.stubGlobal("fetch", fetchMock);
422
-
423
- const got = await downloadArtifact(client, "skills/abc.zip");
424
-
425
- expect(got).toEqual(bytes);
426
- expect(fetchMock).toHaveBeenCalledWith("http://localhost:7234/v1/skill-artifacts/skills/abc.zip");
427
- // The unary lane (10MB-capped) must not be touched when the URL lane works.
428
- expect(client.getSkillArtifact).not.toHaveBeenCalled();
429
- });
430
-
431
- it("falls back to the unary RPC when the server predates the lane", async () => {
432
- const bytes = new Uint8Array([9, 9]);
433
- const client = makeClient({
434
- getSkillArtifact: vi.fn().mockResolvedValue({ artifact: bytes }),
435
- });
436
- vi.stubGlobal("fetch", vi.fn()); // must never be called
437
-
438
- const got = await downloadArtifact(client, "skills/abc.zip");
439
-
440
- expect(got).toEqual(bytes);
441
- expect(client.getSkillArtifact).toHaveBeenCalledWith("skills/abc.zip");
442
- expect(fetch).not.toHaveBeenCalled();
443
- });
444
-
445
- it("does NOT fall back on non-Unimplemented mint failures", async () => {
446
- const client = makeClient({
447
- getSkillArtifactDownloadUrl: vi.fn().mockRejectedValue(
448
- new ConnectError("boom", Code.Internal),
449
- ),
450
- });
451
-
452
- await expect(downloadArtifact(client, "skills/abc.zip")).rejects.toThrow("boom");
453
- // Falling back here would mask real server faults behind the capped lane.
454
- expect(client.getSkillArtifact).not.toHaveBeenCalled();
455
- });
456
-
457
- it("rejects truncated fetches via the minted size", async () => {
458
- const client = makeClient({
459
- getSkillArtifactDownloadUrl: vi.fn().mockResolvedValue({
460
- url: "http://localhost:7234/v1/skill-artifacts/skills/abc.zip",
461
- sizeBytes: 100n,
462
- ttlSeconds: 0,
463
- }),
464
- });
465
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
466
- ok: true,
467
- arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
468
- }));
469
-
470
- await expect(downloadArtifact(client, "skills/abc.zip")).rejects.toThrow(/truncated/);
471
- });
472
-
473
- it("surfaces HTTP failures with the status code", async () => {
474
- const client = makeClient({
475
- getSkillArtifactDownloadUrl: vi.fn().mockResolvedValue({
476
- url: "http://localhost:7234/v1/skill-artifacts/skills/gone.zip",
477
- sizeBytes: 0n,
478
- ttlSeconds: 0,
479
- }),
480
- });
481
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404 }));
482
-
483
- await expect(downloadArtifact(client, "skills/gone.zip")).rejects.toThrow(/HTTP 404/);
484
- });
485
- });
393
+ // downloadArtifact's transfer-lane routing tests moved with the code to
394
+ // shared/__tests__/skill-mount.test.ts (issue #337 extraction).