@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,179 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import AdmZip from "adm-zip";
3
+ import {
4
+ unpackArchive,
5
+ archiveFormat,
6
+ formatExtractedFiles,
7
+ } from "../src/archive-unpacker.js";
8
+ import type { ArtifactInput } from "../src/artifact-analyzer.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function makeZipBuffer(entries: Record<string, string>): Buffer {
15
+ const zip = new AdmZip();
16
+ for (const [path, content] of Object.entries(entries)) {
17
+ zip.addFile(path, Buffer.from(content, "utf-8"));
18
+ }
19
+ return zip.toBuffer();
20
+ }
21
+
22
+ function makeArtifact(name: string): ArtifactInput {
23
+ return { name, source: "test" };
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // archiveFormat
28
+ // ---------------------------------------------------------------------------
29
+
30
+ describe("archiveFormat", () => {
31
+ it("maps zip to zip", () => {
32
+ expect(archiveFormat("zip", "bundle.zip")).toBe("zip");
33
+ });
34
+
35
+ it("maps nupkg to zip", () => {
36
+ expect(archiveFormat("nupkg", "MyService.1.0.0.nupkg")).toBe("zip");
37
+ });
38
+
39
+ it("maps java-archive to zip", () => {
40
+ expect(archiveFormat("java-archive", "app.jar")).toBe("zip");
41
+ });
42
+
43
+ it("maps python-package to zip", () => {
44
+ expect(archiveFormat("python-package", "app-1.0.0-py3-none-any.whl")).toBe("zip");
45
+ });
46
+
47
+ it("maps .tar.gz tarball to tar-gz", () => {
48
+ expect(archiveFormat("tarball", "release.tar.gz")).toBe("tar-gz");
49
+ });
50
+
51
+ it("maps .tgz tarball to tar-gz", () => {
52
+ expect(archiveFormat("tarball", "release.tgz")).toBe("tar-gz");
53
+ });
54
+
55
+ it("maps bare .tar to tar", () => {
56
+ expect(archiveFormat("tarball", "image.tar")).toBe("tar");
57
+ });
58
+
59
+ it("returns null for non-archive types", () => {
60
+ expect(archiveFormat("dockerfile", "Dockerfile")).toBeNull();
61
+ expect(archiveFormat("node-package", "package.json")).toBeNull();
62
+ expect(archiveFormat("unknown", "mystery.dat")).toBeNull();
63
+ });
64
+ });
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // ZIP unpacking
68
+ // ---------------------------------------------------------------------------
69
+
70
+ describe("unpackArchive — zip", () => {
71
+ it("extracts markdown and yaml files", async () => {
72
+ const buf = makeZipBuffer({
73
+ "README.md": "# My Service\nDeploys to Kubernetes.",
74
+ "helm/values.yaml": "replicaCount: 3\nimage: myapp:latest",
75
+ "app.bin": "\x00\x01\x02\x03binary data",
76
+ });
77
+
78
+ const result = await unpackArchive(buf, "zip");
79
+ const paths = result.files.map((f) => f.path);
80
+
81
+ expect(paths).toContain("README.md");
82
+ expect(paths).toContain("helm/values.yaml");
83
+ expect(paths).not.toContain("app.bin");
84
+ });
85
+
86
+ it("extracts Dockerfile and shell scripts", async () => {
87
+ const buf = makeZipBuffer({
88
+ "Dockerfile": "FROM node:20-alpine\nEXPOSE 3000",
89
+ "deploy.sh": "#!/bin/bash\ndocker build .",
90
+ });
91
+
92
+ const result = await unpackArchive(buf, "zip");
93
+ const paths = result.files.map((f) => f.path);
94
+
95
+ expect(paths).toContain("Dockerfile");
96
+ expect(paths).toContain("deploy.sh");
97
+ });
98
+
99
+ it("skips node_modules and .git paths", async () => {
100
+ const buf = makeZipBuffer({
101
+ "node_modules/express/README.md": "Express docs",
102
+ ".git/config": "git config",
103
+ "SYNTH.md": "deployment context",
104
+ });
105
+
106
+ const result = await unpackArchive(buf, "zip");
107
+ const paths = result.files.map((f) => f.path);
108
+
109
+ expect(paths).not.toContain("node_modules/express/README.md");
110
+ expect(paths).not.toContain(".git/config");
111
+ expect(paths).toContain("SYNTH.md");
112
+ });
113
+
114
+ it("counts skipped binary and filtered files", async () => {
115
+ const buf = makeZipBuffer({
116
+ "app.exe": "\x00\x00\x4D\x5A binary",
117
+ "logo.png": "\x89PNG binary",
118
+ "config.yaml": "key: value",
119
+ });
120
+
121
+ const result = await unpackArchive(buf, "zip");
122
+ expect(result.files.length).toBe(1);
123
+ expect(result.skipped).toBeGreaterThan(0);
124
+ });
125
+
126
+ it("returns empty result for corrupt buffer", async () => {
127
+ const result = await unpackArchive(Buffer.from("not a zip"), "zip");
128
+ expect(result.files).toHaveLength(0);
129
+ expect(result.skipped).toBe(0);
130
+ });
131
+
132
+ it("preserves file content accurately", async () => {
133
+ const content = "# Deploy Notes\n\nRun after database migration.\n";
134
+ const buf = makeZipBuffer({ "NOTES.md": content });
135
+
136
+ const result = await unpackArchive(buf, "zip");
137
+ expect(result.files[0].content).toBe(content);
138
+ });
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // formatExtractedFiles
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe("formatExtractedFiles", () => {
146
+ it("formats multiple files with separators", () => {
147
+ const result = formatExtractedFiles({
148
+ files: [
149
+ { path: "README.md", content: "# Hello" },
150
+ { path: "values.yaml", content: "replicas: 2" },
151
+ ],
152
+ skipped: 0,
153
+ });
154
+
155
+ expect(result).toContain("=== README.md ===");
156
+ expect(result).toContain("# Hello");
157
+ expect(result).toContain("=== values.yaml ===");
158
+ expect(result).toContain("replicas: 2");
159
+ });
160
+
161
+ it("includes skipped file count when non-zero", () => {
162
+ const result = formatExtractedFiles({ files: [], skipped: 3 });
163
+ expect(result).toContain("(no readable text files found in archive)");
164
+ });
165
+
166
+ it("appends skip count when files also present", () => {
167
+ const result = formatExtractedFiles({
168
+ files: [{ path: "README.md", content: "hi" }],
169
+ skipped: 5,
170
+ });
171
+
172
+ expect(result).toContain("5 binary or oversized files skipped");
173
+ });
174
+
175
+ it("returns no-files message when empty", () => {
176
+ const result = formatExtractedFiles({ files: [], skipped: 0 });
177
+ expect(result).toContain("no readable text files found");
178
+ });
179
+ });
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { DecisionDebrief } from "@synth-deploy/core";
3
+ import { ArtifactAnalyzer, createArtifactAnalyzer, detectArtifactType } from "../src/artifact-analyzer.js";
4
+ import type { ArtifactInput } from "../src/artifact-analyzer.js";
5
+ import type { LlmClient } from "@synth-deploy/core";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock LLM client
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function createMockLlm(opts: { available?: boolean; response?: string } = {}): LlmClient {
12
+ const available = opts.available ?? true;
13
+ const response = opts.response ?? "{}";
14
+
15
+ return {
16
+ isAvailable: () => available,
17
+ reason: vi.fn().mockResolvedValue(
18
+ available
19
+ ? { ok: true, text: response, model: "test-model", responseTimeMs: 100 }
20
+ : { ok: false, fallback: true, reason: "LLM not configured" },
21
+ ),
22
+ classify: vi.fn().mockResolvedValue({ ok: false, fallback: true, reason: "not used" }),
23
+ healthCheck: vi.fn().mockResolvedValue({ configured: false, healthy: false }),
24
+ getLastHealthStatus: vi.fn().mockReturnValue(null),
25
+ } as unknown as LlmClient;
26
+ }
27
+
28
+ function makeArtifact(overrides: Partial<ArtifactInput> = {}): ArtifactInput {
29
+ return {
30
+ name: "test-artifact",
31
+ source: "test-registry",
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function bufferFrom(text: string): Buffer {
37
+ return Buffer.from(text, "utf-8");
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Type detection
42
+ // ---------------------------------------------------------------------------
43
+
44
+ describe("detectArtifactType", () => {
45
+ it("detects Dockerfile by name", () => {
46
+ expect(detectArtifactType(makeArtifact({ name: "Dockerfile" }))).toBe("dockerfile");
47
+ });
48
+
49
+ it("detects Chart.yaml", () => {
50
+ expect(detectArtifactType(makeArtifact({ name: "Chart.yaml" }))).toBe("helm-chart");
51
+ });
52
+
53
+ it("detects values.yaml", () => {
54
+ expect(detectArtifactType(makeArtifact({ name: "values.yaml" }))).toBe("helm-values");
55
+ });
56
+
57
+ it("detects package.json", () => {
58
+ expect(detectArtifactType(makeArtifact({ name: "package.json" }))).toBe("node-package");
59
+ });
60
+
61
+ it("detects Makefile", () => {
62
+ expect(detectArtifactType(makeArtifact({ name: "Makefile" }))).toBe("makefile");
63
+ });
64
+
65
+ it("detects .tgz", () => {
66
+ expect(detectArtifactType(makeArtifact({ name: "app-1.0.0.tgz" }))).toBe("tarball");
67
+ });
68
+
69
+ it("detects .tar", () => {
70
+ expect(detectArtifactType(makeArtifact({ name: "image.tar" }))).toBe("tarball");
71
+ });
72
+
73
+ it("detects .zip", () => {
74
+ expect(detectArtifactType(makeArtifact({ name: "deploy.zip" }))).toBe("zip");
75
+ });
76
+
77
+ it("detects .nupkg", () => {
78
+ expect(detectArtifactType(makeArtifact({ name: "MyService.1.0.0.nupkg" }))).toBe("nupkg");
79
+ });
80
+
81
+ it("detects .jar", () => {
82
+ expect(detectArtifactType(makeArtifact({ name: "app.jar" }))).toBe("java-archive");
83
+ });
84
+
85
+ it("respects explicit type override", () => {
86
+ expect(detectArtifactType(makeArtifact({ name: "build-config", type: "dockerfile" }))).toBe("dockerfile");
87
+ });
88
+
89
+ it("returns unknown for unrecognized files", () => {
90
+ expect(detectArtifactType(makeArtifact({ name: "mystery.dat" }))).toBe("unknown");
91
+ });
92
+ });
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // LLM analysis
96
+ // ---------------------------------------------------------------------------
97
+
98
+ describe("ArtifactAnalyzer — LLM analysis", () => {
99
+ it("returns llm method when LLM produces valid analysis", async () => {
100
+ const llmResponse = JSON.stringify({
101
+ summary: "A Node.js API server with PostgreSQL dependency",
102
+ dependencies: ["npm:express", "system:postgresql-15"],
103
+ configurationExpectations: { DATABASE_URL: "PostgreSQL connection string", PORT: "HTTP port" },
104
+ deploymentIntent: "Containerized Node.js API deployment",
105
+ confidence: 0.92,
106
+ });
107
+
108
+ const analyzer = createArtifactAnalyzer({
109
+ llm: createMockLlm({ available: true, response: llmResponse }),
110
+ debrief: new DecisionDebrief(),
111
+ });
112
+
113
+ const result = await analyzer.analyze(
114
+ makeArtifact({ name: "package.json", content: bufferFrom('{"name":"api","version":"1.0.0"}') }),
115
+ );
116
+
117
+ expect(result.method).toBe("llm");
118
+ expect(result.analysis.summary).toContain("Node.js API server");
119
+ expect(result.analysis.dependencies).toContain("npm:express");
120
+ expect(result.analysis.dependencies).toContain("system:postgresql-15");
121
+ expect(result.analysis.configurationExpectations["DATABASE_URL"]).toBeDefined();
122
+ expect(result.analysis.confidence).toBe(0.92);
123
+ });
124
+
125
+ it("handles LLM response wrapped in markdown code blocks", async () => {
126
+ const wrappedResponse = `Here is the analysis:\n\n\`\`\`json\n{\n "summary": "A Python web service",\n "dependencies": ["pip:flask"],\n "configurationExpectations": {},\n "deploymentIntent": "Python WSGI deployment",\n "confidence": 0.8\n}\n\`\`\``;
127
+
128
+ const analyzer = createArtifactAnalyzer({
129
+ llm: createMockLlm({ available: true, response: wrappedResponse }),
130
+ debrief: new DecisionDebrief(),
131
+ });
132
+
133
+ const result = await analyzer.analyze(makeArtifact({ name: "app.tar.gz" }));
134
+
135
+ expect(result.method).toBe("llm");
136
+ expect(result.analysis.summary).toContain("Python web service");
137
+ });
138
+
139
+ it("passes artifact content and type to the LLM", async () => {
140
+ const llmResponse = JSON.stringify({
141
+ summary: "Container image",
142
+ dependencies: [],
143
+ configurationExpectations: {},
144
+ deploymentIntent: "Container deployment",
145
+ confidence: 0.9,
146
+ });
147
+
148
+ const mockLlm = createMockLlm({ available: true, response: llmResponse });
149
+ const analyzer = createArtifactAnalyzer({ llm: mockLlm, debrief: new DecisionDebrief() });
150
+
151
+ const content = "FROM node:20-alpine\nEXPOSE 3000\n";
152
+ await analyzer.analyze(makeArtifact({ name: "Dockerfile", content: bufferFrom(content) }));
153
+
154
+ const reasonCall = (mockLlm.reason as ReturnType<typeof vi.fn>).mock.calls[0][0];
155
+ expect(reasonCall.prompt).toContain("Dockerfile");
156
+ expect(reasonCall.prompt).toContain("dockerfile");
157
+ expect(reasonCall.prompt).toContain("FROM node:20-alpine");
158
+ });
159
+
160
+ it("records a debrief entry for every analysis", async () => {
161
+ const debrief = new DecisionDebrief();
162
+ const llmResponse = JSON.stringify({
163
+ summary: "A service",
164
+ dependencies: [],
165
+ configurationExpectations: {},
166
+ confidence: 0.7,
167
+ });
168
+
169
+ const analyzer = createArtifactAnalyzer({
170
+ llm: createMockLlm({ available: true, response: llmResponse }),
171
+ debrief,
172
+ });
173
+
174
+ await analyzer.analyze(makeArtifact({ name: "Dockerfile", content: bufferFrom("FROM alpine") }));
175
+
176
+ const entries = debrief.getByType("artifact-analysis");
177
+ expect(entries.length).toBe(1);
178
+ expect(entries[0].decision).toContain("Dockerfile");
179
+ expect(entries[0].context).toHaveProperty("method", "llm");
180
+ });
181
+ });
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // LLM unavailable
185
+ // ---------------------------------------------------------------------------
186
+
187
+ describe("ArtifactAnalyzer — LLM unavailable", () => {
188
+ it("returns unavailable method when LLM is not configured", async () => {
189
+ const analyzer = createArtifactAnalyzer({
190
+ llm: createMockLlm({ available: false }),
191
+ debrief: new DecisionDebrief(),
192
+ });
193
+
194
+ const result = await analyzer.analyze(makeArtifact({ name: "Dockerfile" }));
195
+
196
+ expect(result.method).toBe("unavailable");
197
+ expect(result.analysis.confidence).toBe(0);
198
+ expect(result.analysis.summary).toContain("LLM is required");
199
+ });
200
+
201
+ it("returns unavailable when LLM returns invalid JSON", async () => {
202
+ const analyzer = createArtifactAnalyzer({
203
+ llm: createMockLlm({ available: true, response: "I cannot produce valid JSON right now." }),
204
+ debrief: new DecisionDebrief(),
205
+ });
206
+
207
+ const result = await analyzer.analyze(makeArtifact({ name: "Dockerfile" }));
208
+
209
+ expect(result.method).toBe("unavailable");
210
+ expect(result.analysis.confidence).toBe(0);
211
+ });
212
+
213
+ it("records a debrief entry even when unavailable", async () => {
214
+ const debrief = new DecisionDebrief();
215
+ const analyzer = createArtifactAnalyzer({
216
+ llm: createMockLlm({ available: false }),
217
+ debrief,
218
+ });
219
+
220
+ await analyzer.analyze(makeArtifact({ name: "app.zip" }));
221
+
222
+ const entries = debrief.getByType("artifact-analysis");
223
+ expect(entries.length).toBe(1);
224
+ expect(entries[0].context).toHaveProperty("method", "unavailable");
225
+ });
226
+ });
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Factory
230
+ // ---------------------------------------------------------------------------
231
+
232
+ describe("createArtifactAnalyzer", () => {
233
+ it("returns an ArtifactAnalyzer instance", () => {
234
+ const analyzer = createArtifactAnalyzer({
235
+ llm: createMockLlm(),
236
+ debrief: new DecisionDebrief(),
237
+ });
238
+ expect(analyzer).toBeInstanceOf(ArtifactAnalyzer);
239
+ });
240
+ });
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import Fastify from "fastify";
3
+ import type { FastifyInstance } from "fastify";
4
+ import { SignJWT } from "jose";
5
+ import {
6
+ UserStore,
7
+ RoleStore,
8
+ UserRoleStore,
9
+ SessionStore,
10
+ } from "@synth-deploy/core";
11
+ import type { UserId, RoleId } from "@synth-deploy/core";
12
+ import { registerAuthMiddleware, generateTokens } from "../src/middleware/auth.js";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const JWT_SECRET = new TextEncoder().encode("test-secret");
19
+ const TEST_USER_ID = "user-1" as UserId;
20
+ const TEST_ROLE_ID = "role-admin" as RoleId;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function createStores() {
27
+ const userStore = new UserStore();
28
+ const roleStore = new RoleStore();
29
+ const userRoleStore = new UserRoleStore(roleStore);
30
+ const sessionStore = new SessionStore();
31
+ return { userStore, roleStore, userRoleStore, sessionStore };
32
+ }
33
+
34
+ function seedTestUser(stores: ReturnType<typeof createStores>) {
35
+ stores.userStore.create({
36
+ id: TEST_USER_ID,
37
+ email: "test@example.com",
38
+ name: "Test User",
39
+ passwordHash: "hashed",
40
+ createdAt: new Date(),
41
+ updatedAt: new Date(),
42
+ });
43
+
44
+ stores.roleStore.create({
45
+ id: TEST_ROLE_ID,
46
+ name: "admin",
47
+ permissions: ["deployment.view"],
48
+ isBuiltIn: true,
49
+ createdAt: new Date(),
50
+ });
51
+
52
+ stores.userRoleStore.assign(TEST_USER_ID, TEST_ROLE_ID, TEST_USER_ID);
53
+ }
54
+
55
+ async function buildApp(): Promise<{
56
+ app: FastifyInstance;
57
+ stores: ReturnType<typeof createStores>;
58
+ token: string;
59
+ }> {
60
+ const stores = createStores();
61
+ seedTestUser(stores);
62
+
63
+ const app = Fastify({ logger: false });
64
+ registerAuthMiddleware(app, stores.userStore, stores.userRoleStore, stores.sessionStore, JWT_SECRET);
65
+
66
+ // Health endpoint — exempt from auth
67
+ app.get("/health", async () => ({ status: "ok" }));
68
+
69
+ // Protected endpoint
70
+ app.get("/api/partitions", async () => ({ partitions: [] }));
71
+
72
+ await app.ready();
73
+
74
+ // Generate a valid token and persist the session
75
+ const { token, refreshToken, expiresAt } = await generateTokens(TEST_USER_ID, JWT_SECRET);
76
+ stores.sessionStore.create({
77
+ id: "session-1",
78
+ userId: TEST_USER_ID,
79
+ token,
80
+ refreshToken,
81
+ expiresAt,
82
+ createdAt: new Date(),
83
+ });
84
+
85
+ return { app, stores, token };
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Tests — auth middleware (JWT-based, always enabled)
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe("auth middleware — enabled", () => {
93
+ let app: FastifyInstance;
94
+ let token: string;
95
+
96
+ beforeEach(async () => {
97
+ const built = await buildApp();
98
+ app = built.app;
99
+ token = built.token;
100
+ });
101
+
102
+ afterEach(async () => {
103
+ await app.close();
104
+ });
105
+
106
+ it("returns 401 when Authorization header is missing", async () => {
107
+ const res = await app.inject({
108
+ method: "GET",
109
+ url: "/api/partitions",
110
+ });
111
+
112
+ expect(res.statusCode).toBe(401);
113
+ expect(JSON.parse(res.payload)).toEqual({ error: "Authentication required" });
114
+ });
115
+
116
+ it("returns 401 when Authorization header is malformed (no Bearer prefix)", async () => {
117
+ const res = await app.inject({
118
+ method: "GET",
119
+ url: "/api/partitions",
120
+ headers: { authorization: "Basic some-credentials" },
121
+ });
122
+
123
+ expect(res.statusCode).toBe(401);
124
+ expect(JSON.parse(res.payload)).toEqual({ error: "Authentication required" });
125
+ });
126
+
127
+ it("returns 401 when Bearer token is invalid", async () => {
128
+ const res = await app.inject({
129
+ method: "GET",
130
+ url: "/api/partitions",
131
+ headers: { authorization: "Bearer not-a-valid-jwt" },
132
+ });
133
+
134
+ expect(res.statusCode).toBe(401);
135
+ expect(JSON.parse(res.payload)).toEqual({ error: "Invalid token" });
136
+ });
137
+
138
+ it("passes request through with a valid JWT token", async () => {
139
+ const res = await app.inject({
140
+ method: "GET",
141
+ url: "/api/partitions",
142
+ headers: { authorization: `Bearer ${token}` },
143
+ });
144
+
145
+ expect(res.statusCode).toBe(200);
146
+ expect(JSON.parse(res.payload)).toEqual({ partitions: [] });
147
+ });
148
+
149
+ it("allows /health without any authorization", async () => {
150
+ const res = await app.inject({
151
+ method: "GET",
152
+ url: "/health",
153
+ });
154
+
155
+ expect(res.statusCode).toBe(200);
156
+ expect(JSON.parse(res.payload)).toEqual({ status: "ok" });
157
+ });
158
+
159
+ it("allows /health even with an invalid token", async () => {
160
+ const res = await app.inject({
161
+ method: "GET",
162
+ url: "/health",
163
+ headers: { authorization: "Bearer totally-wrong" },
164
+ });
165
+
166
+ // /health bypasses auth entirely — should still succeed
167
+ expect(res.statusCode).toBe(200);
168
+ });
169
+ });
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Tests — registerAuthMiddleware return value
173
+ // ---------------------------------------------------------------------------
174
+
175
+ describe("registerAuthMiddleware return value", () => {
176
+ it("always returns { enabled: true }", () => {
177
+ const stores = createStores();
178
+ const app = Fastify({ logger: false });
179
+ const result = registerAuthMiddleware(
180
+ app,
181
+ stores.userStore,
182
+ stores.userRoleStore,
183
+ stores.sessionStore,
184
+ JWT_SECRET,
185
+ );
186
+ expect(result.enabled).toBe(true);
187
+ app.close();
188
+ });
189
+ });