oh-my-copilot 5.6.2 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +2 -0
  4. package/bridge/__pycache__/gyoshu_bridge.cpython-312.pyc +0 -0
  5. package/bridge/claude-md-coordinator.cjs +2 -2
  6. package/bridge/cli.cjs +81405 -76559
  7. package/bridge/mcp-server.cjs +40048 -32187
  8. package/bridge/runtime-cli.cjs +943 -199
  9. package/bridge/team-mcp.cjs +716 -125
  10. package/bridge/team.js +1066 -290
  11. package/dist/__tests__/cli-win32-warning.test.js +7 -0
  12. package/dist/__tests__/cli-win32-warning.test.js.map +1 -1
  13. package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts +2 -0
  14. package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts.map +1 -0
  15. package/dist/__tests__/run-cjs-copilot-worker-routing.test.js +294 -0
  16. package/dist/__tests__/run-cjs-copilot-worker-routing.test.js.map +1 -0
  17. package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts +2 -0
  18. package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts.map +1 -0
  19. package/dist/__tests__/run-cjs-fail-closed-timeout.test.js +68 -0
  20. package/dist/__tests__/run-cjs-fail-closed-timeout.test.js.map +1 -0
  21. package/dist/__tests__/run-cjs-session-end-budget.test.d.ts +2 -0
  22. package/dist/__tests__/run-cjs-session-end-budget.test.d.ts.map +1 -0
  23. package/dist/__tests__/run-cjs-session-end-budget.test.js +171 -0
  24. package/dist/__tests__/run-cjs-session-end-budget.test.js.map +1 -0
  25. package/dist/__tests__/session-end-process-exit.test.js +70 -2
  26. package/dist/__tests__/session-end-process-exit.test.js.map +1 -1
  27. package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts +2 -0
  28. package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts.map +1 -0
  29. package/dist/cli/__tests__/doctor-conflicts-json.test.js +46 -0
  30. package/dist/cli/__tests__/doctor-conflicts-json.test.js.map +1 -0
  31. package/dist/cli/__tests__/team.test.js +67 -0
  32. package/dist/cli/__tests__/team.test.js.map +1 -1
  33. package/dist/cli/__tests__/wait-teleport-json.test.d.ts +2 -0
  34. package/dist/cli/__tests__/wait-teleport-json.test.d.ts.map +1 -0
  35. package/dist/cli/__tests__/wait-teleport-json.test.js +64 -0
  36. package/dist/cli/__tests__/wait-teleport-json.test.js.map +1 -0
  37. package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts +2 -0
  38. package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts.map +1 -0
  39. package/dist/cli/commands/__tests__/smoke-tier2.test.js +110 -0
  40. package/dist/cli/commands/__tests__/smoke-tier2.test.js.map +1 -0
  41. package/dist/cli/commands/__tests__/team-status-json.test.d.ts +2 -0
  42. package/dist/cli/commands/__tests__/team-status-json.test.d.ts.map +1 -0
  43. package/dist/cli/commands/__tests__/team-status-json.test.js +141 -0
  44. package/dist/cli/commands/__tests__/team-status-json.test.js.map +1 -0
  45. package/dist/cli/commands/factory.d.ts +2 -0
  46. package/dist/cli/commands/factory.d.ts.map +1 -1
  47. package/dist/cli/commands/factory.js +8 -1
  48. package/dist/cli/commands/factory.js.map +1 -1
  49. package/dist/cli/commands/smoke.d.ts +36 -0
  50. package/dist/cli/commands/smoke.d.ts.map +1 -0
  51. package/dist/cli/commands/smoke.js +169 -0
  52. package/dist/cli/commands/smoke.js.map +1 -0
  53. package/dist/cli/commands/team.d.ts +2 -0
  54. package/dist/cli/commands/team.d.ts.map +1 -1
  55. package/dist/cli/commands/team.js +124 -10
  56. package/dist/cli/commands/team.js.map +1 -1
  57. package/dist/cli/index.d.ts.map +1 -1
  58. package/dist/cli/index.js +70 -10
  59. package/dist/cli/index.js.map +1 -1
  60. package/dist/cli/win32-warning.d.ts +7 -1
  61. package/dist/cli/win32-warning.d.ts.map +1 -1
  62. package/dist/cli/win32-warning.js +9 -1
  63. package/dist/cli/win32-warning.js.map +1 -1
  64. package/dist/constants/names.d.ts +1 -0
  65. package/dist/constants/names.d.ts.map +1 -1
  66. package/dist/constants/names.js +1 -0
  67. package/dist/constants/names.js.map +1 -1
  68. package/dist/factory/__tests__/check-run.test.js +2 -2
  69. package/dist/factory/__tests__/check-run.test.js.map +1 -1
  70. package/dist/factory/__tests__/listener.test.js +1 -1
  71. package/dist/factory/__tests__/listener.test.js.map +1 -1
  72. package/dist/factory/__tests__/status.test.js +42 -0
  73. package/dist/factory/__tests__/status.test.js.map +1 -1
  74. package/dist/factory/listener.d.ts.map +1 -1
  75. package/dist/factory/listener.js +7 -8
  76. package/dist/factory/listener.js.map +1 -1
  77. package/dist/factory/status.d.ts +19 -1
  78. package/dist/factory/status.d.ts.map +1 -1
  79. package/dist/factory/status.js +49 -8
  80. package/dist/factory/status.js.map +1 -1
  81. package/dist/factory/watchdog.d.ts.map +1 -1
  82. package/dist/factory/watchdog.js +14 -0
  83. package/dist/factory/watchdog.js.map +1 -1
  84. package/dist/factory/watchdog.test.js +45 -0
  85. package/dist/factory/watchdog.test.js.map +1 -1
  86. package/dist/hooks/session-end/__tests__/action-runner.test.js +5 -0
  87. package/dist/hooks/session-end/__tests__/action-runner.test.js.map +1 -1
  88. package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js +186 -1
  89. package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js.map +1 -1
  90. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts +2 -0
  91. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts.map +1 -0
  92. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js +49 -0
  93. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js.map +1 -0
  94. package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js +30 -1
  95. package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js.map +1 -1
  96. package/dist/hooks/session-end/__tests__/spawn-next.test.js +104 -3
  97. package/dist/hooks/session-end/__tests__/spawn-next.test.js.map +1 -1
  98. package/dist/hooks/session-end/__tests__/worker.test.js +6 -0
  99. package/dist/hooks/session-end/__tests__/worker.test.js.map +1 -1
  100. package/dist/hooks/session-end/action-runner.d.ts.map +1 -1
  101. package/dist/hooks/session-end/action-runner.js +2 -1
  102. package/dist/hooks/session-end/action-runner.js.map +1 -1
  103. package/dist/hooks/session-end/chain-enqueuer.d.ts +70 -7
  104. package/dist/hooks/session-end/chain-enqueuer.d.ts.map +1 -1
  105. package/dist/hooks/session-end/chain-enqueuer.js +354 -82
  106. package/dist/hooks/session-end/chain-enqueuer.js.map +1 -1
  107. package/dist/hooks/session-end/foreground-bootstrap.d.ts.map +1 -1
  108. package/dist/hooks/session-end/foreground-bootstrap.js +24 -16
  109. package/dist/hooks/session-end/foreground-bootstrap.js.map +1 -1
  110. package/dist/hooks/session-end/index.d.ts +2 -3
  111. package/dist/hooks/session-end/index.d.ts.map +1 -1
  112. package/dist/hooks/session-end/index.js +11 -13
  113. package/dist/hooks/session-end/index.js.map +1 -1
  114. package/dist/hooks/session-end/spawn-next.d.ts +81 -4
  115. package/dist/hooks/session-end/spawn-next.d.ts.map +1 -1
  116. package/dist/hooks/session-end/spawn-next.js +141 -14
  117. package/dist/hooks/session-end/spawn-next.js.map +1 -1
  118. package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts +17 -0
  119. package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts.map +1 -0
  120. package/dist/hooks/session-end/wiki-foreground-bootstrap.js +20 -0
  121. package/dist/hooks/session-end/wiki-foreground-bootstrap.js.map +1 -0
  122. package/dist/hooks/session-end/worker.d.ts.map +1 -1
  123. package/dist/hooks/session-end/worker.js +11 -6
  124. package/dist/hooks/session-end/worker.js.map +1 -1
  125. package/dist/lib/__tests__/atomic-write.test.js +32 -1
  126. package/dist/lib/__tests__/atomic-write.test.js.map +1 -1
  127. package/dist/lib/__tests__/mode-state-lock.test.js +121 -12
  128. package/dist/lib/__tests__/mode-state-lock.test.js.map +1 -1
  129. package/dist/lib/atomic-write.d.ts +13 -3
  130. package/dist/lib/atomic-write.d.ts.map +1 -1
  131. package/dist/lib/atomic-write.js +29 -15
  132. package/dist/lib/atomic-write.js.map +1 -1
  133. package/dist/lib/mode-state-io.d.ts +2 -2
  134. package/dist/lib/mode-state-io.d.ts.map +1 -1
  135. package/dist/lib/mode-state-io.js +16 -10
  136. package/dist/lib/mode-state-io.js.map +1 -1
  137. package/dist/mcp/__tests__/host-smoke-tool.test.d.ts +2 -0
  138. package/dist/mcp/__tests__/host-smoke-tool.test.d.ts.map +1 -0
  139. package/dist/mcp/__tests__/host-smoke-tool.test.js +174 -0
  140. package/dist/mcp/__tests__/host-smoke-tool.test.js.map +1 -0
  141. package/dist/mcp/disable-tools.d.ts.map +1 -1
  142. package/dist/mcp/disable-tools.js +1 -0
  143. package/dist/mcp/disable-tools.js.map +1 -1
  144. package/dist/mcp/tool-registry.d.ts.map +1 -1
  145. package/dist/mcp/tool-registry.js +2 -0
  146. package/dist/mcp/tool-registry.js.map +1 -1
  147. package/dist/shared/types.d.ts +11 -0
  148. package/dist/shared/types.d.ts.map +1 -1
  149. package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts +2 -0
  150. package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts.map +1 -0
  151. package/dist/smoke/__tests__/copilot-chain-scenario.test.js +154 -0
  152. package/dist/smoke/__tests__/copilot-chain-scenario.test.js.map +1 -0
  153. package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts +2 -0
  154. package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts.map +1 -0
  155. package/dist/smoke/__tests__/copilot-sdk-driver.test.js +591 -0
  156. package/dist/smoke/__tests__/copilot-sdk-driver.test.js.map +1 -0
  157. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts +2 -0
  158. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts.map +1 -0
  159. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js +362 -0
  160. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js.map +1 -0
  161. package/dist/smoke/__tests__/copilot-session-eval.test.d.ts +2 -0
  162. package/dist/smoke/__tests__/copilot-session-eval.test.d.ts.map +1 -0
  163. package/dist/smoke/__tests__/copilot-session-eval.test.js +264 -0
  164. package/dist/smoke/__tests__/copilot-session-eval.test.js.map +1 -0
  165. package/dist/smoke/__tests__/copilot-smoke.test.d.ts +2 -0
  166. package/dist/smoke/__tests__/copilot-smoke.test.d.ts.map +1 -0
  167. package/dist/smoke/__tests__/copilot-smoke.test.js +482 -0
  168. package/dist/smoke/__tests__/copilot-smoke.test.js.map +1 -0
  169. package/dist/smoke/copilot-binary.d.ts +21 -0
  170. package/dist/smoke/copilot-binary.d.ts.map +1 -0
  171. package/dist/smoke/copilot-binary.js +46 -0
  172. package/dist/smoke/copilot-binary.js.map +1 -0
  173. package/dist/smoke/copilot-chain-scenario.d.ts +61 -0
  174. package/dist/smoke/copilot-chain-scenario.d.ts.map +1 -0
  175. package/dist/smoke/copilot-chain-scenario.js +253 -0
  176. package/dist/smoke/copilot-chain-scenario.js.map +1 -0
  177. package/dist/smoke/copilot-sdk-driver.d.ts +189 -0
  178. package/dist/smoke/copilot-sdk-driver.d.ts.map +1 -0
  179. package/dist/smoke/copilot-sdk-driver.js +637 -0
  180. package/dist/smoke/copilot-sdk-driver.js.map +1 -0
  181. package/dist/smoke/copilot-sdk-scenarios.d.ts +200 -0
  182. package/dist/smoke/copilot-sdk-scenarios.d.ts.map +1 -0
  183. package/dist/smoke/copilot-sdk-scenarios.js +620 -0
  184. package/dist/smoke/copilot-sdk-scenarios.js.map +1 -0
  185. package/dist/smoke/copilot-session-env.d.ts +75 -0
  186. package/dist/smoke/copilot-session-env.d.ts.map +1 -0
  187. package/dist/smoke/copilot-session-env.js +154 -0
  188. package/dist/smoke/copilot-session-env.js.map +1 -0
  189. package/dist/smoke/copilot-session-eval.d.ts +90 -0
  190. package/dist/smoke/copilot-session-eval.d.ts.map +1 -0
  191. package/dist/smoke/copilot-session-eval.js +323 -0
  192. package/dist/smoke/copilot-session-eval.js.map +1 -0
  193. package/dist/smoke/copilot-smoke.d.ts +166 -0
  194. package/dist/smoke/copilot-smoke.d.ts.map +1 -0
  195. package/dist/smoke/copilot-smoke.js +688 -0
  196. package/dist/smoke/copilot-smoke.js.map +1 -0
  197. package/dist/smoke/process-utils.d.ts +47 -0
  198. package/dist/smoke/process-utils.d.ts.map +1 -0
  199. package/dist/smoke/process-utils.js +99 -0
  200. package/dist/smoke/process-utils.js.map +1 -0
  201. package/dist/team/__tests__/runtime-v2.monitor.test.js +35 -0
  202. package/dist/team/__tests__/runtime-v2.monitor.test.js.map +1 -1
  203. package/dist/team/__tests__/scaling.test.js +13 -0
  204. package/dist/team/__tests__/scaling.test.js.map +1 -1
  205. package/dist/team/__tests__/sdk-transport.runtime.test.d.ts +2 -0
  206. package/dist/team/__tests__/sdk-transport.runtime.test.d.ts.map +1 -0
  207. package/dist/team/__tests__/sdk-transport.runtime.test.js +96 -0
  208. package/dist/team/__tests__/sdk-transport.runtime.test.js.map +1 -0
  209. package/dist/team/__tests__/sdk-transport.test.d.ts +2 -0
  210. package/dist/team/__tests__/sdk-transport.test.d.ts.map +1 -0
  211. package/dist/team/__tests__/sdk-transport.test.js +650 -0
  212. package/dist/team/__tests__/sdk-transport.test.js.map +1 -0
  213. package/dist/team/__tests__/tmux-session.pane-env.test.d.ts +2 -0
  214. package/dist/team/__tests__/tmux-session.pane-env.test.d.ts.map +1 -0
  215. package/dist/team/__tests__/tmux-session.pane-env.test.js +52 -0
  216. package/dist/team/__tests__/tmux-session.pane-env.test.js.map +1 -0
  217. package/dist/team/api-interop.d.ts.map +1 -1
  218. package/dist/team/api-interop.js +8 -0
  219. package/dist/team/api-interop.js.map +1 -1
  220. package/dist/team/runtime-v2.d.ts +33 -0
  221. package/dist/team/runtime-v2.d.ts.map +1 -1
  222. package/dist/team/runtime-v2.js +338 -24
  223. package/dist/team/runtime-v2.js.map +1 -1
  224. package/dist/team/scaling.d.ts.map +1 -1
  225. package/dist/team/scaling.js +5 -0
  226. package/dist/team/scaling.js.map +1 -1
  227. package/dist/team/sdk-host.d.ts +48 -0
  228. package/dist/team/sdk-host.d.ts.map +1 -0
  229. package/dist/team/sdk-host.js +604 -0
  230. package/dist/team/sdk-host.js.map +1 -0
  231. package/dist/team/sdk-policy.d.ts +79 -0
  232. package/dist/team/sdk-policy.d.ts.map +1 -0
  233. package/dist/team/sdk-policy.js +186 -0
  234. package/dist/team/sdk-policy.js.map +1 -0
  235. package/dist/team/sdk-transport.d.ts +164 -0
  236. package/dist/team/sdk-transport.d.ts.map +1 -0
  237. package/dist/team/sdk-transport.js +266 -0
  238. package/dist/team/sdk-transport.js.map +1 -0
  239. package/dist/team/tmux-session.d.ts +7 -0
  240. package/dist/team/tmux-session.d.ts.map +1 -1
  241. package/dist/team/tmux-session.js +80 -18
  242. package/dist/team/tmux-session.js.map +1 -1
  243. package/dist/team/types.d.ts +2 -0
  244. package/dist/team/types.d.ts.map +1 -1
  245. package/dist/team/types.js.map +1 -1
  246. package/dist/tools/host-smoke-tool.d.ts +34 -0
  247. package/dist/tools/host-smoke-tool.d.ts.map +1 -0
  248. package/dist/tools/host-smoke-tool.js +133 -0
  249. package/dist/tools/host-smoke-tool.js.map +1 -0
  250. package/docs/CLAUDE.md +1 -1
  251. package/docs/DEVELOPERS.md +122 -0
  252. package/docs/HOOKS.md +34 -2
  253. package/docs/MIGRATION.md +194 -3
  254. package/docs/REFERENCE.md +95 -1
  255. package/docs/TOOLS.md +32 -1
  256. package/package.json +11 -2
  257. package/plugin.json +1 -1
  258. package/scripts/dev/repro-state-lock.mjs +1 -1
  259. package/scripts/lib/atomic-write.mjs +4 -2
  260. package/scripts/lib/state-lock.mjs +21 -12
  261. package/scripts/run.cjs +141 -18
  262. package/scripts/session-start.mjs +13 -0
  263. package/scripts/wiki-session-end.mjs +2 -1
  264. package/templates/hooks/lib/atomic-write.mjs +4 -2
