aggregate-failures.test.ts 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /**
  2. * Regression test for the TUI bootstrap aggregation helper. Replaces the
  3. * pre-fix Promise.all behavior where the first rejection drowned every
  4. * sibling endpoint's failure as an unhandled rejection.
  5. */
  6. import { describe, expect, test } from "bun:test"
  7. import { aggregateFailures } from "@opencode-ai/tui/context/aggregate-failures"
  8. describe("aggregateFailures", () => {
  9. test("returns null when every result is fulfilled", () => {
  10. expect(
  11. aggregateFailures([
  12. { name: "config", result: { status: "fulfilled", value: 1 } },
  13. { name: "providers", result: { status: "fulfilled", value: 2 } },
  14. ]),
  15. ).toBeNull()
  16. })
  17. test("names the failed endpoint when one rejects", () => {
  18. const err = aggregateFailures([
  19. { name: "config", result: { status: "fulfilled", value: 1 } },
  20. {
  21. name: "providers",
  22. result: { status: "rejected", reason: new Error("Service unavailable") },
  23. },
  24. ])
  25. expect(err).toBeInstanceOf(Error)
  26. expect(err!.message).toContain("1 of 2")
  27. expect(err!.message).toContain("providers: Service unavailable")
  28. })
  29. test("names every failed endpoint when multiple reject", () => {
  30. const err = aggregateFailures([
  31. { name: "config", result: { status: "rejected", reason: new Error("400 Bad Request") } },
  32. { name: "providers", result: { status: "fulfilled", value: 1 } },
  33. { name: "agents", result: { status: "rejected", reason: { message: "boom" } } },
  34. ])
  35. expect(err).toBeInstanceOf(Error)
  36. expect(err!.message).toContain("2 of 3")
  37. expect(err!.message).toContain("config: 400 Bad Request")
  38. expect(err!.message).toContain("agents: boom")
  39. })
  40. test("formats structured config errors hidden inside SDK error causes", () => {
  41. const configError = {
  42. name: "ConfigInvalidError",
  43. data: {
  44. path: "/tmp/opencode.json",
  45. issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }],
  46. },
  47. }
  48. const err = aggregateFailures([
  49. {
  50. name: "config.get",
  51. result: {
  52. status: "rejected",
  53. reason: new Error("ConfigInvalidError", {
  54. cause: {
  55. body: configError,
  56. },
  57. }),
  58. },
  59. },
  60. ])
  61. expect(err!.message).toContain("config.get: Configuration is invalid at /tmp/opencode.json")
  62. expect(err!.message).toContain("Expected object provider.anthropic.options")
  63. })
  64. test("deduplicates identical failure messages across startup requests", () => {
  65. const reason = new Error("same config problem")
  66. const err = aggregateFailures([
  67. { name: "config.providers", result: { status: "rejected", reason } },
  68. { name: "provider.list", result: { status: "rejected", reason } },
  69. { name: "app.agents", result: { status: "rejected", reason } },
  70. { name: "config.get", result: { status: "rejected", reason } },
  71. { name: "project.sync", result: { status: "fulfilled", value: undefined } },
  72. ])
  73. expect(err!.message).toContain("4 of 5 requests failed: same config problem")
  74. expect(err!.message).toContain("Affected startup requests: config.providers, provider.list, app.agents, config.get")
  75. expect(err!.message.match(/same config problem/g)?.length).toBe(1)
  76. })
  77. test("attaches structured failure list under .cause", () => {
  78. const reason = new Error("nope")
  79. const err = aggregateFailures([{ name: "providers", result: { status: "rejected", reason } }])
  80. expect(err!.cause).toEqual({ failures: [{ name: "providers", reason }] })
  81. })
  82. test("falls back to String() for opaque reasons", () => {
  83. const err = aggregateFailures([{ name: "x", result: { status: "rejected", reason: 42 } }])
  84. expect(err!.message).toContain("x: 42")
  85. })
  86. })