domain.test.ts 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import assert from "node:assert/strict";
  2. import test from "node:test";
  3. import {
  4. normalizeInspection,
  5. normalizeStatus,
  6. parseStatusChangedNotification,
  7. type ReadMethod,
  8. type ThreadSummary,
  9. } from "../src/domain.js";
  10. const allowedMethod = "thread/read" satisfies ReadMethod;
  11. // @ts-expect-error Mutating methods must not satisfy the compile-time allowlist.
  12. const rejectedMethod: ReadMethod = "thread/archive";
  13. void allowedMethod;
  14. void rejectedMethod;
  15. const baseThread = {
  16. id: "thread-1",
  17. sessionId: "thread-1",
  18. parentThreadId: null,
  19. forkedFromId: null,
  20. preview: "Investigate",
  21. ephemeral: false,
  22. modelProvider: "openai",
  23. createdAt: 10,
  24. updatedAt: 20,
  25. recencyAt: 30,
  26. cwd: "/work/project",
  27. source: "cli",
  28. status: { type: "active", activeFlags: ["waitingOnApproval"] },
  29. name: "Investigation",
  30. agentNickname: null,
  31. agentRole: null,
  32. turns: [],
  33. };
  34. test("status normalization preserves all documented states and active flags", () => {
  35. assert.deepEqual(normalizeStatus({ type: "active", activeFlags: ["waitingOnApproval"] }), {
  36. type: "active",
  37. activeFlags: ["waitingOnApproval"],
  38. });
  39. for (const type of ["idle", "notLoaded", "systemError"] as const) {
  40. assert.deepEqual(normalizeStatus({ type }), { type, activeFlags: [] });
  41. }
  42. });
  43. test("status notifications preserve thread identity and semantics", () => {
  44. assert.deepEqual(
  45. parseStatusChangedNotification({
  46. method: "thread/status/changed",
  47. params: { threadId: "thread-1", status: { type: "idle" } },
  48. }),
  49. { threadId: "thread-1", status: { type: "idle", activeFlags: [] } },
  50. );
  51. assert.equal(parseStatusChangedNotification({ method: "turn/completed", params: {} }), null);
  52. });
  53. test("inspection returns bounded recent conversation context", () => {
  54. const result = normalizeInspection(
  55. {
  56. thread: {
  57. ...baseThread,
  58. turns: [
  59. {
  60. id: "turn-1",
  61. items: [
  62. { id: "user-1", type: "userMessage", content: [{ type: "text", text: "older" }] },
  63. { id: "agent-1", type: "agentMessage", text: "recent-answer" },
  64. ],
  65. },
  66. ],
  67. },
  68. },
  69. { maxItems: 1, maxCharacters: 6 },
  70. );
  71. assert.deepEqual(result.context.items.map((item) => item.text), ["answer"]);
  72. assert.equal(result.context.truncated, true);
  73. assert.equal(result.context.omittedItems, 1);
  74. });
  75. test("thread summary type contains no conversation body field", () => {
  76. const summary = normalizeInspection({ thread: baseThread }, { maxItems: 1, maxCharacters: 10 }).thread;
  77. assert.equal("turns" in (summary as ThreadSummary), false);
  78. });
  79. test("malformed thread response is rejected", () => {
  80. assert.throws(
  81. () => normalizeInspection({ thread: { ...baseThread, turns: "bad" } }, { maxItems: 1, maxCharacters: 10 }),
  82. /turns must be an array/,
  83. );
  84. });