@@ -0,0 +1,591 @@
1
+ import { EventEmitter } from 'events';
2
+ import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { PassThrough } from 'stream';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import { parseJsonl } from '../copilot-session-eval.js';
8
+ import { globalModuleRoots, importFromDir, resolveShimTarget, runSdkTier, runtimeCreditCap, satisfiesSdkRange, } from '../copilot-sdk-driver.js';
9
+ import { CREDIT_CAP_SKIP_DETAIL, SCENARIOS, scenarioCheckIds, scenarioExcludedTools, SDK_MISSING_DETAIL } from '../copilot-sdk-scenarios.js';
10
+ import { LIVE_RUN_REFUSED_DETAIL, liveRunRefusal, runCopilotSmoke } from '../copilot-smoke.js';
11
+ import { smokeExitCode } from '../../cli/commands/smoke.js';
12
+ const FIXTURES = join(__dirname, 'fixtures');
13
+ const fixtureEvents = (name) => parseJsonl(readFileSync(join(FIXTURES, `tier2-${name}-events.jsonl`), 'utf-8')).events;
14
+ const LOG = readFileSync(join(FIXTURES, 'tier2-hooks-debug-excerpt.log'), 'utf-8');
15
+ const CLEAN_LOG = LOG.split('\n').filter((l) => !l.includes('[run.cjs]')).join('\n');
16
+ let root;
17
+ let parent;
18
+ function write(path, content) {
19
+ mkdirSync(join(path, '..'), { recursive: true });
20
+ writeFileSync(path, content);
21
+ }
22
+ beforeEach(() => {
23
+ root = mkdtempSync(join(tmpdir(), 'sdk-root-'));
24
+ write(join(root, 'plugin.json'), JSON.stringify({ name: 'oh-my-copilot', version: '9.9.9' }));
25
+ write(join(root, 'package.json'), JSON.stringify({ name: 'oh-my-copilot', version: '9.9.9' }));
26
+ write(join(root, '.mcp.json'), JSON.stringify({ mcpServers: { t: { command: 'node' } } }));
27
+ for (const s of ['alpha', 'plan'])
28
+ write(join(root, 'skills', s, 'SKILL.md'), '# s');
29
+ write(join(root, 'agents', 'architect.md'), '# a');
30
+ write(join(root, 'copilot', 'agents', 'architect.md'), '# a');
31
+ write(join(root, 'dist', 'mcp', 'standalone-server.js'), '// fake');
32
+ parent = mkdtempSync(join(tmpdir(), 'sdk-parent-'));
33
+ mkdirSync(join(parent, 'home'));
34
+ });
35
+ afterEach(() => {
36
+ rmSync(root, { recursive: true, force: true });
37
+ rmSync(parent, { recursive: true, force: true });
38
+ });
39
+ /** Re-stamp a fixture log's ISO timestamps to `iso` (the driver windows lines by time). */
40
+ const restamp = (text, iso = new Date().toISOString()) => text.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z/gm, iso);
41
+ function scenarioOf(prompt) {
42
+ const hit = Object.keys(SCENARIOS).find((s) => SCENARIOS[s].prompt === prompt);
43
+ if (!hit)
44
+ throw new Error(`unexpected prompt ${prompt}`);
45
+ return hit;
46
+ }
47
+ /** A fake @github/copilot-sdk: static RPCs answer from the fake plugin root; prompts replay fixture JSONL. */
48
+ function fakeSdk(opts = {}) {
49
+ const rec = { ops: [], sessions: [], permissions: [] };
50
+ let n = 0;
51
+ const sentAt = {};
52
+ class FakeClient {
53
+ cliProcess = opts.pid ? { pid: opts.pid } : null;
54
+ constructor(options) {
55
+ rec.clientOptions = options;
56
+ rec.connection = options.connection;
57
+ }
58
+ async start() {
59
+ rec.ops.push('start');
60
+ if (opts.startError)
61
+ throw new Error(opts.startError);
62
+ }
63
+ async stop() {
64
+ rec.ops.push('stop');
65
+ if (opts.stopError)
66
+ throw new Error(opts.stopError);
67
+ this.cliProcess = null;
68
+ return [];
69
+ }
70
+ async forceStop() { rec.ops.push('forceStop'); this.cliProcess = null; }
71
+ async getStatus() { return { version: '1.0.91', protocolVersion: 3 }; }
72
+ async listModels() { return (opts.models ?? ['auto']).map((id) => ({ id })); }
73
+ async deleteSession(id) { rec.ops.push(`delete:${id}`); }
74
+ async createSession(config) {
75
+ const id = `s${++n}`;
76
+ rec.ops.push(`create:${id}`);
77
+ rec.sessions.push(config);
78
+ const home = rec.clientOptions.baseDirectory;
79
+ let current;
80
+ const emit = (e) => config.onEvent?.(e);
81
+ return {
82
+ sessionId: id,
83
+ async send({ prompt }) {
84
+ rec.ops.push(`send:${id}`);
85
+ if (opts.sendError)
86
+ throw new Error(opts.sendError);
87
+ current = scenarioOf(prompt);
88
+ const scenario = current;
89
+ for (const kind of ['read', 'shell', 'write']) {
90
+ const decision = await config.onPermissionRequest({ kind, fullCommandText: 'git push --force origin main' }, { sessionId: id });
91
+ rec.permissions.push({ scenario, kind, decision: decision.kind });
92
+ }
93
+ sentAt[scenario] = new Date().toISOString();
94
+ if (opts.hang?.includes(scenario))
95
+ return 'm1';
96
+ setImmediate(() => {
97
+ appendFileSync(join(home, 'logs', 'process-1.log'), `${restamp(opts.log ?? CLEAN_LOG)}\n`);
98
+ const late = opts.lateLine;
99
+ if (late?.during === scenario)
100
+ appendFileSync(join(home, 'logs', 'process-1.log'), `${restamp(late.text, sentAt[late.from])}\n`);
101
+ for (const e of opts.events?.[scenario] ?? fixtureEvents(scenario))
102
+ emit(e);
103
+ });
104
+ return 'm1';
105
+ },
106
+ async abort() {
107
+ rec.ops.push(`abort:${id}`);
108
+ setImmediate(() => {
109
+ emit({ type: 'abort', data: { reason: 'user_initiated' } });
110
+ emit({ type: 'session.idle', data: { aborted: true } });
111
+ });
112
+ },
113
+ async disconnect() { rec.ops.push(`disconnect:${id}`); },
114
+ rpc: {
115
+ plugins: { list: async () => ({ plugins: [{ name: 'oh-my-copilot', version: '9.9.9', enabled: true }] }) },
116
+ skills: {
117
+ list: async () => ({
118
+ skills: ['alpha', 'plan'].map((s) => ({ name: s, source: 'plugin', pluginName: 'oh-my-copilot', path: join(root, 'skills', s, 'SKILL.md') })),
119
+ }),
120
+ },
121
+ agent: { list: async () => ({ agents: [{ name: 'oh-my-copilot:architect' }] }) },
122
+ mcp: {
123
+ list: async () => ({ servers: [{ name: 't', status: 'connected' }] }),
124
+ listTools: async () => ({ tools: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] }),
125
+ },
126
+ tools: {
127
+ initializeAndValidate: async () => { rec.ops.push(`tools.init:${id}`); return {}; },
128
+ getCurrentMetadata: async () => ({
129
+ tools: rec.ops.includes(`tools.init:${id}`)
130
+ ? (opts.toolNames ?? ['view', 't-state_read', 't-notepad_read']).map((name) => ({ name, mcpServerName: name.startsWith('t-') ? 't' : undefined, description: '' }))
131
+ : [], // CLI 1.0.91 reports an empty list until the tool set is initialized
132
+ }),
133
+ },
134
+ },
135
+ };
136
+ }
137
+ }
138
+ const module = {
139
+ CopilotClient: FakeClient,
140
+ RuntimeConnection: { forStdio: (o) => o },
141
+ };
142
+ // The runtime creates its log dir at start.
143
+ mkdirSync(join(parent, 'home', 'logs'), { recursive: true });
144
+ return { loaded: { module, version: '1.0.16', from: 'fake' }, rec };
145
+ }
146
+ function gitSpawnSync(calls, refs = '') {
147
+ return ((command, args, options) => {
148
+ calls.push({ args, cwd: options?.cwd });
149
+ const ok = (stdout) => ({ status: 0, stdout, stderr: '', pid: 1, output: [], signal: null });
150
+ if (command !== 'git')
151
+ return { ...ok(''), status: 1 };
152
+ return ok(args.includes('for-each-ref') ? refs : '');
153
+ });
154
+ }
155
+ function input(loaded, over = {}, gitCalls = []) {
156
+ return {
157
+ root,
158
+ bin: process.platform === 'win32' ? 'C:\\fake\\copilot.exe' : '/fake/copilot',
159
+ binVersion: '1.0.91',
160
+ packageVersion: '9.9.9',
161
+ home: join(parent, 'home'),
162
+ projectDir: join(parent, 'project'),
163
+ remoteDir: join(parent, 'remote.git'),
164
+ env: { COPILOT_HOME: join(parent, 'home'), OMC_GIT_GUARDRAILS: '1' },
165
+ scenarios: ['smoke', 'guardrail'],
166
+ timeoutMs: 5_000,
167
+ maxCredits: 30,
168
+ keepHome: false,
169
+ loadSdk: async () => loaded,
170
+ loadExpectedToolCount: async () => 3,
171
+ spawnSync: gitSpawnSync(gitCalls),
172
+ skillDirs: ['alpha', 'plan'],
173
+ agentFiles: ['architect.md'],
174
+ mcpServer: 't',
175
+ runChain: async () => { throw new Error('chain runner not stubbed'); },
176
+ ...over,
177
+ };
178
+ }
179
+ const byId = (checks) => Object.fromEntries(checks.map((c) => [c.id, c]));
180
+ describe('runSdkTier (fake SDK)', () => {
181
+ it('missing module: every sdk.* and scn.* check carries the install hint and the tier is skipped', async () => {
182
+ const result = await runSdkTier(input(null));
183
+ expect(result.skipped).toBe(SDK_MISSING_DETAIL);
184
+ expect(result.checks.map((c) => c.id)).toEqual([
185
+ 'sdk.available', 'sdk.runtime', 'sdk.plugins', 'sdk.skills', 'sdk.agents', 'sdk.mcp', 'sdk.tools_excluded',
186
+ ...scenarioCheckIds('smoke'), ...scenarioCheckIds('guardrail'),
187
+ ]);
188
+ expect(result.checks.every((c) => !c.ok && c.detail === SDK_MISSING_DETAIL)).toBe(true);
189
+ expect(SDK_MISSING_DETAIL).toBe('@github/copilot-sdk not installed — npm i -g @github/copilot-sdk --omit=optional --ignore-scripts');
190
+ });
191
+ it('a throwing loader counts as missing', async () => {
192
+ const result = await runSdkTier(input(null, { loadSdk: async () => { throw new Error('ERR_MODULE_NOT_FOUND'); } }));
193
+ expect(result.skipped).toBe(SDK_MISSING_DETAIL);
194
+ });
195
+ it('static checks only (scenarios: []): zero prompts sent', async () => {
196
+ const { loaded, rec } = fakeSdk();
197
+ const result = await runSdkTier(input(loaded, { scenarios: [] }));
198
+ expect(result.checks.map((c) => c.id)).toEqual(['sdk.available', 'sdk.runtime', 'sdk.plugins', 'sdk.skills', 'sdk.agents', 'sdk.mcp', 'sdk.tools_excluded']);
199
+ expect(result.checks.filter((c) => !c.ok)).toEqual([]);
200
+ expect(byId(result.checks)['sdk.tools_excluded'].detail).toBe('t-host_smoke absent; 2 other t-* tools offered (3 total)');
201
+ expect(rec.ops).toContain('tools.init:s1'); // the tool set was initialized for the listing
202
+ expect(rec.ops.some((o) => o.startsWith('send:'))).toBe(false);
203
+ expect(result.sdk).toEqual({ version: '1.0.16', runtimeVersion: '1.0.91', protocolVersion: 3, model: 'auto' });
204
+ expect(result.cost).toEqual({ premiumRequests: 0, credits: 0 });
205
+ });
206
+ it('drives the installed exe with debug logs via args, one client, isolated sessions', async () => {
207
+ const { loaded, rec } = fakeSdk();
208
+ const gitCalls = [];
209
+ const result = await runSdkTier(input(loaded, {}, gitCalls));
210
+ expect(result.checks.filter((c) => !c.ok)).toEqual([]);
211
+ expect(rec.connection).toMatchObject({ path: input(null).bin, args: ['--log-level', 'debug', '--max-ai-credits', '30'] });
212
+ expect(rec.connection.env).toMatchObject({ OMC_GIT_GUARDRAILS: '1' });
213
+ expect(rec.clientOptions).toMatchObject({ baseDirectory: join(parent, 'home'), workingDirectory: join(parent, 'project') });
214
+ expect(rec.clientOptions).not.toHaveProperty('logLevel');
215
+ // static + 2 scenario sessions, all with the plugin and without the GitHub MCP server
216
+ expect(rec.sessions).toHaveLength(3);
217
+ for (const s of rec.sessions) {
218
+ expect(s).toMatchObject({ pluginDirectories: [root], workingDirectory: join(parent, 'project'), disabledMcpServers: ['github-mcp-server'] });
219
+ expect(s.excludedTools).toContain('t-host_smoke');
220
+ expect(s).not.toHaveProperty('model'); // only auto listed
221
+ }
222
+ expect(rec.sessions[1].excludedTools).toEqual(scenarioExcludedTools('smoke', 't'));
223
+ expect(rec.sessions[2].excludedTools).toEqual(scenarioExcludedTools('guardrail', 't'));
224
+ // permission policy: smoke grants nothing; guardrail only the push
225
+ expect(rec.permissions.filter((p) => p.decision !== 'reject')).toEqual([{ scenario: 'guardrail', kind: 'shell', decision: 'approve-once' }]);
226
+ // sandbox repo on main with a bare origin; the guardrail checks its refs
227
+ expect(gitCalls.map((c) => c.args.join(' '))).toEqual(expect.arrayContaining([
228
+ '-c init.defaultBranch=main init -q', 'checkout -q -B main', `init -q --bare ${join(parent, 'remote.git')}`,
229
+ `remote add origin ${join(parent, 'remote.git')}`, `--git-dir ${join(parent, 'remote.git')} for-each-ref`,
230
+ ]));
231
+ // event capture per scenario
232
+ expect(Object.keys(result.events)).toEqual(['smoke', 'guardrail']);
233
+ expect(parseJsonl(readFileSync(result.events.guardrail, 'utf-8')).events.length).toBe(fixtureEvents('guardrail').length);
234
+ expect(result.cost.premiumRequests).toBe(2);
235
+ expect(result.cost.credits).toBeGreaterThan(0);
236
+ });
237
+ it.each(['skill', 'delegate'])('%s scenario passes on its captured stream', async (name) => {
238
+ const { loaded } = fakeSdk();
239
+ const result = await runSdkTier(input(loaded, { scenarios: [name] }));
240
+ expect(result.checks.filter((c) => !c.ok)).toEqual([]);
241
+ expect(result.checks.map((c) => c.id).slice(7)).toEqual(scenarioCheckIds(name));
242
+ });
243
+ it('key failures: no deny event, wrong skill path, no subagent event', async () => {
244
+ const guard = fixtureEvents('guardrail').filter((e) => e.type !== 'tool.execution_complete');
245
+ const skill = fixtureEvents('skill').map((e) => (e.type === 'skill.invoked' ? { ...e, data: { ...e.data, path: '/x/skills/other/SKILL.md' } } : e));
246
+ const delegate = fixtureEvents('delegate').filter((e) => !String(e.type).startsWith('subagent.'));
247
+ const { loaded } = fakeSdk({ events: { guardrail: guard, skill, delegate } });
248
+ const c = byId((await runSdkTier(input(loaded, { scenarios: ['guardrail', 'skill', 'delegate'] }))).checks);
249
+ expect(c['scn.guardrail.denied'].ok).toBe(false);
250
+ expect(c['scn.skill.invoked'].ok).toBe(false);
251
+ expect(c['scn.delegate.selected'].ok).toBe(false);
252
+ expect(c['scn.delegate.completed'].ok).toBe(false);
253
+ });
254
+ it('runs chain through the chain runner, never an SDK scenario session, and totals both links', async () => {
255
+ const { loaded, rec } = fakeSdk();
256
+ const evidence = JSON.parse(readFileSync(join(__dirname, 'fixtures', 'tier2-chain-evidence.json'), 'utf-8'));
257
+ const calls = [];
258
+ const result = await runSdkTier(input(loaded, {
259
+ scenarios: ['chain'],
260
+ runChain: async (args) => {
261
+ calls.push(args);
262
+ return { name: 'chain', events: [], idle: true, timedOut: false, timeoutMs: args.timeoutMs, model: 'm', maxCredits: args.maxCredits, budget: args.budget, capped: false, wedged: false, chain: evidence };
263
+ },
264
+ }));
265
+ expect(calls).toHaveLength(1);
266
+ expect(calls[0]).toMatchObject({ projectDir: expect.any(String), home: expect.any(String), eventsPath: expect.stringMatching(/events-chain\.jsonl$/) });
267
+ expect(rec.sessions).toHaveLength(1); // the static-check session only
268
+ const c = byId(result.checks);
269
+ for (const id of ['link1', 'spawned', 'inherited', 'closed', 'premium'])
270
+ expect(c[`scn.chain.${id}`].ok).toBe(true);
271
+ expect(result.cost.premiumRequests).toBe(2);
272
+ });
273
+ it('a pushed ref on the bare remote fails no_push', async () => {
274
+ const { loaded } = fakeSdk();
275
+ const c = byId((await runSdkTier(input(loaded, { scenarios: ['guardrail'], spawnSync: gitSpawnSync([], 'abc commit\trefs/heads/main\n') }))).checks);
276
+ expect(c['scn.guardrail.no_push'].ok).toBe(false);
277
+ });
278
+ it('adapter errors come from the log lines written during the scenario (fail-open timeout not masked)', async () => {
279
+ const { loaded } = fakeSdk({ log: LOG });
280
+ const c = byId((await runSdkTier(input(loaded, { scenarios: ['smoke'] }))).checks)['scn.smoke.adapter_errors'];
281
+ expect(c).toMatchObject({ ok: false, detail: '1 hook stderr line(s) from wiki-session-end.mjs' });
282
+ });
283
+ it('aborts on timeout and still cleans up', async () => {
284
+ const { loaded, rec } = fakeSdk({ hang: ['smoke'] });
285
+ const result = await runSdkTier(input(loaded, { scenarios: ['smoke'], timeoutMs: 50 }));
286
+ expect(byId(result.checks)['scn.smoke.exit']).toMatchObject({ ok: false, detail: 'no session.idle within 50 ms (aborted)' });
287
+ expect(rec.ops).toEqual(['start', 'create:s1', 'tools.init:s1', 'disconnect:s1', 'delete:s1', 'create:s2', 'send:s2', 'abort:s2', 'disconnect:s2', 'delete:s2', 'stop']);
288
+ });
289
+ it('cleanup runs in finally when a session throws: disconnect, deleteSession, then stop', async () => {
290
+ const { loaded, rec } = fakeSdk({ sendError: 'transport closed' });
291
+ const result = await runSdkTier(input(loaded, { scenarios: ['smoke'] }));
292
+ expect(byId(result.checks)['scn.smoke.exit']).toMatchObject({ ok: false, detail: 'session failed: transport closed' });
293
+ expect(rec.ops.slice(-4)).toEqual(['send:s2', 'disconnect:s2', 'delete:s2', 'stop']);
294
+ });
295
+ it('keepHome keeps the on-disk session state (no deleteSession)', async () => {
296
+ const { loaded, rec } = fakeSdk();
297
+ await runSdkTier(input(loaded, { scenarios: ['smoke'], keepHome: true }));
298
+ expect(rec.ops.some((o) => o.startsWith('delete:'))).toBe(false);
299
+ expect(rec.ops.at(-1)).toBe('stop');
300
+ });
301
+ it('a runtime that does not start fails the rest with the reason and still stops the client', async () => {
302
+ const { loaded, rec } = fakeSdk({ startError: 'spawn EACCES' });
303
+ const result = await runSdkTier(input(loaded, { scenarios: ['smoke'] }));
304
+ const c = byId(result.checks);
305
+ expect(c['sdk.available'].ok).toBe(true);
306
+ expect(c['sdk.runtime'].detail).toMatch(/runtime did not start .*spawn EACCES/);
307
+ expect(c['scn.smoke.reply'].ok).toBe(false);
308
+ expect(rec.ops).toEqual(['start', 'stop']);
309
+ });
310
+ it('uses a listed cheap model explicitly and records it', async () => {
311
+ const { loaded, rec } = fakeSdk({ models: ['auto', 'claude-opus-5-5', 'gpt-5-mini'] });
312
+ const result = await runSdkTier(input(loaded, { scenarios: ['smoke'] }));
313
+ expect(rec.sessions[1].model).toBe('gpt-5-mini');
314
+ expect(result.sdk?.model).toBe('gpt-5-mini');
315
+ expect(byId(result.checks)['scn.smoke.cost'].detail).toMatch(/model gpt-5-mini/);
316
+ });
317
+ });
318
+ describe('runSdkTier credit cap, log windows, tree kill', () => {
319
+ it('maxCredits is a cap: the scenario passing it is aborted and the rest are skipped', async () => {
320
+ const { loaded, rec } = fakeSdk();
321
+ const result = await runSdkTier(input(loaded, { scenarios: ['smoke', 'guardrail'], maxCredits: 1e-6 }));
322
+ const c = byId(result.checks);
323
+ expect(c['scn.smoke.exit']).toMatchObject({ ok: false, detail: expect.stringMatching(/^aborted: credit cap reached/) });
324
+ expect(c['scn.smoke.cost'].ok).toBe(false);
325
+ expect(rec.ops).toContain('abort:s2');
326
+ for (const id of scenarioCheckIds('guardrail'))
327
+ expect(c[id]).toMatchObject({ ok: false, detail: CREDIT_CAP_SKIP_DETAIL });
328
+ expect(rec.ops.filter((o) => o.startsWith('send:'))).toEqual(['send:s2']); // guardrail never prompted
329
+ expect(rec.connection.args).toEqual(['--log-level', 'debug', '--max-ai-credits', '30']); // CLI minimum
330
+ });
331
+ it('passes the cap to the runtime when above the CLI minimum', async () => {
332
+ const { loaded, rec } = fakeSdk();
333
+ await runSdkTier(input(loaded, { scenarios: [], maxCredits: 45.5 }));
334
+ expect(rec.connection.args).toEqual(['--log-level', 'debug', '--max-ai-credits', '46']);
335
+ expect(runtimeCreditCap(10)).toBe(30);
336
+ });
337
+ it('a late SessionEnd timeout line of scenario 1 is attributed to scenario 1, not scenario 2', async () => {
338
+ const late = LOG.split('\n').find((l) => l.includes('[run.cjs]'));
339
+ const { loaded } = fakeSdk({ lateLine: { from: 'smoke', during: 'guardrail', text: late } });
340
+ const c = byId((await runSdkTier(input(loaded, { scenarios: ['smoke', 'guardrail'] }))).checks);
341
+ expect(c['scn.smoke.adapter_errors']).toMatchObject({ ok: false, detail: '1 hook stderr line(s) from wiki-session-end.mjs' });
342
+ expect(c['scn.guardrail.adapter_errors'].ok).toBe(true);
343
+ });
344
+ it('kills the runtime tree when the pid outlives stop()', async () => {
345
+ const { loaded, rec } = fakeSdk({ pid: 4242 });
346
+ const killed = [];
347
+ await runSdkTier(input(loaded, { scenarios: [], isAlive: () => killed.length === 0, killTree: (pid) => { killed.push(pid); rec.ops.push(`kill:${pid}`); } }));
348
+ expect(killed).toEqual([4242]);
349
+ expect(rec.ops.slice(-2)).toEqual(['stop', 'kill:4242']);
350
+ });
351
+ it('a failing stop(): tree kill while the runtime lives, then forceStop', async () => {
352
+ const { loaded, rec } = fakeSdk({ pid: 4242, stopError: 'hung' });
353
+ let alive = true;
354
+ await runSdkTier(input(loaded, { scenarios: [], isAlive: () => alive, killTree: (pid) => { alive = false; rec.ops.push(`kill:${pid}`); } }));
355
+ expect(rec.ops.slice(-3)).toEqual(['stop', 'kill:4242', 'forceStop']);
356
+ });
357
+ it('no tree kill when the runtime exited with stop()', async () => {
358
+ const { loaded } = fakeSdk({ pid: 4242 });
359
+ const killed = [];
360
+ await runSdkTier(input(loaded, { scenarios: ['smoke'], isAlive: () => false, killTree: (pid) => { killed.push(pid); } }));
361
+ expect(killed).toEqual([]);
362
+ });
363
+ it('sdk.tools_excluded fails when host_smoke is still offered', async () => {
364
+ const { loaded } = fakeSdk({ toolNames: ['t-state_read', 't-host_smoke'] });
365
+ const c = byId((await runSdkTier(input(loaded, { scenarios: [] }))).checks);
366
+ expect(c['sdk.tools_excluded']).toMatchObject({ ok: false, detail: 't-host_smoke still offered to the model' });
367
+ });
368
+ it('derives the excluded tool from the MCP server name', async () => {
369
+ const { loaded, rec } = fakeSdk({ toolNames: ['omg-state_read'] });
370
+ await runSdkTier(input(loaded, { scenarios: ['smoke'], mcpServer: 'omg' }));
371
+ for (const s of rec.sessions)
372
+ expect(s.excludedTools[0]).toBe('omg-host_smoke');
373
+ });
374
+ it('a Windows .cmd shim is driven through its resolved .js target, or fails with a clear detail', async () => {
375
+ const shimDir = join(parent, 'npm');
376
+ const target = join(shimDir, 'node_modules', '@github', 'copilot', 'npm-loader.js');
377
+ write(target, '// loader');
378
+ const shim = join(shimDir, 'copilot.cmd');
379
+ write(shim, '@ECHO off\r\nGOTO start\r\n:start\r\n"%_prog%" "%dp0%\\node_modules\\@github\\copilot\\npm-loader.js" %*\r\n');
380
+ const { loaded, rec } = fakeSdk();
381
+ await runSdkTier(input(loaded, { scenarios: [], bin: shim, platform: 'win32' }));
382
+ expect(rec.connection.path).toBe(target);
383
+ const bad = join(shimDir, 'other.cmd');
384
+ write(bad, '@ECHO off\r\nnode something-else %*\r\n');
385
+ const fresh = fakeSdk();
386
+ const result = await runSdkTier(input(fresh.loaded, { scenarios: ['smoke'], bin: bad, platform: 'win32' }));
387
+ expect(byId(result.checks)['sdk.runtime'].detail).toMatch(/is a shell shim whose \.js\/\.exe target could not be resolved; pass --copilot-bin/);
388
+ expect(fresh.rec.ops).toEqual([]);
389
+ });
390
+ });
391
+ describe('SDK loading', () => {
392
+ it('globalModuleRoots: only %APPDATA%/npm on win32; no env prefix, no node dir', () => {
393
+ expect(globalModuleRoots({ APPDATA: 'C:\\Users\\u\\AppData\\Roaming', npm_config_prefix: 'C:\\evil' }, 'win32').map((p) => p.replace(/\\/g, '/')))
394
+ .toEqual(['C:/Users/u/AppData/Roaming/npm/node_modules']);
395
+ expect(globalModuleRoots({ npm_config_prefix: '/opt/npm', NPM_CONFIG_PREFIX: '/opt/npm' }, 'linux')).toEqual([]);
396
+ });
397
+ it('satisfiesSdkRange is ^1.0.16', () => {
398
+ for (const v of ['1.0.16', '1.0.17', '1.1.0', '1.20.3'])
399
+ expect(satisfiesSdkRange(v), v).toBe(true);
400
+ for (const v of ['1.0.15', '0.9.9', '2.0.0', '1.0.17-beta.1', '', undefined, 'latest'])
401
+ expect(satisfiesSdkRange(v), String(v)).toBe(false);
402
+ });
403
+ function fakePackage(over, entryFile = 'dist/index.js') {
404
+ const dir = join(parent, 'gm', '@github', 'copilot-sdk');
405
+ write(join(dir, 'package.json'), JSON.stringify({ name: '@github/copilot-sdk', version: '1.0.16', exports: { '.': { import: { default: './dist/index.js' }, require: { default: './dist/cjs/index.js' } } }, ...over }));
406
+ write(join(dir, entryFile), 'export class CopilotClient {}\nexport const RuntimeConnection = { forStdio: (o) => o };\n');
407
+ write(join(dir, 'dist', 'cjs', 'index.js'), 'exports.CopilotClient = class {}; exports.RuntimeConnection = { forStdio: (o) => o };\n');
408
+ write(join(dir, 'dist', 'package.json'), JSON.stringify({ type: 'module' }));
409
+ return dir;
410
+ }
411
+ it('importFromDir loads a matching package', async () => {
412
+ const loaded = await importFromDir(fakePackage({}));
413
+ expect(loaded?.version).toBe('1.0.16');
414
+ expect(typeof loaded?.module.CopilotClient).toBe('function');
415
+ });
416
+ it('importFromDir refuses another name, an out-of-range version, or an entry outside the package', async () => {
417
+ expect(await importFromDir(fakePackage({ name: '@evil/copilot-sdk' }))).toBeNull();
418
+ expect(await importFromDir(fakePackage({ version: '0.9.0' }))).toBeNull();
419
+ expect(await importFromDir(fakePackage({ version: '2.0.0' }))).toBeNull();
420
+ expect(await importFromDir(fakePackage({ exports: { '.': { import: '../../../outside.js', require: './dist/cjs/index.js' } } }))).toBeNull();
421
+ expect(await importFromDir(fakePackage({ exports: undefined, main: '../outside.js' }))).toBeNull();
422
+ });
423
+ it('resolveShimTarget reads cmd and ps1 shims, null when the target is missing', () => {
424
+ const files = {
425
+ 'C:/npm/copilot.cmd': '"%_prog%" "%dp0%\\node_modules\\@github\\copilot\\npm-loader.js" %*',
426
+ 'C:/npm/copilot.ps1': '& "$basedir/node_modules/@github/copilot/npm-loader.js" $args',
427
+ 'C:/npm/plain.cmd': 'node foo %*',
428
+ };
429
+ const read = (p) => { const v = files[p.replace(/\\/g, '/')]; if (v === undefined)
430
+ throw new Error('ENOENT'); return v; };
431
+ const exists = (p) => /npm-loader\.js$/.test(p);
432
+ expect(resolveShimTarget('C:/npm/copilot.cmd', read, exists)?.replace(/\\/g, '/')).toMatch(/\/npm\/node_modules\/@github\/copilot\/npm-loader\.js$/);
433
+ expect(resolveShimTarget('C:/npm/copilot.ps1', read, exists)?.replace(/\\/g, '/')).toMatch(/\/npm\/node_modules\/@github\/copilot\/npm-loader\.js$/);
434
+ expect(resolveShimTarget('C:/npm/plain.cmd', read, exists)).toBeNull();
435
+ expect(resolveShimTarget('C:/npm/missing.cmd', read, exists)).toBeNull();
436
+ expect(resolveShimTarget('C:/npm/copilot.cmd', read, () => false)).toBeNull();
437
+ });
438
+ });
439
+ // ---------------------------------------------------------------------------
440
+ // Through runCopilotSmoke (tier 0 faked like copilot-smoke.test.ts)
441
+ // ---------------------------------------------------------------------------
442
+ function tier0SpawnSync() {
443
+ return ((command, args) => {
444
+ const ok = (stdout) => ({ status: 0, stdout, stderr: '', pid: 1, output: [], signal: null });
445
+ if (args.includes('--version'))
446
+ return ok('GitHub Copilot CLI 1.0.91.\n');
447
+ if (args.includes('plugin'))
448
+ return ok(JSON.stringify([{ name: 'oh-my-copilot', version: '9.9.9', source: 'external' }]));
449
+ if (args.includes('skill'))
450
+ return ok(JSON.stringify(['alpha', 'plan'].map((s) => ({ name: s, source: 'plugin', path: join(root, 'skills', s) }))));
451
+ if (command === 'git' && args.includes('for-each-ref'))
452
+ return ok('');
453
+ return ok('');
454
+ });
455
+ }
456
+ function mcpSpawn() {
457
+ return (() => {
458
+ const child = new EventEmitter();
459
+ child.stdin = new PassThrough();
460
+ child.stdout = new PassThrough();
461
+ child.stderr = new PassThrough();
462
+ child.kill = () => true;
463
+ let buf = '';
464
+ child.stdin.on('data', (chunk) => {
465
+ buf += chunk.toString();
466
+ let nl;
467
+ while ((nl = buf.indexOf('\n')) !== -1) {
468
+ const msg = JSON.parse(buf.slice(0, nl));
469
+ buf = buf.slice(nl + 1);
470
+ if (msg.id === 1)
471
+ child.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} })}\n`);
472
+ if (msg.id === 2)
473
+ child.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, result: { tools: [{}, {}, {}] } })}\n`);
474
+ }
475
+ });
476
+ return child;
477
+ });
478
+ }
479
+ function smokeDeps(loadSdk) {
480
+ const userConfigDir = join(root, 'user-copilot');
481
+ write(join(userConfigDir, 'config.json'), JSON.stringify({ loggedInUsers: [{ login: 'me' }], lastLoggedInUser: { login: 'me' } }));
482
+ return {
483
+ spawnSync: tier0SpawnSync(),
484
+ spawn: mcpSpawn(),
485
+ resolveExecutable: () => join(root, 'copilot.exe'),
486
+ existsSync: () => true,
487
+ loadExpectedToolCount: async () => 3,
488
+ userConfigDir,
489
+ loadSdk,
490
+ };
491
+ }
492
+ describe('runCopilotSmoke tier 2', () => {
493
+ it('SDK missing: tier 0 runs, the report is skipped, exit code 2', async () => {
494
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 2, env: {}, deps: smokeDeps(async () => null) });
495
+ expect(report.tier).toBe(2);
496
+ expect(report.skipped).toBe(SDK_MISSING_DETAIL);
497
+ expect(report.checks.filter((c) => !c.ok).every((c) => c.detail === SDK_MISSING_DETAIL)).toBe(true);
498
+ expect(report.checks.find((c) => c.id === 'mcp.list_tools')?.ok).toBe(true);
499
+ expect(smokeExitCode(report)).toBe(2);
500
+ expect(report.sdk).toBeUndefined();
501
+ });
502
+ it('default scenarios smoke+guardrail with report.sdk, report.cost and per-scenario events (removed without keepHome)', async () => {
503
+ const fake = fakeSdkFor();
504
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 2, env: { OMC_STATE_DIR: '/elsewhere' }, deps: smokeDeps(async () => fake.loaded) });
505
+ expect(report.checks.filter((c) => !c.ok)).toEqual([]);
506
+ expect(report.checks.filter((c) => c.id.startsWith('scn.')).map((c) => c.id)).toEqual([...scenarioCheckIds('smoke'), ...scenarioCheckIds('guardrail')]);
507
+ expect(report.sdk).toMatchObject({ version: '1.0.16', runtimeVersion: '1.0.91', protocolVersion: 3 });
508
+ expect(report.cost.premiumRequests).toBe(2);
509
+ expect(report.artifacts).toEqual({});
510
+ const env = fake.rec.connection.env;
511
+ expect(env).toMatchObject({ OMC_HOOK_FAIL_CLOSED: '1', OMC_GIT_GUARDRAILS: '1', COPILOT_ALLOW_ALL: 'false' });
512
+ expect(env.OMC_STATE_DIR).toBeUndefined();
513
+ expect(existsSync(env.COPILOT_HOME)).toBe(false);
514
+ const config = fake.configAtStart;
515
+ expect(config).toMatchObject({ loggedInUsers: [{ login: 'me' }] });
516
+ expect(config.trustedFolders[0]).toMatch(/omg-smoke-sdk-.*project$/);
517
+ });
518
+ it('CI shape: no stored login, the token env reaches the runtime and config.json carries no identity', async () => {
519
+ const fake = fakeSdkFor();
520
+ const deps = { ...smokeDeps(async () => fake.loaded), userConfigDir: join(root, 'no-login') };
521
+ const env = { COPILOT_GITHUB_TOKEN: 'ci-token', GH_TOKEN: 'gh', GITHUB_TOKEN: 'gha' };
522
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 2, scenarios: [], env, deps });
523
+ expect(report.checks.filter((c) => !c.ok)).toEqual([]);
524
+ expect(fake.rec.connection.env).toMatchObject(env);
525
+ expect(fake.configAtStart).not.toHaveProperty('loggedInUsers');
526
+ expect(fake.configAtStart).not.toHaveProperty('lastLoggedInUser');
527
+ });
528
+ it('keepHome keeps the event captures', async () => {
529
+ const fake = fakeSdkFor();
530
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 2, scenarios: ['smoke'], env: {}, keepHome: true, deps: smokeDeps(async () => fake.loaded) });
531
+ expect(existsSync(report.artifacts.events.smoke)).toBe(true);
532
+ rmSync(join(report.artifacts.copilotHome, '..'), { recursive: true, force: true });
533
+ if (report.artifacts.listHome)
534
+ rmSync(report.artifacts.listHome, { recursive: true, force: true });
535
+ });
536
+ it('no-billing guard: tier 2 scenarios without an injected loadSdk are refused under vitest, zero spawns', async () => {
537
+ let spawns = 0;
538
+ const count = (() => { spawns++; throw new Error('must not spawn'); });
539
+ for (const scenarios of [undefined, ['smoke']]) {
540
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 2, ...(scenarios ? { scenarios } : {}), env: {}, deps: { spawnSync: count, spawn: count } });
541
+ expect(report).toMatchObject({ ok: false, skipped: LIVE_RUN_REFUSED_DETAIL, checks: [{ id: 'cli.guard', ok: false, detail: LIVE_RUN_REFUSED_DETAIL }] });
542
+ expect(smokeExitCode(report)).not.toBe(0);
543
+ }
544
+ expect(spawns).toBe(0);
545
+ expect(LIVE_RUN_REFUSED_DETAIL).toBe('live run refused under vitest — set OMC_LIVE_SMOKE=1 (tier 1) / =2 (tier 2 scenarios)');
546
+ });
547
+ it('no-billing guard: tier 1 without an injected spawn is refused', async () => {
548
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 1, env: {}, deps: { spawnSync: tier0SpawnSync() } });
549
+ expect(report.checks).toEqual([{ id: 'cli.guard', ok: false, detail: LIVE_RUN_REFUSED_DETAIL }]);
550
+ });
551
+ it('no-billing guard: scenarios: [] (static only) still runs', async () => {
552
+ const report = await runCopilotSmoke({ pluginRoot: root, tier: 2, scenarios: [], env: {}, deps: smokeDeps(async () => null) });
553
+ expect(report.checks.some((c) => c.id === 'cli.guard')).toBe(false);
554
+ expect(report.checks.some((c) => c.id === 'plugin.manifest')).toBe(true);
555
+ expect(liveRunRefusal(2, [], {}, { VITEST: 'true' })).toBeNull();
556
+ });
557
+ it('no-billing guard: env and seam rules', () => {
558
+ const test = { VITEST: 'true' };
559
+ expect(liveRunRefusal(2, undefined, {}, { ...test, OMC_LIVE_SMOKE: '2' })).toBeNull();
560
+ expect(liveRunRefusal(2, undefined, {}, { ...test, OMC_LIVE_SMOKE: '1' })).toBe(LIVE_RUN_REFUSED_DETAIL);
561
+ expect(liveRunRefusal(1, undefined, {}, { ...test, OMC_LIVE_SMOKE: '1' })).toBeNull();
562
+ expect(liveRunRefusal(1, undefined, {}, { NODE_ENV: 'test' })).toBe(LIVE_RUN_REFUSED_DETAIL);
563
+ expect(liveRunRefusal(1, undefined, { loadSdk: async () => null }, test)).toBe(LIVE_RUN_REFUSED_DETAIL);
564
+ expect(liveRunRefusal(2, undefined, { spawn: (() => null) }, test)).toBe(LIVE_RUN_REFUSED_DETAIL);
565
+ expect(liveRunRefusal(2, undefined, { loadSdk: async () => null }, test)).toBeNull();
566
+ expect(liveRunRefusal(0, undefined, {}, test)).toBeNull();
567
+ expect(liveRunRefusal(2, undefined, {}, {})).toBeNull(); // not under a test runner
568
+ });
569
+ it('rejects unknown scenario names', async () => {
570
+ await expect(runCopilotSmoke({ pluginRoot: root, tier: 2, scenarios: ['nope'], env: {}, deps: smokeDeps(async () => null) }))
571
+ .rejects.toThrow(/unknown smoke scenario\(s\): nope/);
572
+ });
573
+ });
574
+ /** fakeSdk bound to whatever home runCopilotSmoke creates (logs dir made at start, config captured). */
575
+ function fakeSdkFor() {
576
+ const inner = fakeSdk();
577
+ const out = { loaded: inner.loaded, rec: inner.rec };
578
+ const Base = inner.loaded.module.CopilotClient;
579
+ class HomeClient extends Base {
580
+ home;
581
+ constructor(o) { super(o); this.home = o.baseDirectory; }
582
+ async start() {
583
+ mkdirSync(join(this.home, 'logs'), { recursive: true });
584
+ out.configAtStart = JSON.parse(readFileSync(join(this.home, 'config.json'), 'utf-8'));
585
+ return super.start();
586
+ }
587
+ }
588
+ out.loaded = { ...inner.loaded, module: { ...inner.loaded.module, CopilotClient: HomeClient } };
589
+ return out;
590
+ }
591
+ //# sourceMappingURL=copilot-sdk-driver.test.js.map