@synth-deploy/server 0.1.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 (317) hide show
  1. package/dist/agent/debrief-retention.d.ts +12 -0
  2. package/dist/agent/debrief-retention.d.ts.map +1 -0
  3. package/dist/agent/debrief-retention.js +27 -0
  4. package/dist/agent/debrief-retention.js.map +1 -0
  5. package/dist/agent/envoy-client.d.ts +216 -0
  6. package/dist/agent/envoy-client.d.ts.map +1 -0
  7. package/dist/agent/envoy-client.js +266 -0
  8. package/dist/agent/envoy-client.js.map +1 -0
  9. package/dist/agent/envoy-registry.d.ts +102 -0
  10. package/dist/agent/envoy-registry.d.ts.map +1 -0
  11. package/dist/agent/envoy-registry.js +319 -0
  12. package/dist/agent/envoy-registry.js.map +1 -0
  13. package/dist/agent/health-checker.d.ts +39 -0
  14. package/dist/agent/health-checker.d.ts.map +1 -0
  15. package/dist/agent/health-checker.js +49 -0
  16. package/dist/agent/health-checker.js.map +1 -0
  17. package/dist/agent/mcp-client-manager.d.ts +36 -0
  18. package/dist/agent/mcp-client-manager.d.ts.map +1 -0
  19. package/dist/agent/mcp-client-manager.js +106 -0
  20. package/dist/agent/mcp-client-manager.js.map +1 -0
  21. package/dist/agent/stale-deployment-detector.d.ts +15 -0
  22. package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
  23. package/dist/agent/stale-deployment-detector.js +50 -0
  24. package/dist/agent/stale-deployment-detector.js.map +1 -0
  25. package/dist/agent/step-runner.d.ts +31 -0
  26. package/dist/agent/step-runner.d.ts.map +1 -0
  27. package/dist/agent/step-runner.js +80 -0
  28. package/dist/agent/step-runner.js.map +1 -0
  29. package/dist/agent/synth-agent.d.ts +168 -0
  30. package/dist/agent/synth-agent.d.ts.map +1 -0
  31. package/dist/agent/synth-agent.js +1195 -0
  32. package/dist/agent/synth-agent.js.map +1 -0
  33. package/dist/api/agent.d.ts +36 -0
  34. package/dist/api/agent.d.ts.map +1 -0
  35. package/dist/api/agent.js +867 -0
  36. package/dist/api/agent.js.map +1 -0
  37. package/dist/api/api-keys.d.ts +4 -0
  38. package/dist/api/api-keys.d.ts.map +1 -0
  39. package/dist/api/api-keys.js +118 -0
  40. package/dist/api/api-keys.js.map +1 -0
  41. package/dist/api/artifacts.d.ts +5 -0
  42. package/dist/api/artifacts.d.ts.map +1 -0
  43. package/dist/api/artifacts.js +142 -0
  44. package/dist/api/artifacts.js.map +1 -0
  45. package/dist/api/auth.d.ts +4 -0
  46. package/dist/api/auth.d.ts.map +1 -0
  47. package/dist/api/auth.js +280 -0
  48. package/dist/api/auth.js.map +1 -0
  49. package/dist/api/deployments.d.ts +11 -0
  50. package/dist/api/deployments.d.ts.map +1 -0
  51. package/dist/api/deployments.js +1098 -0
  52. package/dist/api/deployments.js.map +1 -0
  53. package/dist/api/environments.d.ts +5 -0
  54. package/dist/api/environments.d.ts.map +1 -0
  55. package/dist/api/environments.js +69 -0
  56. package/dist/api/environments.js.map +1 -0
  57. package/dist/api/envoy-reports.d.ts +17 -0
  58. package/dist/api/envoy-reports.d.ts.map +1 -0
  59. package/dist/api/envoy-reports.js +138 -0
  60. package/dist/api/envoy-reports.js.map +1 -0
  61. package/dist/api/envoys.d.ts +5 -0
  62. package/dist/api/envoys.d.ts.map +1 -0
  63. package/dist/api/envoys.js +192 -0
  64. package/dist/api/envoys.js.map +1 -0
  65. package/dist/api/fleet.d.ts +11 -0
  66. package/dist/api/fleet.d.ts.map +1 -0
  67. package/dist/api/fleet.js +394 -0
  68. package/dist/api/fleet.js.map +1 -0
  69. package/dist/api/graph.d.ts +8 -0
  70. package/dist/api/graph.d.ts.map +1 -0
  71. package/dist/api/graph.js +355 -0
  72. package/dist/api/graph.js.map +1 -0
  73. package/dist/api/health.d.ts +20 -0
  74. package/dist/api/health.d.ts.map +1 -0
  75. package/dist/api/health.js +248 -0
  76. package/dist/api/health.js.map +1 -0
  77. package/dist/api/idp-schemas.d.ts +41 -0
  78. package/dist/api/idp-schemas.d.ts.map +1 -0
  79. package/dist/api/idp-schemas.js +17 -0
  80. package/dist/api/idp-schemas.js.map +1 -0
  81. package/dist/api/idp.d.ts +6 -0
  82. package/dist/api/idp.d.ts.map +1 -0
  83. package/dist/api/idp.js +620 -0
  84. package/dist/api/idp.js.map +1 -0
  85. package/dist/api/intake.d.ts +10 -0
  86. package/dist/api/intake.d.ts.map +1 -0
  87. package/dist/api/intake.js +418 -0
  88. package/dist/api/intake.js.map +1 -0
  89. package/dist/api/partitions.d.ts +5 -0
  90. package/dist/api/partitions.d.ts.map +1 -0
  91. package/dist/api/partitions.js +113 -0
  92. package/dist/api/partitions.js.map +1 -0
  93. package/dist/api/progress-event-store.d.ts +62 -0
  94. package/dist/api/progress-event-store.d.ts.map +1 -0
  95. package/dist/api/progress-event-store.js +118 -0
  96. package/dist/api/progress-event-store.js.map +1 -0
  97. package/dist/api/schemas.d.ts +1000 -0
  98. package/dist/api/schemas.d.ts.map +1 -0
  99. package/dist/api/schemas.js +328 -0
  100. package/dist/api/schemas.js.map +1 -0
  101. package/dist/api/security-boundaries.d.ts +4 -0
  102. package/dist/api/security-boundaries.d.ts.map +1 -0
  103. package/dist/api/security-boundaries.js +32 -0
  104. package/dist/api/security-boundaries.js.map +1 -0
  105. package/dist/api/settings.d.ts +4 -0
  106. package/dist/api/settings.d.ts.map +1 -0
  107. package/dist/api/settings.js +99 -0
  108. package/dist/api/settings.js.map +1 -0
  109. package/dist/api/system.d.ts +75 -0
  110. package/dist/api/system.d.ts.map +1 -0
  111. package/dist/api/system.js +558 -0
  112. package/dist/api/system.js.map +1 -0
  113. package/dist/api/telemetry.d.ts +4 -0
  114. package/dist/api/telemetry.d.ts.map +1 -0
  115. package/dist/api/telemetry.js +24 -0
  116. package/dist/api/telemetry.js.map +1 -0
  117. package/dist/api/users.d.ts +4 -0
  118. package/dist/api/users.d.ts.map +1 -0
  119. package/dist/api/users.js +173 -0
  120. package/dist/api/users.js.map +1 -0
  121. package/dist/archive-unpacker.d.ts +24 -0
  122. package/dist/archive-unpacker.d.ts.map +1 -0
  123. package/dist/archive-unpacker.js +239 -0
  124. package/dist/archive-unpacker.js.map +1 -0
  125. package/dist/artifact-analyzer.d.ts +59 -0
  126. package/dist/artifact-analyzer.d.ts.map +1 -0
  127. package/dist/artifact-analyzer.js +334 -0
  128. package/dist/artifact-analyzer.js.map +1 -0
  129. package/dist/auth/idp/index.d.ts +9 -0
  130. package/dist/auth/idp/index.d.ts.map +1 -0
  131. package/dist/auth/idp/index.js +5 -0
  132. package/dist/auth/idp/index.js.map +1 -0
  133. package/dist/auth/idp/ldap.d.ts +56 -0
  134. package/dist/auth/idp/ldap.d.ts.map +1 -0
  135. package/dist/auth/idp/ldap.js +276 -0
  136. package/dist/auth/idp/ldap.js.map +1 -0
  137. package/dist/auth/idp/oidc.d.ts +27 -0
  138. package/dist/auth/idp/oidc.d.ts.map +1 -0
  139. package/dist/auth/idp/oidc.js +97 -0
  140. package/dist/auth/idp/oidc.js.map +1 -0
  141. package/dist/auth/idp/role-mapping.d.ts +9 -0
  142. package/dist/auth/idp/role-mapping.d.ts.map +1 -0
  143. package/dist/auth/idp/role-mapping.js +16 -0
  144. package/dist/auth/idp/role-mapping.js.map +1 -0
  145. package/dist/auth/idp/saml.d.ts +40 -0
  146. package/dist/auth/idp/saml.d.ts.map +1 -0
  147. package/dist/auth/idp/saml.js +117 -0
  148. package/dist/auth/idp/saml.js.map +1 -0
  149. package/dist/auth/idp/types.d.ts +23 -0
  150. package/dist/auth/idp/types.d.ts.map +1 -0
  151. package/dist/auth/idp/types.js +2 -0
  152. package/dist/auth/idp/types.js.map +1 -0
  153. package/dist/fleet/fleet-executor.d.ts +35 -0
  154. package/dist/fleet/fleet-executor.d.ts.map +1 -0
  155. package/dist/fleet/fleet-executor.js +228 -0
  156. package/dist/fleet/fleet-executor.js.map +1 -0
  157. package/dist/fleet/fleet-store.d.ts +13 -0
  158. package/dist/fleet/fleet-store.d.ts.map +1 -0
  159. package/dist/fleet/fleet-store.js +13 -0
  160. package/dist/fleet/fleet-store.js.map +1 -0
  161. package/dist/fleet/index.d.ts +5 -0
  162. package/dist/fleet/index.d.ts.map +1 -0
  163. package/dist/fleet/index.js +4 -0
  164. package/dist/fleet/index.js.map +1 -0
  165. package/dist/fleet/representative-selector.d.ts +15 -0
  166. package/dist/fleet/representative-selector.d.ts.map +1 -0
  167. package/dist/fleet/representative-selector.js +71 -0
  168. package/dist/fleet/representative-selector.js.map +1 -0
  169. package/dist/graph/graph-executor.d.ts +36 -0
  170. package/dist/graph/graph-executor.d.ts.map +1 -0
  171. package/dist/graph/graph-executor.js +348 -0
  172. package/dist/graph/graph-executor.js.map +1 -0
  173. package/dist/graph/graph-inference.d.ts +22 -0
  174. package/dist/graph/graph-inference.d.ts.map +1 -0
  175. package/dist/graph/graph-inference.js +149 -0
  176. package/dist/graph/graph-inference.js.map +1 -0
  177. package/dist/graph/graph-store.d.ts +12 -0
  178. package/dist/graph/graph-store.d.ts.map +1 -0
  179. package/dist/graph/graph-store.js +61 -0
  180. package/dist/graph/graph-store.js.map +1 -0
  181. package/dist/graph/index.d.ts +5 -0
  182. package/dist/graph/index.d.ts.map +1 -0
  183. package/dist/graph/index.js +4 -0
  184. package/dist/graph/index.js.map +1 -0
  185. package/dist/index.d.ts +2 -0
  186. package/dist/index.d.ts.map +1 -0
  187. package/dist/index.js +837 -0
  188. package/dist/index.js.map +1 -0
  189. package/dist/intake/index.d.ts +6 -0
  190. package/dist/intake/index.d.ts.map +1 -0
  191. package/dist/intake/index.js +5 -0
  192. package/dist/intake/index.js.map +1 -0
  193. package/dist/intake/intake-processor.d.ts +17 -0
  194. package/dist/intake/intake-processor.d.ts.map +1 -0
  195. package/dist/intake/intake-processor.js +99 -0
  196. package/dist/intake/intake-processor.js.map +1 -0
  197. package/dist/intake/intake-store.d.ts +7 -0
  198. package/dist/intake/intake-store.d.ts.map +1 -0
  199. package/dist/intake/intake-store.js +7 -0
  200. package/dist/intake/intake-store.js.map +1 -0
  201. package/dist/intake/registry-poller.d.ts +41 -0
  202. package/dist/intake/registry-poller.d.ts.map +1 -0
  203. package/dist/intake/registry-poller.js +202 -0
  204. package/dist/intake/registry-poller.js.map +1 -0
  205. package/dist/intake/webhook-handlers.d.ts +37 -0
  206. package/dist/intake/webhook-handlers.d.ts.map +1 -0
  207. package/dist/intake/webhook-handlers.js +268 -0
  208. package/dist/intake/webhook-handlers.js.map +1 -0
  209. package/dist/logger.d.ts +5 -0
  210. package/dist/logger.d.ts.map +1 -0
  211. package/dist/logger.js +15 -0
  212. package/dist/logger.js.map +1 -0
  213. package/dist/mcp/resources.d.ts +9 -0
  214. package/dist/mcp/resources.d.ts.map +1 -0
  215. package/dist/mcp/resources.js +72 -0
  216. package/dist/mcp/resources.js.map +1 -0
  217. package/dist/mcp/server.d.ts +15 -0
  218. package/dist/mcp/server.d.ts.map +1 -0
  219. package/dist/mcp/server.js +20 -0
  220. package/dist/mcp/server.js.map +1 -0
  221. package/dist/mcp/tools.d.ts +9 -0
  222. package/dist/mcp/tools.d.ts.map +1 -0
  223. package/dist/mcp/tools.js +88 -0
  224. package/dist/mcp/tools.js.map +1 -0
  225. package/dist/middleware/auth.d.ts +29 -0
  226. package/dist/middleware/auth.d.ts.map +1 -0
  227. package/dist/middleware/auth.js +76 -0
  228. package/dist/middleware/auth.js.map +1 -0
  229. package/dist/middleware/permissions.d.ts +13 -0
  230. package/dist/middleware/permissions.d.ts.map +1 -0
  231. package/dist/middleware/permissions.js +32 -0
  232. package/dist/middleware/permissions.js.map +1 -0
  233. package/dist/pattern-store.d.ts +104 -0
  234. package/dist/pattern-store.d.ts.map +1 -0
  235. package/dist/pattern-store.js +299 -0
  236. package/dist/pattern-store.js.map +1 -0
  237. package/package.json +54 -0
  238. package/src/agent/debrief-retention.ts +44 -0
  239. package/src/agent/envoy-client.ts +474 -0
  240. package/src/agent/envoy-registry.ts +384 -0
  241. package/src/agent/health-checker.ts +70 -0
  242. package/src/agent/mcp-client-manager.ts +131 -0
  243. package/src/agent/stale-deployment-detector.ts +79 -0
  244. package/src/agent/step-runner.ts +124 -0
  245. package/src/agent/synth-agent.ts +1567 -0
  246. package/src/api/agent.ts +1075 -0
  247. package/src/api/api-keys.ts +129 -0
  248. package/src/api/artifacts.ts +194 -0
  249. package/src/api/auth.ts +320 -0
  250. package/src/api/deployments.ts +1347 -0
  251. package/src/api/environments.ts +97 -0
  252. package/src/api/envoy-reports.ts +159 -0
  253. package/src/api/envoys.ts +237 -0
  254. package/src/api/fleet.ts +510 -0
  255. package/src/api/graph.ts +516 -0
  256. package/src/api/health.ts +311 -0
  257. package/src/api/idp-schemas.ts +19 -0
  258. package/src/api/idp.ts +735 -0
  259. package/src/api/intake.ts +537 -0
  260. package/src/api/partitions.ts +147 -0
  261. package/src/api/progress-event-store.ts +153 -0
  262. package/src/api/schemas.ts +376 -0
  263. package/src/api/security-boundaries.ts +54 -0
  264. package/src/api/settings.ts +118 -0
  265. package/src/api/system.ts +704 -0
  266. package/src/api/telemetry.ts +32 -0
  267. package/src/api/users.ts +210 -0
  268. package/src/archive-unpacker.ts +271 -0
  269. package/src/artifact-analyzer.ts +438 -0
  270. package/src/auth/idp/index.ts +8 -0
  271. package/src/auth/idp/ldap.ts +340 -0
  272. package/src/auth/idp/oidc.ts +117 -0
  273. package/src/auth/idp/role-mapping.ts +22 -0
  274. package/src/auth/idp/saml.ts +148 -0
  275. package/src/auth/idp/types.ts +22 -0
  276. package/src/fleet/fleet-executor.ts +309 -0
  277. package/src/fleet/fleet-store.ts +13 -0
  278. package/src/fleet/index.ts +4 -0
  279. package/src/fleet/representative-selector.ts +83 -0
  280. package/src/graph/graph-executor.ts +446 -0
  281. package/src/graph/graph-inference.ts +184 -0
  282. package/src/graph/graph-store.ts +75 -0
  283. package/src/graph/index.ts +4 -0
  284. package/src/index.ts +916 -0
  285. package/src/intake/index.ts +5 -0
  286. package/src/intake/intake-processor.ts +111 -0
  287. package/src/intake/intake-store.ts +7 -0
  288. package/src/intake/registry-poller.ts +230 -0
  289. package/src/intake/webhook-handlers.ts +328 -0
  290. package/src/logger.ts +19 -0
  291. package/src/mcp/resources.ts +98 -0
  292. package/src/mcp/server.ts +34 -0
  293. package/src/mcp/tools.ts +117 -0
  294. package/src/middleware/auth.ts +103 -0
  295. package/src/middleware/permissions.ts +35 -0
  296. package/src/pattern-store.ts +409 -0
  297. package/tests/agent-mode.test.ts +536 -0
  298. package/tests/api-handlers.test.ts +1245 -0
  299. package/tests/archive-unpacker.test.ts +179 -0
  300. package/tests/artifact-analyzer.test.ts +240 -0
  301. package/tests/auth-middleware.test.ts +189 -0
  302. package/tests/decision-diary.test.ts +957 -0
  303. package/tests/diary-reader.test.ts +782 -0
  304. package/tests/envoy-client.test.ts +342 -0
  305. package/tests/envoy-reports.test.ts +156 -0
  306. package/tests/mcp-tools.test.ts +213 -0
  307. package/tests/orchestration.test.ts +536 -0
  308. package/tests/partition-deletion.test.ts +143 -0
  309. package/tests/partition-isolation.test.ts +830 -0
  310. package/tests/pattern-store.test.ts +371 -0
  311. package/tests/rbac-enforcement.test.ts +409 -0
  312. package/tests/ssrf-validation.test.ts +56 -0
  313. package/tests/stale-deployment.test.ts +85 -0
  314. package/tests/step-runner.test.ts +308 -0
  315. package/tests/ui-journey.test.ts +330 -0
  316. package/tsconfig.json +11 -0
  317. package/vitest.config.ts +27 -0
