@stigmer/sdk 3.0.9-dev.20260616060535 → 3.1.1

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 (240) hide show
  1. package/activity.js +1 -1
  2. package/activity.js.map +1 -1
  3. package/authorization-config.js +1 -1
  4. package/authorization-config.js.map +1 -1
  5. package/billing.js +1 -1
  6. package/billing.js.map +1 -1
  7. package/errors.d.ts +1 -1
  8. package/errors.d.ts.map +1 -1
  9. package/errors.js +2 -2
  10. package/errors.js.map +1 -1
  11. package/execution/__tests__/approval-provenance.test.d.ts +2 -0
  12. package/execution/__tests__/approval-provenance.test.d.ts.map +1 -0
  13. package/execution/__tests__/approval-provenance.test.js +42 -0
  14. package/execution/__tests__/approval-provenance.test.js.map +1 -0
  15. package/execution/__tests__/file-review-fold.corpus.test.d.ts +2 -0
  16. package/execution/__tests__/file-review-fold.corpus.test.d.ts.map +1 -0
  17. package/execution/__tests__/file-review-fold.corpus.test.js +82 -0
  18. package/execution/__tests__/file-review-fold.corpus.test.js.map +1 -0
  19. package/execution/__tests__/file-review-fold.test.d.ts +2 -0
  20. package/execution/__tests__/file-review-fold.test.d.ts.map +1 -0
  21. package/execution/__tests__/file-review-fold.test.js +184 -0
  22. package/execution/__tests__/file-review-fold.test.js.map +1 -0
  23. package/execution/__tests__/to-display-file-change.test.d.ts +2 -0
  24. package/execution/__tests__/to-display-file-change.test.d.ts.map +1 -0
  25. package/execution/__tests__/to-display-file-change.test.js +69 -0
  26. package/execution/__tests__/to-display-file-change.test.js.map +1 -0
  27. package/execution/__tests__/tool-view.file-changes.test.d.ts +2 -0
  28. package/execution/__tests__/tool-view.file-changes.test.d.ts.map +1 -0
  29. package/execution/__tests__/tool-view.file-changes.test.js +86 -0
  30. package/execution/__tests__/tool-view.file-changes.test.js.map +1 -0
  31. package/execution/__tests__/tool-view.fixtures.test.js +3 -0
  32. package/execution/__tests__/tool-view.fixtures.test.js.map +1 -1
  33. package/execution/__tests__/tool-view.output-ref.test.d.ts +2 -0
  34. package/execution/__tests__/tool-view.output-ref.test.d.ts.map +1 -0
  35. package/execution/__tests__/tool-view.output-ref.test.js +64 -0
  36. package/execution/__tests__/tool-view.output-ref.test.js.map +1 -0
  37. package/execution/__tests__/tool-view.search.test.d.ts +2 -0
  38. package/execution/__tests__/tool-view.search.test.d.ts.map +1 -0
  39. package/execution/__tests__/tool-view.search.test.js +200 -0
  40. package/execution/__tests__/tool-view.search.test.js.map +1 -0
  41. package/execution/__tests__/tool-view.shell.test.d.ts +2 -0
  42. package/execution/__tests__/tool-view.shell.test.d.ts.map +1 -0
  43. package/execution/__tests__/tool-view.shell.test.js +104 -0
  44. package/execution/__tests__/tool-view.shell.test.js.map +1 -0
  45. package/execution/approval-provenance.d.ts +30 -0
  46. package/execution/approval-provenance.d.ts.map +1 -0
  47. package/execution/approval-provenance.js +73 -0
  48. package/execution/approval-provenance.js.map +1 -0
  49. package/execution/execution-phases.d.ts +7 -0
  50. package/execution/execution-phases.d.ts.map +1 -0
  51. package/execution/execution-phases.js +21 -0
  52. package/execution/execution-phases.js.map +1 -0
  53. package/execution/file-review-fold.d.ts +44 -0
  54. package/execution/file-review-fold.d.ts.map +1 -0
  55. package/execution/file-review-fold.js +182 -0
  56. package/execution/file-review-fold.js.map +1 -0
  57. package/execution/to-display-file-change.d.ts +14 -0
  58. package/execution/to-display-file-change.d.ts.map +1 -0
  59. package/execution/to-display-file-change.js +57 -0
  60. package/execution/to-display-file-change.js.map +1 -0
  61. package/execution/tool-view.d.ts +8 -2
  62. package/execution/tool-view.d.ts.map +1 -1
  63. package/execution/tool-view.js +205 -21
  64. package/execution/tool-view.js.map +1 -1
  65. package/gen/agent.d.ts +1 -1
  66. package/gen/agent.d.ts.map +1 -1
  67. package/gen/agent.js +2 -2
  68. package/gen/agent.js.map +1 -1
  69. package/gen/agentexecution.d.ts +3 -2
  70. package/gen/agentexecution.d.ts.map +1 -1
  71. package/gen/agentexecution.js +10 -2
  72. package/gen/agentexecution.js.map +1 -1
  73. package/gen/agentinstance.d.ts +1 -1
  74. package/gen/agentinstance.d.ts.map +1 -1
  75. package/gen/agentinstance.js +2 -2
  76. package/gen/agentinstance.js.map +1 -1
  77. package/gen/apikey.js +2 -2
  78. package/gen/apikey.js.map +1 -1
  79. package/gen/artifact.js +2 -2
  80. package/gen/artifact.js.map +1 -1
  81. package/gen/client.d.ts +65 -65
  82. package/gen/client.d.ts.map +1 -1
  83. package/gen/client.js +43 -43
  84. package/gen/client.js.map +1 -1
  85. package/gen/environment.d.ts +1 -1
  86. package/gen/environment.d.ts.map +1 -1
  87. package/gen/environment.js +2 -2
  88. package/gen/environment.js.map +1 -1
  89. package/gen/executioncontext.d.ts +1 -1
  90. package/gen/executioncontext.d.ts.map +1 -1
  91. package/gen/executioncontext.js +2 -2
  92. package/gen/executioncontext.js.map +1 -1
  93. package/gen/iampolicy.js +2 -2
  94. package/gen/iampolicy.js.map +1 -1
  95. package/gen/identityaccount.d.ts +1 -1
  96. package/gen/identityaccount.d.ts.map +1 -1
  97. package/gen/identityaccount.js +2 -2
  98. package/gen/identityaccount.js.map +1 -1
  99. package/gen/identityprovider.d.ts +1 -1
  100. package/gen/identityprovider.d.ts.map +1 -1
  101. package/gen/identityprovider.js +2 -2
  102. package/gen/identityprovider.js.map +1 -1
  103. package/gen/invitation.js +2 -2
  104. package/gen/invitation.js.map +1 -1
  105. package/gen/mcpserver.d.ts +2 -1
  106. package/gen/mcpserver.d.ts.map +1 -1
  107. package/gen/mcpserver.js +3 -2
  108. package/gen/mcpserver.js.map +1 -1
  109. package/gen/oauthapp.d.ts +1 -1
  110. package/gen/oauthapp.d.ts.map +1 -1
  111. package/gen/oauthapp.js +2 -2
  112. package/gen/oauthapp.js.map +1 -1
  113. package/gen/organization.d.ts +1 -1
  114. package/gen/organization.d.ts.map +1 -1
  115. package/gen/organization.js +2 -2
  116. package/gen/organization.js.map +1 -1
  117. package/gen/platformclient.d.ts +1 -1
  118. package/gen/platformclient.d.ts.map +1 -1
  119. package/gen/platformclient.js +2 -2
  120. package/gen/platformclient.js.map +1 -1
  121. package/gen/project.d.ts +1 -1
  122. package/gen/project.d.ts.map +1 -1
  123. package/gen/project.js +2 -2
  124. package/gen/project.js.map +1 -1
  125. package/gen/session.d.ts +1 -1
  126. package/gen/session.d.ts.map +1 -1
  127. package/gen/session.js +2 -2
  128. package/gen/session.js.map +1 -1
  129. package/gen/skill.d.ts +1 -1
  130. package/gen/skill.d.ts.map +1 -1
  131. package/gen/skill.js +2 -2
  132. package/gen/skill.js.map +1 -1
  133. package/gen/workflow.d.ts +1 -1
  134. package/gen/workflow.d.ts.map +1 -1
  135. package/gen/workflow.js +2 -2
  136. package/gen/workflow.js.map +1 -1
  137. package/gen/workflowexecution.d.ts +3 -2
  138. package/gen/workflowexecution.d.ts.map +1 -1
  139. package/gen/workflowexecution.js +10 -2
  140. package/gen/workflowexecution.js.map +1 -1
  141. package/gen/workflowinstance.d.ts +1 -1
  142. package/gen/workflowinstance.d.ts.map +1 -1
  143. package/gen/workflowinstance.js +2 -2
  144. package/gen/workflowinstance.js.map +1 -1
  145. package/github.js +1 -1
  146. package/github.js.map +1 -1
  147. package/index.d.ts +40 -36
  148. package/index.d.ts.map +1 -1
  149. package/index.js +37 -33
  150. package/index.js.map +1 -1
  151. package/internal/interceptors.d.ts +1 -1
  152. package/internal/interceptors.d.ts.map +1 -1
  153. package/internal/interceptors.js +1 -1
  154. package/internal/interceptors.js.map +1 -1
  155. package/node.d.ts +2 -2
  156. package/node.d.ts.map +1 -1
  157. package/node.js +3 -3
  158. package/node.js.map +1 -1
  159. package/package.json +2 -2
  160. package/platform-client-auth.js +2 -2
  161. package/platform-client-auth.js.map +1 -1
  162. package/platform.d.ts +1 -1
  163. package/platform.d.ts.map +1 -1
  164. package/platform.js +1 -1
  165. package/platform.js.map +1 -1
  166. package/resource-availability.js +1 -1
  167. package/resource-availability.js.map +1 -1
  168. package/search.js +1 -1
  169. package/search.js.map +1 -1
  170. package/src/activity.ts +1 -1
  171. package/src/authorization-config.ts +1 -1
  172. package/src/billing.ts +1 -1
  173. package/src/errors.ts +2 -2
  174. package/src/execution/__tests__/approval-provenance.test.ts +60 -0
  175. package/src/execution/__tests__/file-review-fold.corpus.test.ts +127 -0
  176. package/src/execution/__tests__/file-review-fold.test.ts +281 -0
  177. package/src/execution/__tests__/to-display-file-change.test.ts +91 -0
  178. package/src/execution/__tests__/tool-view.file-changes.test.ts +100 -0
  179. package/src/execution/__tests__/tool-view.fixtures.test.ts +4 -0
  180. package/src/execution/__tests__/tool-view.output-ref.test.ts +76 -0
  181. package/src/execution/__tests__/tool-view.search.test.ts +264 -0
  182. package/src/execution/__tests__/tool-view.shell.test.ts +127 -0
  183. package/src/execution/approval-provenance.ts +80 -0
  184. package/src/execution/execution-phases.ts +23 -0
  185. package/src/execution/file-review-fold.ts +210 -0
  186. package/src/execution/to-display-file-change.ts +66 -0
  187. package/src/execution/tool-view.ts +261 -30
  188. package/src/gen/agent.ts +3 -3
  189. package/src/gen/agentexecution.ts +10 -4
  190. package/src/gen/agentinstance.ts +3 -3
  191. package/src/gen/apikey.ts +2 -2
  192. package/src/gen/artifact.ts +2 -2
  193. package/src/gen/client.ts +65 -65
  194. package/src/gen/environment.ts +3 -3
  195. package/src/gen/executioncontext.ts +3 -3
  196. package/src/gen/iampolicy.ts +2 -2
  197. package/src/gen/identityaccount.ts +3 -3
  198. package/src/gen/identityprovider.ts +3 -3
  199. package/src/gen/invitation.ts +2 -2
  200. package/src/gen/mcpserver.ts +5 -3
  201. package/src/gen/oauthapp.ts +3 -3
  202. package/src/gen/organization.ts +3 -3
  203. package/src/gen/platformclient.ts +3 -3
  204. package/src/gen/project.ts +3 -3
  205. package/src/gen/session.ts +3 -3
  206. package/src/gen/skill.ts +3 -3
  207. package/src/gen/workflow.ts +3 -3
  208. package/src/gen/workflowexecution.ts +10 -4
  209. package/src/gen/workflowinstance.ts +3 -3
  210. package/src/github.ts +1 -1
  211. package/src/index.ts +48 -36
  212. package/src/internal/interceptors.ts +2 -2
  213. package/src/node.ts +3 -3
  214. package/src/platform-client-auth.ts +2 -2
  215. package/src/platform.ts +2 -2
  216. package/src/resource-availability.ts +1 -1
  217. package/src/search.ts +1 -1
  218. package/src/stigmer.ts +8 -8
  219. package/src/synth/context.ts +3 -3
  220. package/src/synth/index.ts +6 -6
  221. package/src/synth/writer.ts +4 -4
  222. package/src/transport.ts +2 -2
  223. package/stigmer.d.ts +7 -7
  224. package/stigmer.d.ts.map +1 -1
  225. package/stigmer.js +8 -8
  226. package/stigmer.js.map +1 -1
  227. package/synth/context.d.ts +3 -3
  228. package/synth/context.d.ts.map +1 -1
  229. package/synth/index.d.ts +5 -5
  230. package/synth/index.d.ts.map +1 -1
  231. package/synth/index.js +3 -3
  232. package/synth/index.js.map +1 -1
  233. package/synth/writer.d.ts +1 -1
  234. package/synth/writer.d.ts.map +1 -1
  235. package/synth/writer.js +3 -3
  236. package/synth/writer.js.map +1 -1
  237. package/transport.d.ts +1 -1
  238. package/transport.d.ts.map +1 -1
  239. package/transport.js +1 -1
  240. package/transport.js.map +1 -1
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ ApprovalPolicySource,
4
+ describeApprovalPolicySource,
5
+ isInformativePolicySource,
6
+ } from "../approval-provenance";
7
+
8
+ describe("describeApprovalPolicySource", () => {
9
+ it("maps each known source to a stable human phrase", () => {
10
+ expect(describeApprovalPolicySource(ApprovalPolicySource.CLASSIFIER_DEFAULT)).toBe(
11
+ "required by the tool's default policy",
12
+ );
13
+ expect(describeApprovalPolicySource(ApprovalPolicySource.BUILTIN_CATEGORY)).toBe(
14
+ "required by built-in tool policy",
15
+ );
16
+ expect(describeApprovalPolicySource(ApprovalPolicySource.AGENT_OVERRIDE)).toBe(
17
+ "required by agent override",
18
+ );
19
+ expect(
20
+ describeApprovalPolicySource(ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN),
21
+ ).toBe("required: marked destructive by the server");
22
+ });
23
+
24
+ it("returns null for UNSPECIFIED (legacy / ungated)", () => {
25
+ expect(describeApprovalPolicySource(ApprovalPolicySource.UNSPECIFIED)).toBeNull();
26
+ });
27
+ });
28
+
29
+ describe("isInformativePolicySource", () => {
30
+ it("suppresses the everyday default gating reasons", () => {
31
+ // These add nothing beyond "this tool needs approval" — noise at the gate.
32
+ expect(isInformativePolicySource(ApprovalPolicySource.CLASSIFIER_DEFAULT)).toBe(false);
33
+ expect(isInformativePolicySource(ApprovalPolicySource.BUILTIN_CATEGORY)).toBe(false);
34
+ expect(isInformativePolicySource(ApprovalPolicySource.UNSPECIFIED)).toBe(false);
35
+ });
36
+
37
+ it("surfaces reasons that change the user's understanding of the gate", () => {
38
+ expect(isInformativePolicySource(ApprovalPolicySource.PINNED_OVERRIDE)).toBe(true);
39
+ expect(isInformativePolicySource(ApprovalPolicySource.AGENT_OVERRIDE)).toBe(true);
40
+ expect(
41
+ isInformativePolicySource(ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN),
42
+ ).toBe(true);
43
+ expect(isInformativePolicySource(ApprovalPolicySource.AUTO_APPROVE_ALL)).toBe(true);
44
+ expect(isInformativePolicySource(ApprovalPolicySource.APPROVAL_LEASE)).toBe(true);
45
+ });
46
+
47
+ it("only surfaces a reason that also has a phrase to show", () => {
48
+ // The card renders `isInformative(source) && describe(source) != null`; the
49
+ // two must never disagree for a surfaced source.
50
+ for (const source of [
51
+ ApprovalPolicySource.PINNED_OVERRIDE,
52
+ ApprovalPolicySource.AGENT_OVERRIDE,
53
+ ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN,
54
+ ApprovalPolicySource.AUTO_APPROVE_ALL,
55
+ ApprovalPolicySource.APPROVAL_LEASE,
56
+ ]) {
57
+ expect(describeApprovalPolicySource(source)).not.toBeNull();
58
+ }
59
+ });
60
+ });
@@ -0,0 +1,127 @@
1
+ // Cross-edition parity lock for the file-review projection.
2
+ //
3
+ // The Go (`FileChangeSetProjector` / project.go) and Java (`FileChangeSetProjector`)
4
+ // projectors replay the shared corpus at apis/testdata/hitl/file-review/*.json.
5
+ // This test replays the SAME corpus against the TypeScript fold, so a drift
6
+ // between the SDK and either backend fails here rather than shipping.
7
+ //
8
+ // The corpus `expected` is the server (actionable) projection, which is empty
9
+ // for a terminal execution. The SDK's display fold intentionally has no terminal
10
+ // gate, so to reproduce the server contract exactly this test applies the phase
11
+ // gate the accessor's callers apply: isTerminalPhase(phase) ? [] : fold(stream).
12
+ // That both proves parity for non-terminal vectors AND pins the terminal-empty
13
+ // vector — the one place the SDK deliberately differs is covered by the accessor
14
+ // test in file-review-fold.test.ts, not here.
15
+
16
+ import { readFileSync, readdirSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import { dirname, resolve } from "node:path";
19
+ import { describe, it, expect } from "vitest";
20
+ import { create, fromJson, type JsonValue } from "@bufbuild/protobuf";
21
+ import {
22
+ FileReviewEventSchema,
23
+ FileReviewEventStreamSchema,
24
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
25
+ import {
26
+ ExecutionPhase,
27
+ FileChangeSetStatus,
28
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
29
+ import { foldFileReviewEventStream } from "../file-review-fold";
30
+ import { isTerminalPhase } from "../execution-phases";
31
+
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ // sdk/typescript/src/execution/__tests__ -> repo root is five levels up.
34
+ const corpusDir = resolve(here, "../../../../../apis/testdata/hitl/file-review");
35
+
36
+ /** The `expected` summary shape each vector pins (see the corpus README). */
37
+ interface ExpectedSet {
38
+ id: string;
39
+ status: string; // full proto enum name, e.g. FILE_CHANGE_SET_STATUS_DECIDED
40
+ change_ids?: string[];
41
+ decision_count?: number;
42
+ aggregate_digest?: string;
43
+ has_approved_snapshot?: boolean;
44
+ }
45
+
46
+ interface Vector {
47
+ name: string;
48
+ description?: string;
49
+ execution_id: string;
50
+ phase: string;
51
+ events: JsonValue[];
52
+ expected: ExpectedSet[];
53
+ }
54
+
55
+ function statusFromName(name: string): FileChangeSetStatus {
56
+ const key = name.replace(
57
+ /^FILE_CHANGE_SET_STATUS_/,
58
+ "",
59
+ ) as keyof typeof FileChangeSetStatus;
60
+ const value = FileChangeSetStatus[key];
61
+ if (typeof value !== "number") {
62
+ throw new Error(`unknown FileChangeSetStatus in corpus: ${name}`);
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function phaseFromName(name: string): ExecutionPhase {
68
+ const value = ExecutionPhase[name as keyof typeof ExecutionPhase];
69
+ if (typeof value !== "number") {
70
+ throw new Error(`unknown ExecutionPhase in corpus: ${name}`);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ const vectorFiles = readdirSync(corpusDir).filter(
76
+ (f) => f.endsWith(".json") && f !== "schema.json",
77
+ );
78
+
79
+ describe("file-review fold — cross-edition corpus parity", () => {
80
+ it("finds the corpus vectors", () => {
81
+ expect(vectorFiles.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ for (const file of vectorFiles) {
85
+ const vector = JSON.parse(
86
+ readFileSync(resolve(corpusDir, file), "utf8"),
87
+ ) as Vector;
88
+
89
+ it(`${file}: ${vector.name}`, () => {
90
+ // Decode each event with the generated schema so a malformed fixture fails
91
+ // loudly (matching the backend corpus drivers), then build the stream.
92
+ const events = vector.events.map((ev) =>
93
+ fromJson(FileReviewEventSchema, ev),
94
+ );
95
+ const stream = create(FileReviewEventStreamSchema, {
96
+ executionId: vector.execution_id,
97
+ events,
98
+ });
99
+
100
+ const phase = phaseFromName(vector.phase);
101
+ // Reproduce the server's actionable projection: phase-gated fold.
102
+ const projected = isTerminalPhase(phase)
103
+ ? []
104
+ : foldFileReviewEventStream(stream);
105
+
106
+ const actual = projected.map((set) => ({
107
+ id: set.id,
108
+ status: set.status,
109
+ change_ids: set.changes.map((c) => c.id),
110
+ decision_count: set.decisions.length,
111
+ aggregate_digest: set.aggregateDigest,
112
+ has_approved_snapshot: set.approvedSnapshot !== undefined,
113
+ }));
114
+
115
+ const expected = vector.expected.map((e) => ({
116
+ id: e.id,
117
+ status: statusFromName(e.status),
118
+ change_ids: e.change_ids ?? [],
119
+ decision_count: e.decision_count ?? 0,
120
+ aggregate_digest: e.aggregate_digest ?? "",
121
+ has_approved_snapshot: e.has_approved_snapshot ?? false,
122
+ }));
123
+
124
+ expect(actual).toEqual(expected);
125
+ });
126
+ }
127
+ });
@@ -0,0 +1,281 @@
1
+ // Unit tests for the file-review fold and its display read-seam. The
2
+ // cross-edition parity with the Go/Java projectors is locked separately by
3
+ // file-review-fold.corpus.test.ts; these cover the TS-specific edges and the
4
+ // deliberate terminal divergence of displayFileChangeSets.
5
+
6
+ import { describe, it, expect } from "vitest";
7
+ import { create } from "@bufbuild/protobuf";
8
+ import {
9
+ FileChangeSetSchema,
10
+ CapturedFileChangeSchema,
11
+ FileDecisionSchema,
12
+ FileReviewEventSchema,
13
+ FileReviewEventStreamSchema,
14
+ FileReviewBaselineCapturedSchema,
15
+ FileReviewCandidateCapturedSchema,
16
+ FileReviewReconciledSchema,
17
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
18
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
19
+ import {
20
+ FileChangeKind,
21
+ FileChangeSetStatus,
22
+ FileDecisionAction,
23
+ FileDecisionScope,
24
+ FileReviewEventType,
25
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
+ import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
27
+ import {
28
+ displayFileChangeSets,
29
+ foldFileReviewEventStream,
30
+ } from "../file-review-fold";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Event builders (keep the tests readable)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function change(id: string, path = `src/${id}.ts`): CapturedFileChange {
37
+ return create(CapturedFileChangeSchema, {
38
+ id,
39
+ pathAfter: path,
40
+ kind: FileChangeKind.MODIFY,
41
+ diffComplete: true,
42
+ });
43
+ }
44
+
45
+ function baseline(changeSetId: string) {
46
+ return create(FileReviewEventSchema, {
47
+ changeSetId,
48
+ eventType: FileReviewEventType.BASELINE_CAPTURED,
49
+ payload: {
50
+ case: "baselineCaptured",
51
+ value: create(FileReviewBaselineCapturedSchema, {
52
+ changeSetId,
53
+ turnId: "t1",
54
+ harnessId: "deep-agent",
55
+ }),
56
+ },
57
+ });
58
+ }
59
+
60
+ function candidate(changeSetId: string, changes: CapturedFileChange[], agg = "agg") {
61
+ return create(FileReviewEventSchema, {
62
+ changeSetId,
63
+ eventType: FileReviewEventType.CANDIDATE_CAPTURED,
64
+ payload: {
65
+ case: "candidateCaptured",
66
+ value: create(FileReviewCandidateCapturedSchema, {
67
+ changeSetId,
68
+ changes,
69
+ aggregateDigest: agg,
70
+ }),
71
+ },
72
+ });
73
+ }
74
+
75
+ function fileDecision(
76
+ changeSetId: string,
77
+ fileChangeId: string,
78
+ action = FileDecisionAction.APPROVE,
79
+ ) {
80
+ return create(FileReviewEventSchema, {
81
+ changeSetId,
82
+ eventType: FileReviewEventType.FILE_DECIDED,
83
+ payload: {
84
+ case: "fileDecided",
85
+ value: create(FileDecisionSchema, {
86
+ changeSetId,
87
+ fileChangeId,
88
+ scope: FileDecisionScope.FILE,
89
+ action,
90
+ }),
91
+ },
92
+ });
93
+ }
94
+
95
+ function changeSetDecision(changeSetId: string) {
96
+ return create(FileReviewEventSchema, {
97
+ changeSetId,
98
+ eventType: FileReviewEventType.FILE_DECIDED,
99
+ payload: {
100
+ case: "fileDecided",
101
+ value: create(FileDecisionSchema, {
102
+ changeSetId,
103
+ scope: FileDecisionScope.CHANGE_SET,
104
+ action: FileDecisionAction.APPROVE,
105
+ }),
106
+ },
107
+ });
108
+ }
109
+
110
+ function reconciled(changeSetId: string) {
111
+ return create(FileReviewEventSchema, {
112
+ changeSetId,
113
+ eventType: FileReviewEventType.RECONCILED,
114
+ payload: {
115
+ case: "reconciled",
116
+ value: create(FileReviewReconciledSchema, { changeSetId }),
117
+ },
118
+ });
119
+ }
120
+
121
+ function failed(changeSetId: string) {
122
+ return create(FileReviewEventSchema, {
123
+ changeSetId,
124
+ eventType: FileReviewEventType.FAILED,
125
+ payload: { case: undefined },
126
+ });
127
+ }
128
+
129
+ function stream(...events: ReturnType<typeof baseline>[]) {
130
+ return create(FileReviewEventStreamSchema, { executionId: "aex_1", events });
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // foldFileReviewEventStream
135
+ // ---------------------------------------------------------------------------
136
+
137
+ describe("foldFileReviewEventStream", () => {
138
+ it("returns [] for undefined / empty streams", () => {
139
+ expect(foldFileReviewEventStream(undefined)).toEqual([]);
140
+ expect(foldFileReviewEventStream(stream())).toEqual([]);
141
+ });
142
+
143
+ it("opens a set as CAPTURING on baseline, before any candidate", () => {
144
+ const [set] = foldFileReviewEventStream(stream(baseline("cs1")));
145
+ expect(set.status).toBe(FileChangeSetStatus.CAPTURING);
146
+ expect(set.turnId).toBe("t1");
147
+ expect(set.harnessId).toBe("deep-agent");
148
+ expect(set.changes).toHaveLength(0);
149
+ });
150
+
151
+ it("moves to AWAITING_REVIEW once a candidate carries changes", () => {
152
+ const [set] = foldFileReviewEventStream(
153
+ stream(baseline("cs1"), candidate("cs1", [change("fc1")], "agg1")),
154
+ );
155
+ expect(set.status).toBe(FileChangeSetStatus.AWAITING_REVIEW);
156
+ expect(set.changes.map((c) => c.id)).toEqual(["fc1"]);
157
+ expect(set.aggregateDigest).toBe("agg1");
158
+ });
159
+
160
+ it("replaces the change list wholesale on a later candidate", () => {
161
+ const [set] = foldFileReviewEventStream(
162
+ stream(
163
+ baseline("cs1"),
164
+ candidate("cs1", [change("fc1"), change("fc2")]),
165
+ candidate("cs1", [change("fc3")]),
166
+ ),
167
+ );
168
+ expect(set.changes.map((c) => c.id)).toEqual(["fc3"]);
169
+ });
170
+
171
+ it("marks DECIDED only when every file has a verdict", () => {
172
+ const partial = foldFileReviewEventStream(
173
+ stream(
174
+ baseline("cs1"),
175
+ candidate("cs1", [change("fc1"), change("fc2")]),
176
+ fileDecision("cs1", "fc1"),
177
+ ),
178
+ )[0];
179
+ expect(partial.status).toBe(FileChangeSetStatus.AWAITING_REVIEW);
180
+ expect(partial.decisions).toHaveLength(1);
181
+
182
+ const full = foldFileReviewEventStream(
183
+ stream(
184
+ baseline("cs1"),
185
+ candidate("cs1", [change("fc1"), change("fc2")]),
186
+ fileDecision("cs1", "fc1"),
187
+ fileDecision("cs1", "fc2"),
188
+ ),
189
+ )[0];
190
+ expect(full.status).toBe(FileChangeSetStatus.DECIDED);
191
+ });
192
+
193
+ it("treats a CHANGE_SET decision as covering every file", () => {
194
+ const [set] = foldFileReviewEventStream(
195
+ stream(
196
+ baseline("cs1"),
197
+ candidate("cs1", [change("fc1"), change("fc2")]),
198
+ changeSetDecision("cs1"),
199
+ ),
200
+ );
201
+ expect(set.status).toBe(FileChangeSetStatus.DECIDED);
202
+ });
203
+
204
+ it("keeps terminal RECONCILED/FAILED sticky against a later decision", () => {
205
+ const reconciledSet = foldFileReviewEventStream(
206
+ stream(
207
+ baseline("cs1"),
208
+ candidate("cs1", [change("fc1")]),
209
+ reconciled("cs1"),
210
+ fileDecision("cs1", "fc1"), // arrives after reconcile — must not downgrade
211
+ ),
212
+ )[0];
213
+ expect(reconciledSet.status).toBe(FileChangeSetStatus.RECONCILED);
214
+
215
+ const failedSet = foldFileReviewEventStream(
216
+ stream(baseline("cs1"), candidate("cs1", [change("fc1")]), failed("cs1"))
217
+ )[0];
218
+ expect(failedSet.status).toBe(FileChangeSetStatus.FAILED);
219
+ });
220
+
221
+ it("groups multiple change sets in first-seen order", () => {
222
+ const sets = foldFileReviewEventStream(
223
+ stream(
224
+ baseline("cs2"),
225
+ candidate("cs2", [change("fc-a")]),
226
+ baseline("cs1"),
227
+ candidate("cs1", [change("fc-b")]),
228
+ ),
229
+ );
230
+ expect(sets.map((s) => s.id)).toEqual(["cs2", "cs1"]);
231
+ });
232
+
233
+ it("skips events with no change_set_id", () => {
234
+ const orphan = create(FileReviewEventSchema, {
235
+ changeSetId: "",
236
+ eventType: FileReviewEventType.BASELINE_CAPTURED,
237
+ });
238
+ expect(foldFileReviewEventStream(stream(orphan))).toEqual([]);
239
+ });
240
+ });
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // displayFileChangeSets — projection-first, fold-on-terminal
244
+ // ---------------------------------------------------------------------------
245
+
246
+ describe("displayFileChangeSets", () => {
247
+ it("returns [] for undefined status", () => {
248
+ expect(displayFileChangeSets(undefined)).toEqual([]);
249
+ });
250
+
251
+ it("prefers the server projection when present (preserving its reference)", () => {
252
+ const projected = [create(FileChangeSetSchema, { id: "cs1" })];
253
+ const status = create(AgentExecutionStatusSchema, {
254
+ fileChangeSets: projected,
255
+ // A ledger is also present, but the projection wins for a live execution.
256
+ fileReviewEventStream: stream(baseline("cs1")),
257
+ });
258
+ const result = displayFileChangeSets(status);
259
+ expect(result).toBe(status.fileChangeSets); // same ref — no re-fold, memo-safe
260
+ });
261
+
262
+ it("folds the ledger when the projection is empty (terminal execution)", () => {
263
+ const status = create(AgentExecutionStatusSchema, {
264
+ fileChangeSets: [], // terminal: server projects nil
265
+ fileReviewEventStream: stream(
266
+ baseline("cs1"),
267
+ candidate("cs1", [change("fc1")]),
268
+ changeSetDecision("cs1"),
269
+ reconciled("cs1"),
270
+ ),
271
+ });
272
+ const result = displayFileChangeSets(status);
273
+ expect(result.map((s) => s.id)).toEqual(["cs1"]);
274
+ expect(result[0].status).toBe(FileChangeSetStatus.RECONCILED);
275
+ });
276
+
277
+ it("returns [] when neither projection nor ledger has content", () => {
278
+ const status = create(AgentExecutionStatusSchema, {});
279
+ expect(displayFileChangeSets(status)).toEqual([]);
280
+ });
281
+ });
@@ -0,0 +1,91 @@
1
+ // Unit tests for the CapturedFileChange -> FileChange display projection.
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import {
6
+ CapturedFileChangeSchema,
7
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
8
+ import { FileContentSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
9
+ import {
10
+ FileChangeCaptureLevel,
11
+ FileChangeKind,
12
+ FileChangeType,
13
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
+ import { toDisplayFileChange } from "../to-display-file-change";
15
+
16
+ function inline(text: string) {
17
+ return create(FileContentSchema, { body: { case: "inline", value: text } });
18
+ }
19
+
20
+ describe("toDisplayFileChange", () => {
21
+ it("maps a MODIFY with both sides to a WHOLE_FILE before/after change", () => {
22
+ const fc = toDisplayFileChange(
23
+ create(CapturedFileChangeSchema, {
24
+ id: "fc1",
25
+ pathBefore: "src/a.ts",
26
+ pathAfter: "src/a.ts",
27
+ kind: FileChangeKind.MODIFY,
28
+ before: inline("old"),
29
+ after: inline("new"),
30
+ }),
31
+ );
32
+ expect(fc.path).toBe("src/a.ts");
33
+ expect(fc.changeType).toBe(FileChangeType.MODIFY);
34
+ expect(fc.captureLevel).toBe(FileChangeCaptureLevel.WHOLE_FILE);
35
+ expect(fc.before?.body.case).toBe("inline");
36
+ expect(fc.after?.body.case).toBe("inline");
37
+ expect(fc.renameFrom).toBe("");
38
+ });
39
+
40
+ it("maps ADD -> CREATE and uses pathAfter", () => {
41
+ const fc = toDisplayFileChange(
42
+ create(CapturedFileChangeSchema, {
43
+ id: "fc1",
44
+ pathAfter: "src/new.ts",
45
+ kind: FileChangeKind.ADD,
46
+ after: inline("hello"),
47
+ }),
48
+ );
49
+ expect(fc.changeType).toBe(FileChangeType.CREATE);
50
+ expect(fc.path).toBe("src/new.ts");
51
+ });
52
+
53
+ it("maps DELETE -> DELETE and uses pathBefore when pathAfter is empty", () => {
54
+ const fc = toDisplayFileChange(
55
+ create(CapturedFileChangeSchema, {
56
+ id: "fc1",
57
+ pathBefore: "src/gone.ts",
58
+ kind: FileChangeKind.DELETE,
59
+ before: inline("bye"),
60
+ }),
61
+ );
62
+ expect(fc.changeType).toBe(FileChangeType.DELETE);
63
+ expect(fc.path).toBe("src/gone.ts");
64
+ });
65
+
66
+ it("maps RENAME -> RENAME and carries renameFrom = pathBefore", () => {
67
+ const fc = toDisplayFileChange(
68
+ create(CapturedFileChangeSchema, {
69
+ id: "fc1",
70
+ pathBefore: "src/old-name.ts",
71
+ pathAfter: "src/new-name.ts",
72
+ kind: FileChangeKind.RENAME,
73
+ }),
74
+ );
75
+ expect(fc.changeType).toBe(FileChangeType.RENAME);
76
+ expect(fc.path).toBe("src/new-name.ts");
77
+ expect(fc.renameFrom).toBe("src/old-name.ts");
78
+ });
79
+
80
+ it("carries a binary side through so the renderer can detect it", () => {
81
+ const fc = toDisplayFileChange(
82
+ create(CapturedFileChangeSchema, {
83
+ id: "fc1",
84
+ pathAfter: "img.png",
85
+ kind: FileChangeKind.ADD,
86
+ after: create(FileContentSchema, { isBinary: true }),
87
+ }),
88
+ );
89
+ expect(fc.after?.isBinary).toBe(true);
90
+ });
91
+ });
@@ -0,0 +1,100 @@
1
+ // Covers normalizeToolResult's file-edit / file-write views, reconstructed from
2
+ // the tool args (and the Cursor result envelope). Phase 5 Slice 4 removed the
3
+ // ToolCall.file_changes capture (message.proto field 22), so the args are the
4
+ // single source for the inline transcript diff; captured file review renders via
5
+ // FileReviewCard / the FileChangeSet ledger, tested separately.
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { create, type JsonObject } from "@bufbuild/protobuf";
9
+ import { ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
10
+ import {
11
+ ToolCallStatus,
12
+ ToolKind,
13
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
+ import { normalizeToolResult } from "../tool-view";
15
+
16
+ /** A completed FILE_EDIT tool call carrying the given args/result. */
17
+ function editToolCall(opts: { args?: Record<string, unknown>; result?: string }) {
18
+ return create(ToolCallSchema, {
19
+ id: "tc-1",
20
+ name: "edit_file",
21
+ toolKind: ToolKind.FILE_EDIT,
22
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
23
+ args: (opts.args ?? {}) as JsonObject,
24
+ result: opts.result ?? "",
25
+ });
26
+ }
27
+
28
+ /** A completed FILE_WRITE tool call carrying the given args. */
29
+ function writeToolCall(opts: { args?: Record<string, unknown> }) {
30
+ return create(ToolCallSchema, {
31
+ id: "tc-w",
32
+ name: "write_file",
33
+ toolKind: ToolKind.FILE_WRITE,
34
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
35
+ args: (opts.args ?? {}) as JsonObject,
36
+ });
37
+ }
38
+
39
+ describe("normalizeEdit — from args", () => {
40
+ it("reconstructs a diff from old_string / new_string", () => {
41
+ const view = normalizeToolResult(
42
+ editToolCall({
43
+ args: { path: "legacy.ts", old_string: "foo", new_string: "bar" },
44
+ result: "Successfully replaced 1 occurrence(s)",
45
+ }),
46
+ );
47
+ expect(view.type).toBe("diff");
48
+ if (view.type !== "diff") return;
49
+ expect(view.oldText).toBe("foo");
50
+ expect(view.newText).toBe("bar");
51
+ expect(view.path).toBe("legacy.ts");
52
+ // The row carries no whole-file capture; captureLevel is left unset.
53
+ expect(view.captureLevel).toBeUndefined();
54
+ });
55
+
56
+ it("parses the Cursor result envelope stats when present", () => {
57
+ const view = normalizeToolResult(
58
+ editToolCall({
59
+ args: { path: "x.ts" },
60
+ result: JSON.stringify({
61
+ value: { linesAdded: 3, linesRemoved: 2, diffString: "@@ patch @@" },
62
+ }),
63
+ }),
64
+ );
65
+ if (view.type !== "diff") return;
66
+ expect(view.linesAdded).toBe(3);
67
+ expect(view.linesRemoved).toBe(2);
68
+ expect(view.unifiedDiff).toBe("@@ patch @@");
69
+ expect(view.captureLevel).toBeUndefined();
70
+ });
71
+
72
+ it("resolves the path from the args (path / file_path / file / filename)", () => {
73
+ expect(
74
+ (() => {
75
+ const v = normalizeToolResult(editToolCall({ args: { file_path: "src/app/main.ts", old_string: "a", new_string: "b" } }));
76
+ return v.type === "diff" ? v.path : "";
77
+ })(),
78
+ ).toBe("src/app/main.ts");
79
+ });
80
+ });
81
+
82
+ describe("normalizeWrite — from args", () => {
83
+ it("renders the proposed whole-file content from the args (contents)", () => {
84
+ const CONTENT = "# Notes\n\n- first\n- second\n";
85
+ const view = normalizeToolResult(
86
+ writeToolCall({ args: { path: "notes.md", contents: CONTENT } }),
87
+ );
88
+ expect(view.type).toBe("file");
89
+ if (view.type !== "file") return;
90
+ expect(view.path).toBe("notes.md");
91
+ expect(view.content).toBe(CONTENT);
92
+ });
93
+
94
+ it("defaults to empty content when the args carry none", () => {
95
+ const view = normalizeToolResult(writeToolCall({ args: { path: "empty.ts" } }));
96
+ if (view.type !== "file") return;
97
+ expect(view.content).toBe("");
98
+ expect(view.path).toBe("empty.ts");
99
+ });
100
+ });
@@ -49,6 +49,7 @@ describe("result-view fixtures", () => {
49
49
  type: string;
50
50
  path?: string;
51
51
  exitCode?: number;
52
+ command?: string;
52
53
  count?: number;
53
54
  mcpServerSlug?: string;
54
55
  linesAdded?: number;
@@ -92,6 +93,9 @@ describe("result-view fixtures", () => {
92
93
  if (c.expected.exitCode !== undefined && view.type === "terminal") {
93
94
  expect(view.exitCode).toBe(c.expected.exitCode);
94
95
  }
96
+ if (c.expected.command !== undefined && view.type === "terminal") {
97
+ expect(view.command).toBe(c.expected.command);
98
+ }
95
99
  if (c.expected.count !== undefined && view.type === "search") {
96
100
  expect(view.count).toBe(c.expected.count);
97
101
  }