agent.test.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. import { afterEach, test, expect } from "bun:test"
  2. import path from "path"
  3. import { tmpdir } from "../fixture/fixture"
  4. import { Instance } from "../../src/project/instance"
  5. import { Agent } from "../../src/agent/agent"
  6. import { Permission } from "../../src/permission"
  7. // Helper to evaluate permission for a tool with wildcard pattern
  8. function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
  9. if (!agent) return undefined
  10. return Permission.evaluate(permission, "*", agent.permission).action
  11. }
  12. afterEach(async () => {
  13. await Instance.disposeAll()
  14. })
  15. test("returns default native agents when no config", async () => {
  16. await using tmp = await tmpdir()
  17. await Instance.provide({
  18. directory: tmp.path,
  19. fn: async () => {
  20. const agents = await Agent.list()
  21. const names = agents.map((a) => a.name)
  22. expect(names).toContain("code") // kilocode_change
  23. expect(names).toContain("plan")
  24. expect(names).toContain("debug") // kilocode_change
  25. expect(names).toContain("orchestrator") // kilocode_change
  26. expect(names).toContain("ask") // kilocode_change
  27. expect(names).toContain("general")
  28. expect(names).toContain("explore")
  29. expect(names).toContain("compaction")
  30. expect(names).toContain("title")
  31. expect(names).toContain("summary")
  32. },
  33. })
  34. })
  35. // kilocode_change start - renamed from "build" to "code"
  36. test("code agent has correct default properties", async () => {
  37. await using tmp = await tmpdir()
  38. await Instance.provide({
  39. directory: tmp.path,
  40. fn: async () => {
  41. const code = await Agent.get("code")
  42. expect(code).toBeDefined()
  43. expect(code?.mode).toBe("primary")
  44. expect(code?.native).toBe(true)
  45. expect(evalPerm(code, "edit")).toBe("allow")
  46. expect(evalPerm(code, "bash")).toBe("ask")
  47. },
  48. })
  49. })
  50. // kilocode_change end
  51. // kilocode_change start - ask agent tests
  52. test("ask agent has correct default properties", async () => {
  53. await using tmp = await tmpdir()
  54. await Instance.provide({
  55. directory: tmp.path,
  56. fn: async () => {
  57. const ask = await Agent.get("ask")
  58. expect(ask).toBeDefined()
  59. expect(ask?.mode).toBe("primary")
  60. expect(ask?.native).toBe(true)
  61. // ask agent should allow read-only tools
  62. expect(evalPerm(ask, "read")).toBe("allow")
  63. expect(evalPerm(ask, "grep")).toBe("allow")
  64. expect(evalPerm(ask, "glob")).toBe("allow")
  65. expect(evalPerm(ask, "webfetch")).toBe("allow")
  66. expect(evalPerm(ask, "websearch")).toBe("allow")
  67. expect(evalPerm(ask, "codesearch")).toBe("allow")
  68. // ask agent should deny edit and bash
  69. expect(evalPerm(ask, "edit")).toBe("deny")
  70. expect(evalPerm(ask, "bash")).toBe("deny")
  71. expect(evalPerm(ask, "task")).toBe("deny")
  72. // ask agent should gate .env files
  73. expect(Permission.evaluate("read", ".env", ask!.permission).action).toBe("ask")
  74. expect(Permission.evaluate("read", "config.env.local", ask!.permission).action).toBe("ask")
  75. expect(Permission.evaluate("read", ".env.example", ask!.permission).action).toBe("allow")
  76. expect(Permission.evaluate("read", "src/index.ts", ask!.permission).action).toBe("allow")
  77. },
  78. })
  79. })
  80. test("ask agent denies edit/write/bash even when user config adds a specific edit allow", async () => {
  81. await using tmp = await tmpdir({
  82. config: {
  83. permission: {
  84. edit: { "src/output.log": "allow" },
  85. },
  86. },
  87. })
  88. await Instance.provide({
  89. directory: tmp.path,
  90. fn: async () => {
  91. const ask = await Agent.get("ask")
  92. expect(ask).toBeDefined()
  93. // user config must not leak edit capability into ask mode — even for the
  94. // specific path the user allowed, ask mode must still deny it
  95. expect(Permission.evaluate("edit", "src/output.log", ask!.permission).action).toBe("deny")
  96. expect(evalPerm(ask, "bash")).toBe("deny")
  97. expect(evalPerm(ask, "task")).toBe("deny")
  98. // safe tools still work
  99. expect(evalPerm(ask, "read")).toBe("allow")
  100. expect(evalPerm(ask, "grep")).toBe("allow")
  101. // disabled() hides tools entirely from LLM — bash is NOT disabled because it has specific allow rules
  102. const disabled = Permission.disabled(["edit", "write", "bash"], ask!.permission)
  103. expect(disabled.has("edit")).toBe(true)
  104. expect(disabled.has("write")).toBe(true)
  105. expect(disabled.has("bash")).toBe(false)
  106. },
  107. })
  108. })
  109. // kilocode_change end
  110. // kilocode_change start
  111. test("plan agent asks before edits except .kilo/plans/* and .opencode/plans/*", async () => {
  112. await using tmp = await tmpdir()
  113. await Instance.provide({
  114. directory: tmp.path,
  115. fn: async () => {
  116. const plan = await Agent.get("plan")
  117. expect(plan).toBeDefined()
  118. // Wildcard requires permission
  119. expect(evalPerm(plan, "edit")).toBe("ask")
  120. // kilocode_change start
  121. // .kilo/plans/ is the primary allowed path
  122. expect(Permission.evaluate("edit", ".kilo/plans/foo.md", plan!.permission).action).toBe("allow")
  123. // kilocode_change end
  124. // .opencode/plans/ is also allowed as backward compat fallback
  125. expect(Permission.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow")
  126. },
  127. })
  128. })
  129. // kilocode_change end
  130. test("explore agent denies edit and write", async () => {
  131. await using tmp = await tmpdir()
  132. await Instance.provide({
  133. directory: tmp.path,
  134. fn: async () => {
  135. const explore = await Agent.get("explore")
  136. expect(explore).toBeDefined()
  137. expect(explore?.mode).toBe("subagent")
  138. expect(evalPerm(explore, "edit")).toBe("deny")
  139. expect(evalPerm(explore, "write")).toBe("deny")
  140. expect(evalPerm(explore, "todowrite")).toBe("deny")
  141. },
  142. })
  143. })
  144. test("explore agent asks for external directories and allows Truncate.GLOB", async () => {
  145. const { Truncate } = await import("../../src/tool/truncate")
  146. await using tmp = await tmpdir()
  147. await Instance.provide({
  148. directory: tmp.path,
  149. fn: async () => {
  150. const explore = await Agent.get("explore")
  151. expect(explore).toBeDefined()
  152. expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
  153. expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
  154. },
  155. })
  156. })
  157. test("general agent denies todo tools", async () => {
  158. await using tmp = await tmpdir()
  159. await Instance.provide({
  160. directory: tmp.path,
  161. fn: async () => {
  162. const general = await Agent.get("general")
  163. expect(general).toBeDefined()
  164. expect(general?.mode).toBe("subagent")
  165. expect(general?.hidden).toBeUndefined()
  166. expect(evalPerm(general, "todowrite")).toBe("deny")
  167. },
  168. })
  169. })
  170. test("compaction agent denies all permissions", async () => {
  171. await using tmp = await tmpdir()
  172. await Instance.provide({
  173. directory: tmp.path,
  174. fn: async () => {
  175. const compaction = await Agent.get("compaction")
  176. expect(compaction).toBeDefined()
  177. expect(compaction?.hidden).toBe(true)
  178. expect(evalPerm(compaction, "bash")).toBe("deny")
  179. expect(evalPerm(compaction, "edit")).toBe("deny")
  180. expect(evalPerm(compaction, "read")).toBe("deny")
  181. },
  182. })
  183. })
  184. test("custom agent from config creates new agent", async () => {
  185. await using tmp = await tmpdir({
  186. config: {
  187. agent: {
  188. my_custom_agent: {
  189. model: "openai/gpt-4",
  190. description: "My custom agent",
  191. temperature: 0.5,
  192. top_p: 0.9,
  193. },
  194. },
  195. },
  196. })
  197. await Instance.provide({
  198. directory: tmp.path,
  199. fn: async () => {
  200. const custom = await Agent.get("my_custom_agent")
  201. expect(custom).toBeDefined()
  202. expect(String(custom?.model?.providerID)).toBe("openai")
  203. expect(String(custom?.model?.modelID)).toBe("gpt-4")
  204. expect(custom?.description).toBe("My custom agent")
  205. expect(custom?.temperature).toBe(0.5)
  206. expect(custom?.topP).toBe(0.9)
  207. expect(custom?.native).toBe(false)
  208. expect(custom?.mode).toBe("all")
  209. },
  210. })
  211. })
  212. test("custom agent config overrides native agent properties", async () => {
  213. await using tmp = await tmpdir({
  214. config: {
  215. agent: {
  216. // kilocode_change start
  217. code: {
  218. model: "anthropic/claude-3",
  219. description: "Custom code agent",
  220. temperature: 0.7,
  221. color: "#FF0000",
  222. },
  223. // kilocode_change end
  224. },
  225. },
  226. })
  227. await Instance.provide({
  228. directory: tmp.path,
  229. fn: async () => {
  230. // kilocode_change start - renamed from "build" to "code"
  231. const code = await Agent.get("code")
  232. expect(code).toBeDefined()
  233. expect(String(code?.model?.providerID)).toBe("anthropic")
  234. expect(String(code?.model?.modelID)).toBe("claude-3")
  235. expect(code?.description).toBe("Custom code agent")
  236. expect(code?.temperature).toBe(0.7)
  237. expect(code?.color).toBe("#FF0000")
  238. expect(code?.native).toBe(true)
  239. // kilocode_change end
  240. },
  241. })
  242. })
  243. test("agent disable removes agent from list", async () => {
  244. await using tmp = await tmpdir({
  245. config: {
  246. agent: {
  247. explore: { disable: true },
  248. },
  249. },
  250. })
  251. await Instance.provide({
  252. directory: tmp.path,
  253. fn: async () => {
  254. const explore = await Agent.get("explore")
  255. expect(explore).toBeUndefined()
  256. const agents = await Agent.list()
  257. const names = agents.map((a) => a.name)
  258. expect(names).not.toContain("explore")
  259. },
  260. })
  261. })
  262. test("agent permission config merges with defaults", async () => {
  263. await using tmp = await tmpdir({
  264. config: {
  265. agent: {
  266. // kilocode_change start
  267. code: {
  268. // kilocode_change end
  269. permission: {
  270. bash: {
  271. "rm -rf *": "deny",
  272. },
  273. },
  274. },
  275. },
  276. },
  277. })
  278. await Instance.provide({
  279. directory: tmp.path,
  280. fn: async () => {
  281. // kilocode_change start - renamed from "build" to "code"
  282. const code = await Agent.get("code")
  283. expect(code).toBeDefined()
  284. // Specific pattern is denied
  285. expect(Permission.evaluate("bash", "rm -rf *", code!.permission).action).toBe("deny")
  286. // Edit still allowed
  287. expect(evalPerm(code, "edit")).toBe("allow")
  288. // kilocode_change end
  289. },
  290. })
  291. })
  292. test("global permission config applies to all agents", async () => {
  293. await using tmp = await tmpdir({
  294. config: {
  295. permission: {
  296. bash: "deny",
  297. },
  298. },
  299. })
  300. await Instance.provide({
  301. directory: tmp.path,
  302. fn: async () => {
  303. // kilocode_change start - renamed from "build" to "code"
  304. const code = await Agent.get("code")
  305. expect(code).toBeDefined()
  306. expect(evalPerm(code, "bash")).toBe("deny")
  307. // kilocode_change end
  308. },
  309. })
  310. })
  311. test("agent steps/maxSteps config sets steps property", async () => {
  312. await using tmp = await tmpdir({
  313. config: {
  314. agent: {
  315. // kilocode_change start - renamed from "build" to "code"
  316. code: { steps: 50 },
  317. // kilocode_change end
  318. plan: { maxSteps: 100 },
  319. },
  320. },
  321. })
  322. await Instance.provide({
  323. directory: tmp.path,
  324. fn: async () => {
  325. const code = await Agent.get("code") // kilocode_change
  326. const plan = await Agent.get("plan")
  327. expect(code?.steps).toBe(50) // kilocode_change
  328. expect(plan?.steps).toBe(100)
  329. },
  330. })
  331. })
  332. test("agent mode can be overridden", async () => {
  333. await using tmp = await tmpdir({
  334. config: {
  335. agent: {
  336. explore: { mode: "primary" },
  337. },
  338. },
  339. })
  340. await Instance.provide({
  341. directory: tmp.path,
  342. fn: async () => {
  343. const explore = await Agent.get("explore")
  344. expect(explore?.mode).toBe("primary")
  345. },
  346. })
  347. })
  348. test("agent name can be overridden", async () => {
  349. await using tmp = await tmpdir({
  350. config: {
  351. agent: {
  352. code: { name: "Coder" }, // kilocode_change
  353. },
  354. },
  355. })
  356. await Instance.provide({
  357. directory: tmp.path,
  358. fn: async () => {
  359. // kilocode_change start - renamed from "build" to "code"
  360. const code = await Agent.get("code")
  361. expect(code?.name).toBe("Coder")
  362. // kilocode_change end
  363. },
  364. })
  365. })
  366. test("agent prompt can be set from config", async () => {
  367. await using tmp = await tmpdir({
  368. config: {
  369. agent: {
  370. code: { prompt: "Custom system prompt" }, // kilocode_change
  371. },
  372. },
  373. })
  374. await Instance.provide({
  375. directory: tmp.path,
  376. fn: async () => {
  377. // kilocode_change start - renamed from "build" to "code"
  378. const code = await Agent.get("code")
  379. expect(code?.prompt).toBe("Custom system prompt")
  380. // kilocode_change end
  381. },
  382. })
  383. })
  384. test("unknown agent properties are placed into options", async () => {
  385. await using tmp = await tmpdir({
  386. config: {
  387. agent: {
  388. code: {
  389. random_property: "hello",
  390. another_random: 123,
  391. },
  392. },
  393. },
  394. })
  395. await Instance.provide({
  396. directory: tmp.path,
  397. fn: async () => {
  398. // kilocode_change start - renamed from "build" to "code"
  399. const code = await Agent.get("code")
  400. expect(code?.options.random_property).toBe("hello")
  401. expect(code?.options.another_random).toBe(123)
  402. // kilocode_change end
  403. },
  404. })
  405. })
  406. test("agent options merge correctly", async () => {
  407. await using tmp = await tmpdir({
  408. config: {
  409. agent: {
  410. // kilocode_change start - renamed from "build" to "code"
  411. code: {
  412. // kilocode_change end
  413. options: {
  414. custom_option: true,
  415. another_option: "value",
  416. },
  417. },
  418. },
  419. },
  420. })
  421. await Instance.provide({
  422. directory: tmp.path,
  423. fn: async () => {
  424. // kilocode_change start - renamed from "build" to "code"
  425. const code = await Agent.get("code")
  426. expect(code?.options.custom_option).toBe(true)
  427. expect(code?.options.another_option).toBe("value")
  428. // kilocode_change end
  429. },
  430. })
  431. })
  432. test("multiple custom agents can be defined", async () => {
  433. await using tmp = await tmpdir({
  434. config: {
  435. agent: {
  436. agent_a: {
  437. description: "Agent A",
  438. mode: "subagent",
  439. },
  440. agent_b: {
  441. description: "Agent B",
  442. mode: "primary",
  443. },
  444. },
  445. },
  446. })
  447. await Instance.provide({
  448. directory: tmp.path,
  449. fn: async () => {
  450. const agentA = await Agent.get("agent_a")
  451. const agentB = await Agent.get("agent_b")
  452. expect(agentA?.description).toBe("Agent A")
  453. expect(agentA?.mode).toBe("subagent")
  454. expect(agentB?.description).toBe("Agent B")
  455. expect(agentB?.mode).toBe("primary")
  456. },
  457. })
  458. })
  459. test("Agent.list keeps the default agent first and sorts the rest by name", async () => {
  460. await using tmp = await tmpdir({
  461. config: {
  462. default_agent: "plan",
  463. agent: {
  464. zebra: {
  465. description: "Zebra",
  466. mode: "subagent",
  467. },
  468. alpha: {
  469. description: "Alpha",
  470. mode: "subagent",
  471. },
  472. },
  473. },
  474. })
  475. await Instance.provide({
  476. directory: tmp.path,
  477. fn: async () => {
  478. const names = (await Agent.list()).map((a) => a.name)
  479. expect(names[0]).toBe("plan")
  480. expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
  481. },
  482. })
  483. })
  484. test("Agent.get returns undefined for non-existent agent", async () => {
  485. await using tmp = await tmpdir()
  486. await Instance.provide({
  487. directory: tmp.path,
  488. fn: async () => {
  489. const nonExistent = await Agent.get("does_not_exist")
  490. expect(nonExistent).toBeUndefined()
  491. },
  492. })
  493. })
  494. test("default permission includes doom_loop and external_directory as ask", async () => {
  495. await using tmp = await tmpdir()
  496. await Instance.provide({
  497. directory: tmp.path,
  498. fn: async () => {
  499. // kilocode_change start - renamed from "build" to "code"
  500. const code = await Agent.get("code")
  501. expect(evalPerm(code, "doom_loop")).toBe("ask")
  502. expect(evalPerm(code, "external_directory")).toBe("ask")
  503. // kilocode_change end
  504. },
  505. })
  506. })
  507. test("webfetch is allowed by default", async () => {
  508. await using tmp = await tmpdir()
  509. await Instance.provide({
  510. directory: tmp.path,
  511. fn: async () => {
  512. // kilocode_change start - renamed from "build" to "code"
  513. const code = await Agent.get("code")
  514. expect(evalPerm(code, "webfetch")).toBe("allow")
  515. // kilocode_change end
  516. },
  517. })
  518. })
  519. test("legacy tools config converts to permissions", async () => {
  520. await using tmp = await tmpdir({
  521. config: {
  522. agent: {
  523. // kilocode_change start - renamed from "build" to "code"
  524. code: {
  525. // kilocode_change end
  526. tools: {
  527. bash: false,
  528. read: false,
  529. },
  530. },
  531. },
  532. },
  533. })
  534. await Instance.provide({
  535. directory: tmp.path,
  536. fn: async () => {
  537. // kilocode_change start - renamed from "build" to "code"
  538. const code = await Agent.get("code")
  539. expect(evalPerm(code, "bash")).toBe("deny")
  540. expect(evalPerm(code, "read")).toBe("deny")
  541. // kilocode_change end
  542. },
  543. })
  544. })
  545. test("legacy tools config maps write/edit/patch/multiedit to edit permission", async () => {
  546. await using tmp = await tmpdir({
  547. config: {
  548. agent: {
  549. // kilocode_change start - renamed from "build" to "code"
  550. code: {
  551. // kilocode_change end
  552. tools: {
  553. write: false,
  554. },
  555. },
  556. },
  557. },
  558. })
  559. await Instance.provide({
  560. directory: tmp.path,
  561. fn: async () => {
  562. // kilocode_change start - renamed from "build" to "code"
  563. const code = await Agent.get("code")
  564. expect(evalPerm(code, "edit")).toBe("deny")
  565. // kilocode_change end
  566. },
  567. })
  568. })
  569. test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => {
  570. const { Truncate } = await import("../../src/tool/truncate")
  571. await using tmp = await tmpdir({
  572. config: {
  573. permission: {
  574. external_directory: "deny",
  575. },
  576. },
  577. })
  578. await Instance.provide({
  579. directory: tmp.path,
  580. fn: async () => {
  581. // kilocode_change start - renamed from "build" to "code"
  582. const build = await Agent.get("code")
  583. // kilocode_change end
  584. expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
  585. expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
  586. expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
  587. },
  588. })
  589. })
  590. test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
  591. const { Truncate } = await import("../../src/tool/truncate")
  592. await using tmp = await tmpdir({
  593. config: {
  594. agent: {
  595. // kilocode_change start - renamed from "build" to "code"
  596. code: {
  597. // kilocode_change end
  598. permission: {
  599. external_directory: "deny",
  600. },
  601. },
  602. },
  603. },
  604. })
  605. await Instance.provide({
  606. directory: tmp.path,
  607. fn: async () => {
  608. // kilocode_change start - renamed from "build" to "code"
  609. const build = await Agent.get("code")
  610. // kilocode_change end
  611. expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
  612. expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
  613. expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
  614. },
  615. })
  616. })
  617. test("explicit Truncate.GLOB deny is respected", async () => {
  618. const { Truncate } = await import("../../src/tool/truncate")
  619. await using tmp = await tmpdir({
  620. config: {
  621. permission: {
  622. external_directory: {
  623. "*": "deny",
  624. [Truncate.GLOB]: "deny",
  625. },
  626. },
  627. },
  628. })
  629. await Instance.provide({
  630. directory: tmp.path,
  631. fn: async () => {
  632. // kilocode_change start - renamed from "build" to "code"
  633. const build = await Agent.get("code")
  634. // kilocode_change end
  635. expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
  636. expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
  637. },
  638. })
  639. })
  640. test("skill directories are allowed for external_directory", async () => {
  641. await using tmp = await tmpdir({
  642. git: true,
  643. init: async (dir) => {
  644. const skillDir = path.join(dir, ".kilo", "skill", "perm-skill") // kilocode_change: .kilo is primary
  645. await Bun.write(
  646. path.join(skillDir, "SKILL.md"),
  647. `---
  648. name: perm-skill
  649. description: Permission skill.
  650. ---
  651. # Permission Skill
  652. `,
  653. )
  654. },
  655. })
  656. const home = process.env.KILO_TEST_HOME
  657. process.env.KILO_TEST_HOME = tmp.path
  658. try {
  659. await Instance.provide({
  660. directory: tmp.path,
  661. fn: async () => {
  662. const build = await Agent.get("build")
  663. const skillDir = path.join(tmp.path, ".kilo", "skill", "perm-skill") // kilocode_change: .kilo is primary
  664. const target = path.join(skillDir, "reference", "notes.md")
  665. expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
  666. },
  667. })
  668. } finally {
  669. process.env.KILO_TEST_HOME = home
  670. }
  671. })
  672. test("defaultAgent returns build when no default_agent config", async () => {
  673. await using tmp = await tmpdir()
  674. await Instance.provide({
  675. directory: tmp.path,
  676. fn: async () => {
  677. const agent = await Agent.defaultAgent()
  678. expect(agent).toBe("code") // kilocode_change
  679. },
  680. })
  681. })
  682. test("defaultAgent respects default_agent config set to plan", async () => {
  683. await using tmp = await tmpdir({
  684. config: {
  685. default_agent: "plan",
  686. },
  687. })
  688. await Instance.provide({
  689. directory: tmp.path,
  690. fn: async () => {
  691. const agent = await Agent.defaultAgent()
  692. expect(agent).toBe("plan")
  693. },
  694. })
  695. })
  696. test("defaultAgent respects default_agent config set to custom agent with mode all", async () => {
  697. await using tmp = await tmpdir({
  698. config: {
  699. default_agent: "my_custom",
  700. agent: {
  701. my_custom: {
  702. description: "My custom agent",
  703. },
  704. },
  705. },
  706. })
  707. await Instance.provide({
  708. directory: tmp.path,
  709. fn: async () => {
  710. const agent = await Agent.defaultAgent()
  711. expect(agent).toBe("my_custom")
  712. },
  713. })
  714. })
  715. test("defaultAgent throws when default_agent points to subagent", async () => {
  716. await using tmp = await tmpdir({
  717. config: {
  718. default_agent: "explore",
  719. },
  720. })
  721. await Instance.provide({
  722. directory: tmp.path,
  723. fn: async () => {
  724. await expect(Agent.defaultAgent()).rejects.toThrow('default agent "explore" is a subagent')
  725. },
  726. })
  727. })
  728. test("defaultAgent throws when default_agent points to hidden agent", async () => {
  729. await using tmp = await tmpdir({
  730. config: {
  731. default_agent: "compaction",
  732. },
  733. })
  734. await Instance.provide({
  735. directory: tmp.path,
  736. fn: async () => {
  737. await expect(Agent.defaultAgent()).rejects.toThrow('default agent "compaction" is hidden')
  738. },
  739. })
  740. })
  741. test("defaultAgent throws when default_agent points to non-existent agent", async () => {
  742. await using tmp = await tmpdir({
  743. config: {
  744. default_agent: "does_not_exist",
  745. },
  746. })
  747. await Instance.provide({
  748. directory: tmp.path,
  749. fn: async () => {
  750. await expect(Agent.defaultAgent()).rejects.toThrow('default agent "does_not_exist" not found')
  751. },
  752. })
  753. })
  754. // kilocode_change start - renamed from "build" to "code"
  755. test("defaultAgent returns plan when code is disabled and default_agent not set", async () => {
  756. // kilocode_change end
  757. await using tmp = await tmpdir({
  758. config: {
  759. agent: {
  760. // kilocode_change start - renamed from "build" to "code"
  761. code: { disable: true },
  762. // kilocode_change end
  763. },
  764. },
  765. })
  766. await Instance.provide({
  767. directory: tmp.path,
  768. fn: async () => {
  769. const agent = await Agent.defaultAgent()
  770. // kilocode_change - code is disabled, so it should return plan (next primary agent)
  771. expect(agent).toBe("plan")
  772. },
  773. })
  774. })
  775. test("defaultAgent throws when all primary agents are disabled", async () => {
  776. await using tmp = await tmpdir({
  777. config: {
  778. agent: {
  779. // kilocode_change start - disable all primary agents
  780. code: { disable: true },
  781. plan: { disable: true },
  782. debug: { disable: true },
  783. orchestrator: { disable: true },
  784. ask: { disable: true },
  785. // kilocode_change end
  786. },
  787. },
  788. })
  789. await Instance.provide({
  790. directory: tmp.path,
  791. fn: async () => {
  792. // kilocode_change - all primary agents are disabled
  793. await expect(Agent.defaultAgent()).rejects.toThrow("no primary visible agent found")
  794. },
  795. })
  796. })
  797. // kilocode_change start - Backward compatibility tests for "build" -> "code" rename
  798. test("Agent.get('build') returns code agent for backward compatibility", async () => {
  799. await using tmp = await tmpdir()
  800. await Instance.provide({
  801. directory: tmp.path,
  802. fn: async () => {
  803. const build = await Agent.get("build")
  804. const code = await Agent.get("code")
  805. expect(build).toBeDefined()
  806. expect(build).toBe(code)
  807. expect(build?.name).toBe("code")
  808. },
  809. })
  810. })
  811. test("agent.build config applies to code agent for backward compatibility", async () => {
  812. await using tmp = await tmpdir({
  813. config: {
  814. agent: {
  815. build: {
  816. temperature: 0.8,
  817. color: "#00FF00",
  818. },
  819. },
  820. },
  821. })
  822. await Instance.provide({
  823. directory: tmp.path,
  824. fn: async () => {
  825. const code = await Agent.get("code")
  826. expect(code).toBeDefined()
  827. expect(code?.temperature).toBe(0.8)
  828. expect(code?.color).toBe("#00FF00")
  829. },
  830. })
  831. })
  832. test("default_agent: 'build' returns code agent for backward compatibility", async () => {
  833. await using tmp = await tmpdir({
  834. config: {
  835. default_agent: "build",
  836. },
  837. })
  838. await Instance.provide({
  839. directory: tmp.path,
  840. fn: async () => {
  841. const agent = await Agent.defaultAgent()
  842. expect(agent).toBe("code")
  843. },
  844. })
  845. })
  846. test("agent.build disable removes code agent for backward compatibility", async () => {
  847. await using tmp = await tmpdir({
  848. config: {
  849. agent: {
  850. build: { disable: true },
  851. },
  852. },
  853. })
  854. await Instance.provide({
  855. directory: tmp.path,
  856. fn: async () => {
  857. const code = await Agent.get("code")
  858. expect(code).toBeUndefined()
  859. const agents = await Agent.list()
  860. const names = agents.map((a) => a.name)
  861. expect(names).not.toContain("code")
  862. },
  863. })
  864. })
  865. // kilocode_change end