@@ -0,0 +1,782 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ DecisionDebrief,
4
+ ArtifactStore,
5
+ EnvironmentStore,
6
+ PartitionStore,
7
+ generatePostmortem,
8
+ generateOperationHistory,
9
+ } from "@synth-deploy/core";
10
+ import type {
11
+ Partition,
12
+ Environment,
13
+ Deployment,
14
+ } from "@synth-deploy/core";
15
+ import {
16
+ SynthAgent,
17
+ InMemoryDeploymentStore,
18
+ } from "../src/agent/synth-agent.js";
19
+ import type {
20
+ ServiceHealthChecker,
21
+ HealthCheckResult,
22
+ } from "../src/agent/health-checker.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Test helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ class MockHealthChecker implements ServiceHealthChecker {
29
+ private responses: HealthCheckResult[] = [];
30
+
31
+ willReturn(...results: HealthCheckResult[]): void {
32
+ this.responses.push(...results);
33
+ }
34
+
35
+ async check(): Promise<HealthCheckResult> {
36
+ const next = this.responses.shift();
37
+ if (next) return next;
38
+ return { reachable: true, responseTimeMs: 1, error: null };
39
+ }
40
+ }
41
+
42
+ const HEALTHY: HealthCheckResult = {
43
+ reachable: true,
44
+ responseTimeMs: 5,
45
+ error: null,
46
+ };
47
+
48
+ const CONN_REFUSED: HealthCheckResult = {
49
+ reachable: false,
50
+ responseTimeMs: null,
51
+ error: "ECONNREFUSED: Connection refused",
52
+ };
53
+
54
+ const DNS_FAILURE: HealthCheckResult = {
55
+ reachable: false,
56
+ responseTimeMs: null,
57
+ error: "ENOTFOUND: DNS resolution failed for production.example.com",
58
+ };
59
+
60
+ function makePartition(overrides: Partial<Partition> = {}): Partition {
61
+ return {
62
+ id: "partition-1",
63
+ name: "Acme Corp",
64
+ variables: {},
65
+ createdAt: new Date(),
66
+ ...overrides,
67
+ };
68
+ }
69
+
70
+ function makeEnvironment(overrides: Partial<Environment> = {}): Environment {
71
+ return {
72
+ id: "env-prod",
73
+ name: "production",
74
+ variables: {},
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ const DEFAULT_ARTIFACT_ID = "artifact-web-app";
80
+
81
+ function forceInsertPartition(store: PartitionStore, p: Partition): void {
82
+ if (!store.get(p.id)) {
83
+ (store as any).partitions.set(p.id, p);
84
+ }
85
+ }
86
+
87
+ function forceInsertEnvironment(store: EnvironmentStore, e: Environment): void {
88
+ if (!store.get(e.id)) {
89
+ (store as any).environments.set(e.id, e);
90
+ }
91
+ }
92
+
93
+ function forceInsertArtifact(store: ArtifactStore, id: string, name: string): void {
94
+ if (!store.get(id)) {
95
+ (store as any).artifacts.set(id, {
96
+ id,
97
+ name,
98
+ type: "nodejs",
99
+ analysis: {
100
+ summary: "test artifact",
101
+ dependencies: [],
102
+ configurationExpectations: {},
103
+ deploymentIntent: "rolling",
104
+ confidence: 0.9,
105
+ },
106
+ annotations: [],
107
+ learningHistory: [],
108
+ createdAt: new Date(),
109
+ updatedAt: new Date(),
110
+ });
111
+ }
112
+ }
113
+
114
+ interface TestStores {
115
+ partitions: PartitionStore;
116
+ environments: EnvironmentStore;
117
+ artifacts: ArtifactStore;
118
+ }
119
+
120
+ /**
121
+ * Test convenience: seeds stores with entities and triggers a deployment.
122
+ * Mirrors the old testDeploy helper that accepted bare partition/environment/operation objects.
123
+ */
124
+ async function testDeploy(
125
+ agent: SynthAgent,
126
+ stores: TestStores,
127
+ oldTrigger: { partitionId?: string; version?: string; variables?: Record<string, string> },
128
+ partition?: Partition,
129
+ environment?: Environment,
130
+ ): Promise<Deployment> {
131
+ const p = partition ?? makePartition();
132
+ const e = environment ?? makeEnvironment();
133
+ const version = oldTrigger.version ?? "2.0.0";
134
+
135
+ const effectivePartition = oldTrigger.partitionId && oldTrigger.partitionId !== p.id
136
+ ? makePartition({ id: oldTrigger.partitionId })
137
+ : p;
138
+
139
+ // Ensure entities are in stores so the agent can look them up
140
+ forceInsertPartition(stores.partitions, effectivePartition);
141
+ forceInsertEnvironment(stores.environments, e);
142
+ forceInsertArtifact(stores.artifacts, DEFAULT_ARTIFACT_ID, "web-app");
143
+
144
+ return agent.triggerDeployment({
145
+ artifactId: DEFAULT_ARTIFACT_ID,
146
+ artifactVersionId: version,
147
+ environmentId: e.id,
148
+ partitionId: effectivePartition.id,
149
+ triggeredBy: "user",
150
+ variables: oldTrigger.variables,
151
+ });
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // SCENARIO 1: Simulated Postmortem
156
+ // ---------------------------------------------------------------------------
157
+ //
158
+ // Success condition: Given a deployment that failed, a reviewer should be
159
+ // able to read the Diary and understand exactly what the agent decided,
160
+ // why it rolled back or continued, and what the suggested fix is,
161
+ // without reading any log files.
162
+ // ---------------------------------------------------------------------------
163
+
164
+ describe("Simulated Postmortem — failed deployment read experience", () => {
165
+ let diary: DecisionDebrief;
166
+ let deployments: InMemoryDeploymentStore;
167
+ let healthChecker: MockHealthChecker;
168
+ let agent: SynthAgent;
169
+ let stores: TestStores;
170
+
171
+ beforeEach(() => {
172
+ diary = new DecisionDebrief();
173
+ deployments = new InMemoryDeploymentStore();
174
+ healthChecker = new MockHealthChecker();
175
+ const partStore = new PartitionStore();
176
+ const envStore = new EnvironmentStore();
177
+ const artStore = new ArtifactStore();
178
+ stores = { partitions: partStore, environments: envStore, artifacts: artStore };
179
+ agent = new SynthAgent(diary, deployments, artStore, envStore, partStore, healthChecker, {
180
+ healthCheckBackoffMs: 1,
181
+ executionDelayMs: 1,
182
+ });
183
+ });
184
+
185
+ it("health check failure postmortem — reviewer understands what happened without logs", async () => {
186
+ // Scenario: deployment fails because target environment is unreachable
187
+ healthChecker.willReturn(CONN_REFUSED, CONN_REFUSED);
188
+
189
+ const deployment = await testDeploy(agent, stores, {});
190
+
191
+ expect(deployment.status).toBe("failed");
192
+
193
+ const entries = diary.getByDeployment(deployment.id);
194
+ const postmortem = generatePostmortem(entries, deployment);
195
+
196
+ // 1. Reviewer can identify WHAT was being deployed
197
+ expect(postmortem.summary).toContain("web-app");
198
+ expect(postmortem.summary).toContain("v2.0.0");
199
+ expect(postmortem.summary).toContain("production");
200
+ expect(postmortem.summary).toContain("Acme Corp");
201
+ expect(postmortem.summary).toContain("FAILED");
202
+
203
+ // 2. Reviewer can see every decision in chronological order
204
+ expect(postmortem.timeline.length).toBeGreaterThanOrEqual(3);
205
+ const steps = postmortem.timeline.map((t) => t.step);
206
+ expect(steps).toContain("pipeline-plan");
207
+ expect(steps).toContain("configuration-resolved");
208
+ expect(steps).toContain("health-check");
209
+
210
+ // 3. Reviewer can understand WHY it failed
211
+ expect(postmortem.failureAnalysis).not.toBeNull();
212
+ expect(postmortem.failureAnalysis!.failedStep).toBe(
213
+ "preflight-health-check",
214
+ );
215
+ expect(postmortem.failureAnalysis!.whatHappened).toContain(
216
+ "Deployment failed",
217
+ );
218
+ expect(postmortem.failureAnalysis!.whyItFailed).toContain(
219
+ "ECONNREFUSED",
220
+ );
221
+
222
+ // 4. Reviewer gets a SUGGESTED FIX
223
+ expect(postmortem.failureAnalysis!.suggestedFix.length).toBeGreaterThan(
224
+ 10,
225
+ );
226
+ expect(
227
+ postmortem.failureAnalysis!.suggestedFix.toLowerCase(),
228
+ ).toContain("verify");
229
+
230
+ // 5. The formatted output is self-contained
231
+ expect(postmortem.formatted).toContain("# Deployment Postmortem");
232
+ expect(postmortem.formatted).toContain("## Summary");
233
+ expect(postmortem.formatted).toContain("## Decision Timeline");
234
+ expect(postmortem.formatted).toContain("## Failure Analysis");
235
+ expect(postmortem.formatted).toContain("### Suggested Fix");
236
+ expect(postmortem.formatted).toContain("## Outcome");
237
+ });
238
+
239
+ it("DNS failure postmortem — immediate abort explained clearly", async () => {
240
+ healthChecker.willReturn(DNS_FAILURE);
241
+
242
+ const deployment = await testDeploy(agent, stores, {});
243
+
244
+ const entries = diary.getByDeployment(deployment.id);
245
+ const postmortem = generatePostmortem(entries, deployment);
246
+
247
+ // The postmortem should explain that DNS failures don't benefit from retry
248
+ expect(postmortem.failureAnalysis).not.toBeNull();
249
+ expect(postmortem.failureAnalysis!.whyItFailed).toContain("DNS");
250
+ expect(postmortem.failureAnalysis!.suggestedFix.toLowerCase()).toContain(
251
+ "dns",
252
+ );
253
+
254
+ // Timeline should show the abort happened without retry
255
+ const healthEntries = postmortem.timeline.filter(
256
+ (t) => t.step === "health-check",
257
+ );
258
+ expect(healthEntries.length).toBeGreaterThanOrEqual(1);
259
+ const abortEntry = healthEntries.find((h) =>
260
+ h.decision.toLowerCase().includes("abort"),
261
+ );
262
+ expect(abortEntry).toBeDefined();
263
+ });
264
+
265
+ it("configuration block postmortem — cross-env conflict explained", async () => {
266
+ // Two connectivity variables pointing cross-environment → should block
267
+ const partition = makePartition({
268
+ variables: {
269
+ DB_HOST: "staging-db.internal",
270
+ API_ENDPOINT: "https://staging-api.example.com",
271
+ },
272
+ });
273
+ const env = makeEnvironment({
274
+ name: "production",
275
+ variables: {
276
+ DB_HOST: "prod-db.internal",
277
+ API_ENDPOINT: "https://prod-api.example.com",
278
+ },
279
+ });
280
+
281
+ healthChecker.willReturn(HEALTHY);
282
+
283
+ const deployment = await testDeploy(agent, stores, {}, partition, env);
284
+
285
+ expect(deployment.status).toBe("failed");
286
+
287
+ const entries = diary.getByDeployment(deployment.id);
288
+ const postmortem = generatePostmortem(entries, deployment);
289
+
290
+ // Should explain the configuration block
291
+ expect(postmortem.failureAnalysis).not.toBeNull();
292
+ expect(postmortem.failureAnalysis!.failedStep).toBe(
293
+ "resolve-configuration",
294
+ );
295
+
296
+ // Conflicts should be surfaced
297
+ expect(postmortem.configuration.conflictCount).toBeGreaterThan(0);
298
+ expect(postmortem.configuration.conflicts.length).toBeGreaterThan(0);
299
+
300
+ // Suggested fix should mention verifying variable bindings
301
+ expect(postmortem.failureAnalysis!.suggestedFix.length).toBeGreaterThan(
302
+ 10,
303
+ );
304
+
305
+ // Formatted output should contain conflict details
306
+ expect(postmortem.formatted).toContain("Conflicts");
307
+ });
308
+
309
+ it("successful deployment postmortem — no failure analysis, clean outcome", async () => {
310
+ healthChecker.willReturn(HEALTHY);
311
+
312
+ const deployment = await testDeploy(agent, stores, {});
313
+
314
+ expect(deployment.status).toBe("succeeded");
315
+
316
+ const entries = diary.getByDeployment(deployment.id);
317
+ const postmortem = generatePostmortem(entries, deployment);
318
+
319
+ expect(postmortem.summary).toContain("SUCCEEDED");
320
+ expect(postmortem.failureAnalysis).toBeNull();
321
+ expect(postmortem.outcome).toContain("completed");
322
+
323
+ // Should NOT contain failure analysis section in formatted output
324
+ expect(postmortem.formatted).not.toContain("## Failure Analysis");
325
+ expect(postmortem.formatted).toContain("## Outcome");
326
+ });
327
+
328
+ it("retry-then-succeed postmortem — shows the full decision chain", async () => {
329
+ // First health check fails (connection refused), retry succeeds
330
+ healthChecker.willReturn(CONN_REFUSED, HEALTHY);
331
+
332
+ const deployment = await testDeploy(agent, stores, {});
333
+
334
+ expect(deployment.status).toBe("succeeded");
335
+
336
+ const entries = diary.getByDeployment(deployment.id);
337
+ const postmortem = generatePostmortem(entries, deployment);
338
+
339
+ // Timeline should show the retry decision chain
340
+ const healthEntries = postmortem.timeline.filter(
341
+ (t) => t.step === "health-check",
342
+ );
343
+ expect(healthEntries.length).toBeGreaterThanOrEqual(2);
344
+
345
+ // Should show retry decision and recovery
346
+ const retryEntry = healthEntries.find((h) =>
347
+ h.decision.toLowerCase().includes("retry"),
348
+ );
349
+ const recoveryEntry = healthEntries.find((h) =>
350
+ h.decision.toLowerCase().includes("recovered"),
351
+ );
352
+ expect(retryEntry).toBeDefined();
353
+ expect(recoveryEntry).toBeDefined();
354
+
355
+ // No failure analysis — deployment ultimately succeeded
356
+ expect(postmortem.failureAnalysis).toBeNull();
357
+ expect(postmortem.summary).toContain("SUCCEEDED");
358
+ });
359
+
360
+ it("postmortem formatted output is self-contained — readable without any other document", async () => {
361
+ healthChecker.willReturn(CONN_REFUSED, CONN_REFUSED);
362
+
363
+ const deployment = await testDeploy(
364
+ agent,
365
+ stores,
366
+ { version: "3.1.0" },
367
+ makePartition({ name: "Widget Inc" }),
368
+ makeEnvironment({ name: "staging" }),
369
+ );
370
+
371
+ const entries = diary.getByDeployment(deployment.id);
372
+ const postmortem = generatePostmortem(entries, deployment);
373
+
374
+ const text = postmortem.formatted;
375
+
376
+ // Contains all identifying information
377
+ expect(text).toContain("web-app");
378
+ expect(text).toContain("3.1.0");
379
+ expect(text).toContain("staging");
380
+ expect(text).toContain("Widget Inc");
381
+ expect(text).toContain(deployment.id);
382
+
383
+ // Contains timing information
384
+ expect(text).toContain("Started:");
385
+ expect(text).toContain("Completed:");
386
+ expect(text).toContain("Duration:");
387
+
388
+ // Contains the decision timeline with reasoning
389
+ expect(text).toContain("PIPELINE-PLAN");
390
+ expect(text).toContain("Decision:");
391
+ expect(text).toContain("Reasoning:");
392
+
393
+ // Contains suggested fix
394
+ expect(text).toContain("Suggested Fix");
395
+
396
+ // A reviewer reading this text can answer:
397
+ // - What was deployed? (artifact, version, environment, partition)
398
+ // - What did the agent decide? (timeline)
399
+ // - Why did it fail? (failure analysis)
400
+ // - What should I do? (suggested fix)
401
+ });
402
+ });
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // SCENARIO 2: Simulated Onboarding Read
406
+ // ---------------------------------------------------------------------------
407
+ //
408
+ // Success condition: Given an artifact with 10 deployments in its history,
409
+ // a new engineer should be able to read the Diary and understand the
410
+ // artifact's configuration decisions and deployment patterns.
411
+ // ---------------------------------------------------------------------------
412
+
413
+ describe("Simulated Onboarding — operation history read experience", () => {
414
+ let diary: DecisionDebrief;
415
+ let deploymentStore: InMemoryDeploymentStore;
416
+ let healthChecker: MockHealthChecker;
417
+ let agent: SynthAgent;
418
+ let stores: TestStores;
419
+
420
+ beforeEach(() => {
421
+ diary = new DecisionDebrief();
422
+ deploymentStore = new InMemoryDeploymentStore();
423
+ healthChecker = new MockHealthChecker();
424
+ const partStore = new PartitionStore();
425
+ const envStore = new EnvironmentStore();
426
+ const artStore = new ArtifactStore();
427
+ stores = { partitions: partStore, environments: envStore, artifacts: artStore };
428
+ agent = new SynthAgent(diary, deploymentStore, artStore, envStore, partStore, healthChecker, {
429
+ healthCheckBackoffMs: 1,
430
+ executionDelayMs: 1,
431
+ });
432
+ });
433
+
434
+ async function runDeploymentHistory(): Promise<Deployment[]> {
435
+ const partition = makePartition({
436
+ id: "acme",
437
+ name: "Acme Corp",
438
+ variables: { APP_ENV: "production", LOG_LEVEL: "warn" },
439
+ });
440
+ const prodEnv = makeEnvironment({
441
+ id: "env-prod",
442
+ name: "production",
443
+ variables: { APP_ENV: "production", LOG_LEVEL: "info" },
444
+ });
445
+ const stagingEnv = makeEnvironment({
446
+ id: "env-staging",
447
+ name: "staging",
448
+ variables: { APP_ENV: "staging", LOG_LEVEL: "debug" },
449
+ });
450
+
451
+ const results: Deployment[] = [];
452
+
453
+ // Deployment 1: v1.0.0 to staging — clean success
454
+ healthChecker.willReturn(HEALTHY);
455
+ results.push(
456
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.0.0" }, partition, stagingEnv),
457
+ );
458
+
459
+ // Deployment 2: v1.0.0 to production — clean success
460
+ healthChecker.willReturn(HEALTHY);
461
+ results.push(
462
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.0.0" }, partition, prodEnv),
463
+ );
464
+
465
+ // Deployment 3: v1.1.0 to staging — with LOG_LEVEL conflict
466
+ healthChecker.willReturn(HEALTHY);
467
+ results.push(
468
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.1.0", variables: { LOG_LEVEL: "error" } }, partition, stagingEnv),
469
+ );
470
+
471
+ // Deployment 4: v1.1.0 to production — health check fails then recovers
472
+ healthChecker.willReturn(CONN_REFUSED, HEALTHY);
473
+ results.push(
474
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.1.0" }, partition, prodEnv),
475
+ );
476
+
477
+ // Deployment 5: v1.2.0 to staging — clean
478
+ healthChecker.willReturn(HEALTHY);
479
+ results.push(
480
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.2.0" }, partition, stagingEnv),
481
+ );
482
+
483
+ // Deployment 6: v1.2.0 to production — DNS failure
484
+ healthChecker.willReturn(DNS_FAILURE);
485
+ results.push(
486
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.2.0" }, partition, prodEnv),
487
+ );
488
+
489
+ // Deployment 7: v1.2.0 to production retry — succeeds after fix
490
+ healthChecker.willReturn(HEALTHY);
491
+ results.push(
492
+ await testDeploy(agent, stores, { partitionId: "acme", version: "1.2.0" }, partition, prodEnv),
493
+ );
494
+
495
+ // Deployment 8: v2.0.0 to staging — clean
496
+ healthChecker.willReturn(HEALTHY);
497
+ results.push(
498
+ await testDeploy(agent, stores, { partitionId: "acme", version: "2.0.0" }, partition, stagingEnv),
499
+ );
500
+
501
+ // Deployment 9: v2.0.0 to production — with variable conflict
502
+ healthChecker.willReturn(HEALTHY);
503
+ results.push(
504
+ await testDeploy(agent, stores, { partitionId: "acme", version: "2.0.0", variables: { LOG_LEVEL: "debug" } }, partition, prodEnv),
505
+ );
506
+
507
+ // Deployment 10: v2.1.0 to staging — clean
508
+ healthChecker.willReturn(HEALTHY);
509
+ results.push(
510
+ await testDeploy(agent, stores, { partitionId: "acme", version: "2.1.0" }, partition, stagingEnv),
511
+ );
512
+
513
+ return results;
514
+ }
515
+
516
+ it("new engineer can see overall operation health at a glance", async () => {
517
+ const deploymentResults = await runDeploymentHistory();
518
+ const allEntries = diary.getByPartition("acme");
519
+ const history = generateOperationHistory(allEntries, deploymentResults);
520
+
521
+ // Overview tells the engineer the big picture
522
+ expect(history.overview.totalDeployments).toBe(10);
523
+ expect(history.overview.succeeded).toBe(9);
524
+ expect(history.overview.failed).toBe(1);
525
+ expect(history.overview.successRate).toBe("90%");
526
+
527
+ // Environments used
528
+ expect(history.overview.environments).toContain("production");
529
+ expect(history.overview.environments).toContain("staging");
530
+
531
+ // Versions deployed
532
+ expect(history.overview.versions).toContain("1.0.0");
533
+ expect(history.overview.versions).toContain("1.1.0");
534
+ expect(history.overview.versions).toContain("1.2.0");
535
+ expect(history.overview.versions).toContain("2.0.0");
536
+ expect(history.overview.versions).toContain("2.1.0");
537
+ });
538
+
539
+ it("new engineer can trace every deployment outcome", async () => {
540
+ const deploymentResults = await runDeploymentHistory();
541
+ const allEntries = diary.getByPartition("acme");
542
+ const history = generateOperationHistory(allEntries, deploymentResults);
543
+
544
+ // All 10 deployments are listed
545
+ expect(history.deployments).toHaveLength(10);
546
+
547
+ // Each deployment has version, environment, outcome, and key decision
548
+ for (const d of history.deployments) {
549
+ expect(d.version).toBeTruthy();
550
+ expect(d.environment).toBeTruthy();
551
+ expect(["succeeded", "failed"]).toContain(d.outcome);
552
+ expect(d.keyDecision.length).toBeGreaterThan(5);
553
+ }
554
+
555
+ // Deployment 6 (v1.2.0 to production) was the failure
556
+ const failedDeploys = history.deployments.filter(
557
+ (d) => d.outcome === "failed",
558
+ );
559
+ expect(failedDeploys).toHaveLength(1);
560
+ expect(failedDeploys[0].version).toBe("1.2.0");
561
+ expect(failedDeploys[0].environment).toBe("production");
562
+ });
563
+
564
+ it("new engineer can see configuration patterns and recurring issues", async () => {
565
+ const deploymentResults = await runDeploymentHistory();
566
+ const allEntries = diary.getByPartition("acme");
567
+ const history = generateOperationHistory(allEntries, deploymentResults);
568
+
569
+ // Configuration patterns are surfaced
570
+ expect(history.configurationPatterns.length).toBeGreaterThan(0);
571
+
572
+ // Should see variable conflict pattern
573
+ const conflictPattern = history.configurationPatterns.find((p) =>
574
+ p.pattern.toLowerCase().includes("conflict") ||
575
+ p.pattern.toLowerCase().includes("override"),
576
+ );
577
+ expect(conflictPattern).toBeDefined();
578
+ expect(conflictPattern!.occurrences).toBeGreaterThanOrEqual(1);
579
+ expect(conflictPattern!.detail.length).toBeGreaterThan(10);
580
+ });
581
+
582
+ it("new engineer can understand per-environment behavior", async () => {
583
+ const deploymentResults = await runDeploymentHistory();
584
+ const allEntries = diary.getByPartition("acme");
585
+ const history = generateOperationHistory(allEntries, deploymentResults);
586
+
587
+ // Environment notes present for both environments
588
+ expect(history.environmentNotes.length).toBe(2);
589
+
590
+ const prodNotes = history.environmentNotes.find(
591
+ (n) => n.environment === "production",
592
+ );
593
+ const stagingNotes = history.environmentNotes.find(
594
+ (n) => n.environment === "staging",
595
+ );
596
+
597
+ expect(prodNotes).toBeDefined();
598
+ expect(stagingNotes).toBeDefined();
599
+
600
+ // Production had failures
601
+ expect(prodNotes!.deploymentCount).toBe(5);
602
+ // 4 succeeded, 1 failed = 80%
603
+ expect(prodNotes!.successRate).toBe("80%");
604
+ // Production should have notes about failures
605
+ const hasFailureNote = prodNotes!.notes.some((n) =>
606
+ n.toLowerCase().includes("failure"),
607
+ );
608
+ expect(hasFailureNote).toBe(true);
609
+
610
+ // Staging was clean
611
+ expect(stagingNotes!.deploymentCount).toBe(5);
612
+ expect(stagingNotes!.successRate).toBe("100%");
613
+ });
614
+
615
+ it("new engineer can read the formatted output and understand everything", async () => {
616
+ const deploymentResults = await runDeploymentHistory();
617
+ const allEntries = diary.getByPartition("acme");
618
+ const history = generateOperationHistory(allEntries, deploymentResults);
619
+
620
+ const text = history.formatted;
621
+
622
+ // Structure is present
623
+ expect(text).toContain("# Operation Deployment History");
624
+ expect(text).toContain("## Overview");
625
+ expect(text).toContain("## Deployment Timeline");
626
+ expect(text).toContain("## Configuration Patterns");
627
+ expect(text).toContain("## Environment Notes");
628
+
629
+ // Key stats are visible
630
+ expect(text).toContain("Total deployments: 10");
631
+ expect(text).toContain("9 succeeded");
632
+ expect(text).toContain("1 failed");
633
+ expect(text).toContain("90%");
634
+
635
+ // Deployment timeline entries are numbered
636
+ expect(text).toContain("1. v1.0.0");
637
+ expect(text).toContain("10. v2.1.0");
638
+
639
+ // Environment breakdown is present
640
+ expect(text).toContain("### production");
641
+ expect(text).toContain("### staging");
642
+
643
+ // A new engineer reading this text can answer:
644
+ // - How many deployments has this artifact had? (10)
645
+ // - What's the success rate? (90%)
646
+ // - Which environments are used? (production, staging)
647
+ // - What versions have been deployed? (1.0.0 through 2.1.0)
648
+ // - Are there recurring configuration issues? (yes, variable conflicts)
649
+ // - Which environment has problems? (production — 80% vs staging 100%)
650
+ // - What kind of failures occur? (DNS, health check issues)
651
+ });
652
+
653
+ it("deployment timeline shows outcome markers (OK vs FAILED) for quick scanning", async () => {
654
+ const deploymentResults = await runDeploymentHistory();
655
+ const allEntries = diary.getByPartition("acme");
656
+ const history = generateOperationHistory(allEntries, deploymentResults);
657
+
658
+ const text = history.formatted;
659
+
660
+ // Quick-scan markers
661
+ const okCount = (text.match(/-- OK/g) || []).length;
662
+ const failedCount = (text.match(/-- FAILED/g) || []).length;
663
+
664
+ expect(okCount).toBe(9);
665
+ expect(failedCount).toBe(1);
666
+ });
667
+
668
+ it("deployment timeline shows conflict counts where they occurred", async () => {
669
+ const deploymentResults = await runDeploymentHistory();
670
+ const allEntries = diary.getByPartition("acme");
671
+ const history = generateOperationHistory(allEntries, deploymentResults);
672
+
673
+ // Deployments with conflicts should have conflict counts
674
+ const deploymentsWithConflicts = history.deployments.filter(
675
+ (d) => d.conflictCount > 0,
676
+ );
677
+ expect(deploymentsWithConflicts.length).toBeGreaterThan(0);
678
+
679
+ // The formatted text should show conflict markers
680
+ const text = history.formatted;
681
+ expect(text).toContain("conflict");
682
+ });
683
+
684
+ it("handles an operation with zero deployments gracefully", () => {
685
+ const history = generateOperationHistory([], []);
686
+
687
+ expect(history.overview.totalDeployments).toBe(0);
688
+ expect(history.overview.successRate).toBe("N/A");
689
+ expect(history.deployments).toHaveLength(0);
690
+ expect(history.configurationPatterns).toHaveLength(0);
691
+ expect(history.environmentNotes).toHaveLength(0);
692
+
693
+ expect(history.formatted).toContain("Total deployments: 0");
694
+ });
695
+ });
696
+
697
+ // ---------------------------------------------------------------------------
698
+ // Postmortem structural guarantees
699
+ // ---------------------------------------------------------------------------
700
+
701
+ describe("Postmortem report — structural guarantees", () => {
702
+ let diary: DecisionDebrief;
703
+ let deployments: InMemoryDeploymentStore;
704
+ let healthChecker: MockHealthChecker;
705
+ let agent: SynthAgent;
706
+ let stores: TestStores;
707
+
708
+ beforeEach(() => {
709
+ diary = new DecisionDebrief();
710
+ deployments = new InMemoryDeploymentStore();
711
+ healthChecker = new MockHealthChecker();
712
+ const partStore = new PartitionStore();
713
+ const envStore = new EnvironmentStore();
714
+ const artStore = new ArtifactStore();
715
+ stores = { partitions: partStore, environments: envStore, artifacts: artStore };
716
+ agent = new SynthAgent(diary, deployments, artStore, envStore, partStore, healthChecker, {
717
+ healthCheckBackoffMs: 1,
718
+ executionDelayMs: 1,
719
+ });
720
+ });
721
+
722
+ it("timeline entries are sorted chronologically", async () => {
723
+ healthChecker.willReturn(CONN_REFUSED, HEALTHY);
724
+
725
+ const deployment = await testDeploy(agent, stores, {});
726
+
727
+ const entries = diary.getByDeployment(deployment.id);
728
+ const postmortem = generatePostmortem(entries, deployment);
729
+
730
+ for (let i = 1; i < postmortem.timeline.length; i++) {
731
+ expect(
732
+ postmortem.timeline[i].timestamp.getTime(),
733
+ ).toBeGreaterThanOrEqual(
734
+ postmortem.timeline[i - 1].timestamp.getTime(),
735
+ );
736
+ }
737
+ });
738
+
739
+ it("configuration section accurately reflects variable and conflict counts", async () => {
740
+ const partition = makePartition({
741
+ variables: { LOG_LEVEL: "error", APP_ENV: "production" },
742
+ });
743
+ const env = makeEnvironment({
744
+ variables: { LOG_LEVEL: "warn", APP_ENV: "production", DB_POOL: "10" },
745
+ });
746
+ healthChecker.willReturn(HEALTHY);
747
+
748
+ const deployment = await testDeploy(agent, stores, { variables: { LOG_LEVEL: "debug" } }, partition, env);
749
+
750
+ const entries = diary.getByDeployment(deployment.id);
751
+ const postmortem = generatePostmortem(entries, deployment);
752
+
753
+ // LOG_LEVEL has three-way conflict (env → partition → trigger), total vars = 3
754
+ expect(postmortem.configuration.variableCount).toBeGreaterThanOrEqual(3);
755
+ expect(postmortem.configuration.conflictCount).toBeGreaterThanOrEqual(1);
756
+ });
757
+
758
+ it("failure analysis is null for successful deployments", async () => {
759
+ healthChecker.willReturn(HEALTHY);
760
+
761
+ const deployment = await testDeploy(agent, stores, {});
762
+
763
+ const entries = diary.getByDeployment(deployment.id);
764
+ const postmortem = generatePostmortem(entries, deployment);
765
+
766
+ expect(postmortem.failureAnalysis).toBeNull();
767
+ });
768
+
769
+ it("failure analysis includes the failed step name", async () => {
770
+ healthChecker.willReturn(DNS_FAILURE);
771
+
772
+ const deployment = await testDeploy(agent, stores, {});
773
+
774
+ const entries = diary.getByDeployment(deployment.id);
775
+ const postmortem = generatePostmortem(entries, deployment);
776
+
777
+ expect(postmortem.failureAnalysis).not.toBeNull();
778
+ expect(postmortem.failureAnalysis!.failedStep).toBe(
779
+ "preflight-health-check",
780
+ );
781
+ });
782
+ });